The bundled sequence had the mechanism backwards (it PTRACE_ATTACHed the now-root parent) and never rooted anything. Replaced it with the proven Jann Horn (Project Zero #1903) / bcoles technique: - a "middle" process execs setuid pkexec (euid 0 for a window); - its child spins until it sees middle is euid 0, calls PTRACE_TRACEME (recording middle's ROOT creds as 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, still traced; - staged execveat() self-re-exec injects the payload as root. The staged self-re-exec needs the exploit to exist as its own binary with an argv[0] stage dispatcher, so the proven PoC is embedded verbatim (ptrace_helper_src.h — only spawn_shell() changed, to plant a root-owned proof + setuid bash instead of only an interactive shell), compiled on the target at runtime with unique -DSK_PROOF/-DSK_ROOTBASH paths, run, and verified by stat()'ing the root-owned artifacts (never self-report). Verified out-of-band on Ubuntu 18.04.0 / 4.15.0-50: `skeletonkey --exploit ptrace_traceme` as uid 1000 -> EXPLOIT_OK + a -rwsr-xr-x root:root bash. Real-world precondition, honestly reported: pkexec must authorize an auto-discovered implicit-active=yes helper, which needs an active local session (desktop) or a permissive polkit policy; over inactive ssh it returns "Not authorized" and the module reports EXPLOIT_FAIL with that diagnosis. Gated on a C compiler + pkexec; x86_64 only (register-level injection). cleanup() removes the artifacts. Unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
12 KiB
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_OKwhile 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 <host> → 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 |
Fixed this session
pwnkit— reportedEXPLOIT_OKbut did not root (glibc "Could not open converter … to PWNKIT"). Root cause: missing theGCONV_PATH=.re-injection directory +chdir(workdir). Fixed → now lands real root on a vulnerable host. (commit24b839e)ptrace_traceme(CVE-2019-13272) — first made honest (it had reported a falseEXPLOIT_OKwith 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 setuidpkexec(euid 0 for a window); its child spins until it sees that euid-0, callsPTRACE_TRACEME(recording the parent's root creds as its ptracer_cred — the bug), then execspkexecitself — the traced setuid exec is not degraded because ptracer_cred is root, so the child becomes real root, and a stagedexecveat()self-re-exec injects the payload. Ported the proven Jann Horn / bcoles PoC verbatim (onlyspawn_shell()changed, to plant a root-owned proof + setuid bash), embedded asptrace_helper_src.h, compiled on the target at runtime with unique-DSK_PROOF/-DSK_ROOTBASHpaths, run, and verified bystat()-ing the root-owned artifacts. Real-world precondition (honestly reported): pkexec must authorize an auto-discoveredimplicit-active=yeshelper, 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 reportsEXPLOIT_FAILwith that diagnosis. On the headless VM this was isolated with a permissivepklafor 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 boguschown-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):- Overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) — FUSE must
be mounted in the init ns via the setuid
fusermounthelper (libfuse does this). A raw/dev/fuseserver was tried and abandoned: its INIT handshake needspoll()on the non-blocking fd, and a malformed reply destabilised the kernel — fragile and inappropriate. libfuse is linked conditionally (pkg-configfuse/fuse3), matchingpack2theroot. - fuse2 low-level API (
fuse_mount/fuse_new/fuse_loop_mt, empty args) —fuse_mainadvertises splice/copy_file_range caps that make the kernel attemptcopy_file_rangeat copy-up → ENOSYS with no fallback. read_bufcallback (copy-up's splice read path).ioctlcallback — copy-up issuesFS_IOC_GETFLAGSon 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 comparingfops.
- Overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) — FUSE must
be mounted in the init ns via the setuid
cgroup_release_agent— two real bugs fixed (commit8c45b2b): it readgetuid()afterunshare(CLONE_NEWUSER)(→65534, souid_mapwrite was"0 65534 1"→ EPERM), and it omittedCLONE_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 (itsrelease_agentis 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. Thegetuid()-after-unsharebug 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. |
sudo_samedit |
CVE-2021-3156 | Ubuntu 18.04.2 + 20.04.0 | honest EXPLOIT_FAIL; Baron Samedit is heap/libc-version-specific, needs a per-target tuned PoC. Hardest. |
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-exploitationkernel 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_DROPverdict combo thatnft_verdict_init()fails to reject, fires the double-free, and runs themsg_msgcg-96 groom — all real. But its arb-write is "FALLBACK-DEPTH": the exactpipapo_elemlayout + value-pointer offset needed to redirect the write atmodprobe_pathis a documented TODO, so the write doesn't land → honestEXPLOIT_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 staticlibnftnl/libmnl. -
Scope of the remaining fix. Making
nf_tables --full-chainland root means either (a) completing the module's own per-kernelpipapo_elemarb-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 returningEXPLOIT_FAILby 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/<name>/release-<date>/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.shis the reference check.