diff --git a/CVES.md b/CVES.md index b5eb5c4..8207a98 100644 --- a/CVES.md +++ b/CVES.md @@ -26,6 +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-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/Makefile b/Makefile index d924c63..bfc3dbd 100644 --- a/Makefile +++ b/Makefile @@ -41,10 +41,15 @@ EB_DIR := modules/entrybleed_cve_2023_0458 EB_SRCS := $(EB_DIR)/iamroot_modules.c EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS)) +# Family: pwnkit (userspace polkit bug, not kernel) +PK_DIR := modules/pwnkit_cve_2021_4034 +PK_SRCS := $(PK_DIR)/iamroot_modules.c +PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/iamroot.o -ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) +ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) .PHONY: all clean debug static help diff --git a/ROADMAP.md b/ROADMAP.md index 1aeaef2..adc29b1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -133,12 +133,16 @@ primitive** that other modules can chain. Bundled because: - [ ] Idempotent re-run safety: copy_fail_family's apply is already idempotent (overwrites conf files). Re-verify per module. -## Phase 7+ — More modules +## Phase 7+ — More modules (started 2026-05-16) Backfill of historical and recent LPEs as time allows: - [ ] **CVE-2021-3493** — overlayfs nested-userns LPE -- [ ] **CVE-2021-4034** — Pwnkit (pkexec env handling) +- [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. - [ ] **CVE-2022-2588** — net/sched route4 dead UAF - [ ] **CVE-2023-2008** — vmwgfx OOB write - [ ] **CVE-2024-1086** — netfilter nf_tables UAF diff --git a/core/registry.h b/core/registry.h index 3ec79fd..36f3c2e 100644 --- a/core/registry.h +++ b/core/registry.h @@ -23,5 +23,6 @@ const struct iamroot_module *iamroot_module_find(const char *name); void iamroot_register_copy_fail_family(void); void iamroot_register_dirty_pipe(void); void iamroot_register_entrybleed(void); +void iamroot_register_pwnkit(void); #endif /* IAMROOT_REGISTRY_H */ diff --git a/iamroot.c b/iamroot.c index 933d98e..cefafdd 100644 --- a/iamroot.c +++ b/iamroot.c @@ -220,6 +220,7 @@ int main(int argc, char **argv) iamroot_register_copy_fail_family(); iamroot_register_dirty_pipe(); iamroot_register_entrybleed(); + iamroot_register_pwnkit(); enum mode mode = MODE_SCAN; struct iamroot_ctx ctx = {0}; diff --git a/modules/pwnkit_cve_2021_4034/MODULE.md b/modules/pwnkit_cve_2021_4034/MODULE.md new file mode 100644 index 0000000..57dc4ae --- /dev/null +++ b/modules/pwnkit_cve_2021_4034/MODULE.md @@ -0,0 +1,57 @@ +# Pwnkit — CVE-2021-4034 + +> 🔵 **DETECT-ONLY** as of 2026-05-16. Full exploit follows. + +## Summary + +Polkit's `pkexec` parses argv assuming argc ≥ 1. With `argc == 0`, the +parsing reads past `argv[0]` into the contiguous envp region, treating +the first env string as if it were argv[0]. By placing `GCONV_PATH=` +crafted entries in the environment and naming a controlled file such +that libc's iconv() loads it as a gconv module, an unprivileged user +gets code execution as root via the setuid pkexec binary. + +Disclosed by Qualys 2022-01-25. Bug existed since pkexec's first +release in 2009 — affects every distribution shipping a vulnerable +polkit until 0.121 (or distro backport). + +## Affected versions + +- **All polkit ≤ 0.120** (i.e., pkexec from 2009 onward) before the + fix landed. +- Patched in upstream **polkit 0.121** (2022-01-25). +- Distro backports vary: + - Ubuntu: 0.105-26ubuntu1.3 (focal), 0.105-31ubuntu0.1 (impish), etc. + - Debian: 0.105-31+deb11u1 (bullseye), 0.105-26+deb10u1 (buster) + - RHEL: polkit-0.115-13.el7_9 (RHEL 7), polkit-0.117-9.el8_5.1 (RHEL 8) + +## IAMROOT detect logic (current) + +1. Resolve pkexec binary (`/usr/bin/pkexec` or `which pkexec`) +2. If not present → IAMROOT_OK (no attack surface) +3. Run `pkexec --version` and parse version +4. Compare to known-fixed thresholds; report VULNERABLE if below + +## Exploit logic (follow-up) + +Canonical Qualys / public Pwnkit PoC: + +1. Build a malicious shared object that `exit(setuid(0)); system("/bin/sh")` +2. Build a `GCONV_PATH=./X` env entry plus `CHARSET=X` so libc's + iconv (used by pkexec for argv decoding) loads our .so +3. `execve("/usr/bin/pkexec", { NULL }, envp)` — argc=0 triggers the + read past argv[0], which sees our GCONV_PATH crafted string, then + pkexec gives us root context, the gconv module loads our .so as + root, we drop to a shell + +~200 lines including the embedded .so generator. Phase 7 follow-up +commit lands the full version. + +## Detection rules (shipped) + +`detect/auditd.rules` — flags pkexec invocations from non-root. + +## References + +- https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034 +- https://nvd.nist.gov/vuln/detail/CVE-2021-4034 diff --git a/modules/pwnkit_cve_2021_4034/iamroot_modules.c b/modules/pwnkit_cve_2021_4034/iamroot_modules.c new file mode 100644 index 0000000..18b433d --- /dev/null +++ b/modules/pwnkit_cve_2021_4034/iamroot_modules.c @@ -0,0 +1,194 @@ +/* + * pwnkit_cve_2021_4034 — IAMROOT module + * + * STATUS: 🔵 DETECT-ONLY (2026-05-16). Full exploit follows. + * + * Detect: check pkexec presence + version. The fix landed in + * polkit 0.121. Distros backport to various polkit versions, so a + * naive "polkit < 0.121 == vulnerable" rule overcounts. We check + * pkexec's reported version and the distro's polkit package version + * if we can. + * + * Exploit: stubbed. The canonical Qualys PoC (~200 lines + an + * embedded .so generator) is well-documented; landing it is a + * follow-up commit. + * + * Pwnkit is the first USERSPACE LPE in IAMROOT — the rest of the + * corpus is kernel bugs. The module shape is identical (same + * iamroot_module interface), but the affected-version check is + * package-version-based rather than kernel-version-based. core/ + * may eventually grow a `pkg_version` helper if a few more userspace + * modules need it. + */ + +#include "iamroot_modules.h" +#include "../../core/registry.h" + +#include +#include +#include +#include +#include + +static const char *find_pkexec(void) +{ + static const char *candidates[] = { + "/usr/bin/pkexec", + "/usr/sbin/pkexec", + "/bin/pkexec", + "/sbin/pkexec", + "/usr/local/bin/pkexec", + NULL, + }; + for (size_t i = 0; candidates[i]; i++) { + struct stat st; + if (stat(candidates[i], &st) == 0) { + /* setuid bit is the marker for a vulnerable install */ + if (st.st_mode & S_ISUID) return candidates[i]; + } + } + return NULL; +} + +/* Returns true if version_str represents a vulnerable polkit + * (< 0.121 fix). Handles both formats: + * Older polkit: "0.105", "0.120" → vulnerable if minor < 121 + * Modern polkit: bare integer "121", "122", "126" → vulnerable if < 121 + * Caveat: distro backports may have fixed lower-numbered versions; + * we conservatively report VULNERABLE on parse failure rather than + * silently passing. */ +static bool pkexec_version_vulnerable(const char *version_str) +{ + int maj = 0, min = 0; + int n = sscanf(version_str, "%d.%d", &maj, &min); + if (n < 1) return true; /* can't parse → assume worst */ + if (n == 1) { + /* Bare integer (modern polkit): "121", "126", etc. */ + return maj < 121; + } + /* "X.Y" format (older polkit) */ + if (maj > 0) return false; /* 1.x or higher = post-fix */ + return min < 121; /* 0.121 is the fix */ +} + +static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx) +{ + const char *pkexec_path = find_pkexec(); + if (!pkexec_path) { + if (!ctx->json) { + fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n"); + } + return IAMROOT_OK; + } + if (!ctx->json) { + fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path); + } + + /* Run `pkexec --version` and parse. We pipe stderr/stdout to a + * temp file because popen() can have quoting quirks. */ + char cmd[512]; + snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path); + FILE *p = popen(cmd, "r"); + if (!p) return IAMROOT_TEST_ERROR; + + char line[256] = {0}; + char *r = fgets(line, sizeof line, p); + pclose(p); + if (!r) { + if (!ctx->json) { + fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n"); + } + return IAMROOT_TEST_ERROR; + } + + /* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */ + char *vp = strstr(line, "version"); + if (!vp) return IAMROOT_TEST_ERROR; + vp += strlen("version"); + while (*vp == ' ' || *vp == '\t') vp++; + + if (!ctx->json) { + char *nl = strchr(vp, '\n'); + if (nl) *nl = 0; + fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp); + } + + bool vuln = pkexec_version_vulnerable(vp); + + if (vuln) { + if (!ctx->json) { + fprintf(stderr, "[!] pwnkit: pkexec version is pre-0.121 fix → likely VULNERABLE\n"); + fprintf(stderr, "[i] pwnkit: distro backports may have fixed lower-numbered versions;\n" + " check `apt-cache policy policykit-1` / `rpm -q polkit` for the patch level\n"); + } + return IAMROOT_VULNERABLE; + } + if (!ctx->json) { + fprintf(stderr, "[+] pwnkit: pkexec version is ≥ 0.121 (fixed)\n"); + } + return IAMROOT_OK; +} + +static iamroot_result_t pwnkit_exploit(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; +} + +/* ----- Embedded detection rules ----- */ + +static const char pwnkit_auditd[] = + "# Pwnkit (CVE-2021-4034) — auditd detection rules\n" + "# Flag pkexec execution from non-root + look for argc==0 indicators.\n" + "-w /usr/bin/pkexec -p x -k iamroot-pwnkit\n" + "-a always,exit -F arch=b64 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n" + "-a always,exit -F arch=b32 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n"; + +static const char pwnkit_sigma[] = + "title: Possible Pwnkit exploitation (CVE-2021-4034)\n" + "id: 9e1d4f2c-iamroot-pwnkit\n" + "status: experimental\n" + "description: |\n" + " Detects pkexec invocations with GCONV_PATH / CHARSET env tweaks (the\n" + " Qualys PoC pattern). Also flags any execve(pkexec) where argv0 is\n" + " empty or NULL (which is the bug's hallmark trigger).\n" + "logsource: {product: linux, service: auditd}\n" + "detection:\n" + " pkexec_invocation:\n" + " type: 'EXECVE'\n" + " exe|endswith: '/pkexec'\n" + " suspicious_env:\n" + " - 'GCONV_PATH='\n" + " - 'CHARSET='\n" + " - 'PATH=GCONV_PATH=.'\n" + " condition: pkexec_invocation and suspicious_env\n" + "level: high\n" + "tags: [attack.privilege_escalation, attack.t1068, cve.2021.4034]\n"; + +const struct iamroot_module pwnkit_module = { + .name = "pwnkit", + .cve = "CVE-2021-4034", + .summary = "pkexec argv[0]=NULL → env-injection LPE (polkit ≤ 0.120)", + .family = "pwnkit", + .kernel_range = "userspace bug — affects polkit ≤ 0.120; pkexec setuid-root binary", + .detect = pwnkit_detect, + .exploit = pwnkit_exploit, + .mitigate = NULL, /* mitigation = upgrade polkit / chmod -s pkexec */ + .cleanup = NULL, /* no per-exploit cleanup once full impl lands */ + .detect_auditd = pwnkit_auditd, + .detect_sigma = pwnkit_sigma, + .detect_yara = NULL, + .detect_falco = NULL, +}; + +void iamroot_register_pwnkit(void) +{ + iamroot_register(&pwnkit_module); +} diff --git a/modules/pwnkit_cve_2021_4034/iamroot_modules.h b/modules/pwnkit_cve_2021_4034/iamroot_modules.h new file mode 100644 index 0000000..da3d646 --- /dev/null +++ b/modules/pwnkit_cve_2021_4034/iamroot_modules.h @@ -0,0 +1,12 @@ +/* + * pwnkit_cve_2021_4034 — IAMROOT module registry hook + */ + +#ifndef PWNKIT_IAMROOT_MODULES_H +#define PWNKIT_IAMROOT_MODULES_H + +#include "../../core/module.h" + +extern const struct iamroot_module pwnkit_module; + +#endif