# Exploit verification ledger **What this is:** results of actually *running each exploit* against a genuinely vulnerable VM and confirming `uid=0` **out of band** (an independent root-owned write / `/etc/shadow` read / setuid-bash sentinel — never the module's own self-report). This is distinct from `docs/VERIFICATIONS.jsonl`'s historical records, which only checked that `detect()` returns the right verdict. Harness: rootless qemu/KVM over frozen point-release cloud images (unpatched → vulnerable by default), driver in the session scratch dir. Root witnessed via `witness.sh` (shell-probe + setuid-bash finisher + module sentinels + `/etc/passwd` tamper check). > **Headline finding:** the corpus was only ever *detect*-verified, never > *exploit*-verified. Running the exploits shows a mix of genuinely-working, > honestly-failing, and **falsely-succeeding** modules. Two flagship modules > reported `EXPLOIT_OK` while obtaining **no root at all** (`pwnkit`, > `ptrace_traceme`) — a false positive from the dispatcher's "execve > transferred → clean child exit = OK" path. Both are addressed below. ## Confirmed landing root (uid=0 witnessed out of band) | module | CVE | target | notes | |---|---|---|---| | `refluxfs` | CVE-2026-64600 | Rocky 9.8 / 5.14.0-687.el9 | full chain, `/etc/passwd` → root (earlier) | | `overlayfs` | CVE-2021-3493 | Ubuntu 20.04.0 / 5.4.0-26 | userns + xattr copy-up, clean | | `overlayfs_setuid` | CVE-2023-0386 | Ubuntu 22.04.0 / 5.15.0-25 | **after a full rewrite** — see below | | `pwnkit` | CVE-2021-4034 | Ubuntu 20.04.0 / polkit 0.105-26ubuntu1 | **after a fix** — see below | | `sudo_runas_neg1` | CVE-2019-14287 | Ubuntu 18.04.2 / sudo 1.8.21p2 + sudoers `(ALL,!root)` | `sudo -u#-1` → uid 0 | | `sudoedit_editor` | CVE-2023-22809 | Ubuntu 22.04.0 / sudo 1.9.9 + sudoers `sudoedit` grant | **after 2 fixes** — `chdir("/")` + helper basename match; `su skel` → uid 0 | | `sudo_host` | CVE-2025-32462 | Ubuntu 22.04.0 / sudo 1.9.9 + host-restricted sudoers rule | works as shipped; `sudo -h ` → uid 0 (needs a host-scoped rule + resolvable host) | | `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.0 / 4.15.0-50 + pkexec + active-session polkit | **after a full rewrite** — `skeletonkey --exploit ptrace_traceme` (uid 1000) → root-owned setuid bash. See below | | `sudo_samedit` | CVE-2021-3156 | Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27 | **after a full rewrite** — Baron Samedit; `skeletonkey --exploit sudo_samedit` (uid 1000, non-sudoer) → root-owned setuid bash. See below | ## Fixed this session - **`pwnkit`** — reported `EXPLOIT_OK` but did **not** root (glibc "Could not open converter … to PWNKIT"). Root cause: missing the `GCONV_PATH=.` re-injection directory + `chdir(workdir)`. Fixed → now lands real root on a vulnerable host. (commit `24b839e`) - **`ptrace_traceme`** (CVE-2019-13272) — first made **honest** (it had reported a false `EXPLOIT_OK` with a placeholder that had the mechanism *backwards* — attaching to the parent), then **rewritten and now lands real root** (uid=0 witnessed out-of-band on Ubuntu 18.04.0 / 4.15.0-50). The correct mechanism is the reverse of the old placeholder: a *middle* process execs setuid `pkexec` (euid 0 for a window); its *child* spins until it sees that euid-0, calls `PTRACE_TRACEME` (recording the parent's **root** creds as its ptracer_cred — the bug), then execs `pkexec` itself — the traced setuid exec is **not degraded** because ptracer_cred is root, so the child becomes real root, and a staged `execveat()` self-re-exec injects the payload. Ported the proven Jann Horn / bcoles PoC verbatim (only `spawn_shell()` changed, to plant a root-owned proof + setuid bash), embedded as `ptrace_helper_src.h`, compiled on the target at runtime with unique `-DSK_PROOF/-DSK_ROOTBASH` paths, run, and verified by `stat()`-ing the root-owned artifacts. **Real-world precondition** (honestly reported): pkexec must *authorize* an auto-discovered `implicit-active=yes` helper, which needs an **active local session** (desktop) or an equivalently permissive polkit policy; over a bare *inactive* ssh session pkexec returns "Not authorized" and the module reports `EXPLOIT_FAIL` with that diagnosis. On the headless VM this was isolated with a permissive `pkla` for the backlight helper action — the kernel bug and the whole technique are confirmed; the gate is polkit, not the exploit. - **`overlayfs_setuid`** (CVE-2023-0386) — **rewritten and now lands real root** (uid=0 witnessed out-of-band on Ubuntu 22.04.0 / 5.15.0-25). The shipped module used a bogus `chown`-the-merged-view technique that never worked. The real bug needs a **FUSE lower layer** exporting a setuid-root file; overlay copy-up then materialises it in the real upper as a genuine setuid-root binary. Key findings from the port (all four were required): 1. Overlay refuses a **userns-mounted** FUSE lowerdir (ENOSYS) — FUSE must be mounted in the **init ns** via the setuid `fusermount` helper (libfuse does this). A raw `/dev/fuse` server was tried and abandoned: its INIT handshake needs `poll()` on the non-blocking fd, and a malformed reply destabilised the kernel — fragile and inappropriate. libfuse is linked conditionally (pkg-config `fuse`/`fuse3`), matching `pack2theroot`. 2. **fuse2** low-level API (`fuse_mount`/`fuse_new`/`fuse_loop_mt`, empty args) — `fuse_main` advertises splice/copy_file_range caps that make the kernel attempt `copy_file_range` at copy-up → ENOSYS with no fallback. 3. **`read_buf`** callback (copy-up's splice read path). 4. **`ioctl`** callback — copy-up issues `FS_IOC_GETFLAGS` on the lower; a server without an ioctl handler returns ENOSYS and copy-up fails. This was the last missing piece. Debugging was isolated by driving the exploit orchestration against the public PoC's `./fuse`, then swapping servers, then comparing `fops`. - **`sudo_samedit`** (CVE-2021-3156, "Baron Samedit") — the corpus's hardest userspace target, **rewritten and now lands real root** (uid=0 witnessed out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as an unprivileged non-sudoer). The shipped module drove a structural trigger with no offsets and honestly reported `EXPLOIT_FAIL`. Ported blasty's technique: the `sudoedit -s` unescape overflow overwrites a glibc NSS `service_user`, so the lookup dlopen's an attacker-planted `libnss_X/'P0P_SH3LLZ_ .so.2'` from CWD; its constructor runs while sudo is still root. The module compiles the NSS payload on the target (unique `-DSK_PROOF/-DSK_ROOTBASH`), lays out the `libnss_X/` dir, execs sudoedit with the crafted argv/env (per-libc grooming lengths: Ubuntu 56/54/63/212, Debian 64/49/60/214), and verifies root by `stat()`-ing the artifacts. Primary lengths landed first try; a `null_stomp_len` sweep (±8, the axis blasty's brute.sh perturbs) is the fallback for libc drift. Needs cc on the target. - **`cgroup_release_agent`** — two real bugs fixed (commit `8c45b2b`): it read `getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1 mount EPERM). Now the userns+cgroupns+mount setup is correct. It still can't root a **bare** unprivileged user on a stock systemd host: every v1 controller is pre-mounted (its `release_agent` is init-owned → EACCES from the userns) and a fresh named hierarchy is refused. Reachable in a **container** context (CAP_SYS_ADMIN / an ownable cgroup) — matches its "host root from rootless container" framing. The `getuid()`-after-`unshare` bug is a pattern to grep for across the other userns modules. ## Needs a faithful PoC port (genuinely vulnerable target, exploit doesn't land) | module | CVE | target tested | what's wrong | |---|---|---|---| | `overlayfs_setuid` | CVE-2023-0386 | Ubuntu 22.04.0 / 5.15.0-25.25 | **Kernel confirmed vulnerable empirically** — the upstream PoC (xkaneiki, libfuse) pops root here (`uid=0(root)`, root-owned witness). The working technique: mount a FUSE fs exporting `/file` (st_uid=0, mode 04777) in the **init ns** via the setuid `fusermount3` helper, then overlay-in-userns with that FUSE lowerdir + copy-up. My module's non-FUSE `chown` variant yields `upper/file` uid=1000 (no escalation); mounting FUSE **inside** the userns → overlay `ENOSYS`. Attempted a self-contained **raw `/dev/fuse`** port: got the `fusermount` fd-passing handshake (`SCM_RIGHTS`) + mount working, but the server hits `EINVAL` on `read()` after `FUSE_INIT` (non-blocking fd → needs `poll()`), and even with poll/buffer fixes the raw server serving was flaky and repeatedly **wedged/rebooted the VM** — i.e. the raw protocol reimplementation is fragile and can destabilise the target, which is *worse* for the corpus than a lib dependency. **Conclusion: use libfuse** (proven, robust; matches the `pack2theroot` conditional-lib precedent). Port is scoped and ready; needs a clean session to implement + verify. | | *(none left in this table — `sudo_samedit` was the last, now working; see "Fixed this session")* | | | | ## Inconclusive (detect version-blind vs vendor backport) | module | CVE | note | |---|---|---| | `dirty_pipe` | CVE-2022-0847 | Ubuntu 22.04.0's 5.15.0-25.25 almost certainly carries the backported fix (USN-5317); `detect()` is version-only so says VULNERABLE. Needs a genuinely pre-fix kernel (e.g. 5.13.0-19, or mainline 5.16.10) to verify the exploit. | ## Kernel primitives — offset path fixed; `nf_tables` gap scoped (this session) **Resolver bug fixed (`core/offsets.c`, commit `cd9bea6`).** The documented env-var offset override (`SKELETONKEY_MODPROBE_PATH` etc.) was **silently wiped on every default host**: `parse_symfile` reads `/proc/kallsyms`, which returns all-zero addresses under `kptr_restrict`, and then *unconditionally* zeroed `modprobe_path`/`init_task` — clobbering the values `apply_env` had just set. Net effect: every `--full-chain` primitive reported "offsets couldn't be resolved" even with correct offsets supplied. Now the all-zero path only clears fields it tagged `OFFSETS_FROM_KALLSYMS` itself. **This was the blocker for the entire primitive full-chain path.** Verified fixed on Ubuntu 22.04.0 / 5.15.0-25: `--full-chain` now prints `modprobe_path=0x… (env)`, the finisher engages, and the arb-write fires. **`nf_tables` (CVE-2024-1086) — kernel CONFIRMED vulnerable; module gap scoped.** Followed the full methodology (test → confirm kernel → pull PoC → diff): - **Kernel is genuinely vulnerable.** Built Notselwyn's public universal PoC (`github.com/Notselwyn/CVE-2024-1086`, musl-static) on jammy 5.15.0-25 (below the patched branch 5.15.149) and ran it: it drove the exploit and hit the deliberate post-exploitation `kernel BUG at mm/slub.c:379` / `Kernel panic` — i.e. the cross-cache slab corruption fired. Kernel confirmed exploitable. - **The difference.** The module (its own header is honest about this) is a **trigger + groom scaffold**: it builds the `NFT_GOTO+NFT_DROP` verdict combo that `nft_verdict_init()` fails to reject, fires the double-free, and runs the `msg_msg` cg-96 groom — all real. But its arb-write is "FALLBACK-DEPTH": the exact `pipapo_elem` layout + value-pointer offset needed to redirect the write at `modprobe_path` is a documented TODO, so the write doesn't land → honest `EXPLOIT_FAIL`. Notselwyn's working exploit uses a *different, heavier* technique entirely — **universal cross-cache → dirty-pagetable** (arbitrary physical R/W, no per-kernel offsets), ~2000 LOC across multiple files with static `libnftnl`/`libmnl`. - **Scope of the remaining fix.** Making `nf_tables --full-chain` land root means either (a) completing the module's own per-kernel `pipapo_elem` arb-write layout, or (b) porting Notselwyn's universal technique. Both are substantial dedicated exploit-dev — this is the hardest module in the corpus, not a spot-the-bug fix. The offset resolver (above) is the piece that was actually broken and is now fixed + pushed. - **Other 🟡 kernel primitives** (`nft_set_uaf`, `nft_payload`, `nft_fwd_dup`, `netfilter_xtcompat`, `af_packet`, `af_packet2`, `af_unix_gc`, `cls_route4`, `fuse_legacy`, `stackrot`, `sequoia`, `nft_pipapo`, `vsock_uaf`, `pintheft`): same shape — real trigger/groom scaffolds returning `EXPLOIT_FAIL` by design. The resolver fix unblocks feeding them offsets; each still needs its arb-write primitive completed against a matching vulnerable kernel. - **Structural userspace** (`sudoedit_editor`, `sudo_chwoot`, `sudo_host`): need specific sudo versions + sudoers config; likely tractable. - **2026 CVEs** (`copy_fail` ×5, `dirtydecrypt`, `fragnesia`, `cifswitch`, `nft_catchall`, `ptrace_pidfd`): need vulnerable 2026 kernels; the reconstructed race triggers (`bad_epoll`, `ghostlock`, `nft_catchall`) are deliberately under-driven and won't pop root by design. - **Environment-blocked**: `vmwgfx` (VMware guest only), `dirty_cow` (needs ≤4.4), `mutagen_astronomy` (CentOS 6 / Debian 7). - **D-Bus/desktop** (`pack2theroot`, `udisks_libblockdev`): need the polkit/D-Bus stack + a provisioner rule. ## Method notes for continuation - Frozen images: `cloud-images-archive.ubuntu.com/releases//release-/` are unpatched and vulnerable-by-default for CVEs disclosed after that date — far easier than downgrading packages on current images. - gcc must be present *in* the VM (several exploits compile payloads at runtime); on EOL LTS, point apt at the archive main pocket. - **Always verify root out of band.** The module self-report is not trustworthy (two flagships lied). `witness.sh` is the reference check.