Files
SKELETONKEY/docs/EXPLOITED.md
T
KaraZajac 82ba6e0d08
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
sudo_samedit: working CVE-2021-3156 (Baron Samedit) exploit — lands real root
The shipped module drove a structural sudoedit trigger with no heap-grooming
offsets and honestly reported EXPLOIT_FAIL. Ported blasty's proven technique:
the `sudoedit -s` set_cmnd() unescape overflow overwrites a glibc NSS
service_user, so the subsequent NSS lookup dlopen's an attacker-planted
libnss_X/'P0P_SH3LLZ_ .so.2' from CWD, whose constructor runs while sudo is
still root.

Implementation follows the corpus's runtime-compile pattern: build the NSS
payload on the target (unique -DSK_PROOF/-DSK_ROOTBASH), lay out the libnss_X/
dir, exec sudoedit with the crafted argv (AAAA..\ / \ / BBBB..\) and env
(null_stomp × "\\", "X/P0P_SH3LLZ_", padded LC_ALL) from that CWD, and verify
root by stat()'ing the root-owned artifacts — never self-report. Grooming
lengths are libc-family specific (Ubuntu 56/54/63/212, Debian 64/49/60/214);
a null_stomp_len ±8 sweep (blasty brute.sh's axis) is the fallback for drift.

Verified out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as uid 1000
(non-sudoer — the overflow precedes the sudoers/password check): `skeletonkey
--exploit sudo_samedit` -> EXPLOIT_OK + a -rwsr-xr-x root:root bash, primary
lengths landing first try. Gated on cc + sudoedit; cleanup() removes artifacts
and the scratch build dir. 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
2026-07-23 22:22:11 -04:00

184 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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 |
| `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/<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.sh` is the reference check.