diff --git a/CVES.md b/CVES.md index f52c69f..0a43aab 100644 --- a/CVES.md +++ b/CVES.md @@ -23,10 +23,11 @@ Status legend: | CVE-2026-43284 (v6) | Dirty Frag — IPv6 xfrm-ESP (`esp6`) | LPE | mainline 2026-05-XX | `dirty_frag_esp6` | 🟢 | V6 STORE shift auto-calibrated per kernel build | | CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | | | (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix | -| 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-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 + **active sentinel probe** (`--active` fires the primitive against a /tmp probe file and verifies the page cache poisoning lands — catches silent distro backports the version check misses). 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. | | 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` | 🟢 | 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-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🔵 | Detect-only. Branch-backport ranges checked (6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269). Also checks unprivileged user_ns clone availability (the exploit's trigger gate) — reports PRECOND_FAIL if userns is locked down even when the kernel is vulnerable. Full Notselwyn-style exploit is the next nf_tables commit. | | 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 bfc3dbd..94491a3 100644 --- a/Makefile +++ b/Makefile @@ -46,10 +46,15 @@ PK_DIR := modules/pwnkit_cve_2021_4034 PK_SRCS := $(PK_DIR)/iamroot_modules.c PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS)) +# Family: nf_tables (CVE-2024-1086) +NFT_DIR := modules/nf_tables_cve_2024_1086 +NFT_SRCS := $(NFT_DIR)/iamroot_modules.c +NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/iamroot.o -ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) +ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) .PHONY: all clean debug static help diff --git a/ROADMAP.md b/ROADMAP.md index 9a6bdb1..6b80017 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -147,7 +147,13 @@ Backfill of historical and recent LPEs as time allows: 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 +- [x] **CVE-2024-1086** — nf_tables UAF: 🔵 detect-only landed + (2026-05-16). Branch-backport thresholds for 5.4 / 5.10 / 5.15 / + 6.1 / 6.6 / 6.7 plus mainline 6.8. Detect also probes + unprivileged user_ns clone availability — kernel-vulnerable hosts + with userns locked down get IAMROOT_PRECOND_FAIL (kernel still + needs patching but unprivileged-exploit path is closed). Full + Notselwyn-style exploit follows. - [ ] Fragnesia (if it lands as a CVE) - [ ] Anything we ourselves disclose — bundled AFTER upstream patch ships (responsible-disclosure-first) diff --git a/core/registry.h b/core/registry.h index 36f3c2e..2104797 100644 --- a/core/registry.h +++ b/core/registry.h @@ -24,5 +24,6 @@ void iamroot_register_copy_fail_family(void); void iamroot_register_dirty_pipe(void); void iamroot_register_entrybleed(void); void iamroot_register_pwnkit(void); +void iamroot_register_nf_tables(void); #endif /* IAMROOT_REGISTRY_H */ diff --git a/iamroot.c b/iamroot.c index cefafdd..179c873 100644 --- a/iamroot.c +++ b/iamroot.c @@ -221,6 +221,7 @@ int main(int argc, char **argv) iamroot_register_dirty_pipe(); iamroot_register_entrybleed(); iamroot_register_pwnkit(); + iamroot_register_nf_tables(); enum mode mode = MODE_SCAN; struct iamroot_ctx ctx = {0}; diff --git a/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c index 08dd956..292847b 100644 --- a/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c +++ b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c @@ -212,10 +212,48 @@ static const struct kernel_range dirty_pipe_range = { sizeof(dirty_pipe_patched_branches[0]), }; +/* Active sentinel probe: write a known byte into a /tmp probe file + * via the Dirty Pipe primitive, then re-read to verify the page cache + * was actually poisoned. This catches the case where /proc/version + * looks vulnerable (e.g. Debian 5.10.0-30 — apparent version 5.10.30) + * but the distro silently backported the fix without bumping the + * upstream version number visible to uname(). + * + * Side effects: creates and removes a single file under /tmp. No + * /etc/passwd writes; safe to run from --scan --active. */ +static int dirty_pipe_active_probe(void) +{ + char probe_path[] = "/tmp/iamroot-dirty-pipe-probe-XXXXXX"; + int fd = mkstemp(probe_path); + if (fd < 0) return -1; + const char seed[16] = "ABCDABCDABCDABCD"; + if (write(fd, seed, sizeof seed) != sizeof seed) { close(fd); unlink(probe_path); return -1; } + fsync(fd); + close(fd); + + /* Try writing 'X' at offset 4 — well inside the first page, not + * page-aligned (offset 4 → page-relative offset 4, not 0). */ + int rc = dirty_pipe_write(probe_path, 4, "X", 1); + if (rc < 0) { + unlink(probe_path); + return 0; /* primitive could not even fire — patched or blocked */ + } + + /* Re-open and read; if the primitive works, byte 4 reads as 'X'. + * Use O_RDONLY + read, which goes through the page cache (which + * we just poisoned if the bug is live). */ + fd = open(probe_path, O_RDONLY); + if (fd < 0) { unlink(probe_path); return -1; } + char readback[16] = {0}; + ssize_t got = read(fd, readback, sizeof readback); + close(fd); + unlink(probe_path); + if (got < 5) return -1; + return readback[4] == 'X' ? 1 : 0; +} + static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx) { - (void)ctx; - struct kernel_version v; if (!kernel_version_current(&v)) { fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n"); @@ -231,19 +269,51 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx) return IAMROOT_OK; } - bool patched = kernel_range_is_patched(&dirty_pipe_range, &v); - if (patched) { + bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v); + + /* Active probe overrides version-only verdict when requested. + * The version check is necessary-but-not-sufficient: distros + * silently backport fixes without bumping the upstream version + * visible to uname(). The active probe fires the actual primitive + * and confirms whether it lands. */ + if (ctx->active_probe) { if (!ctx->json) { - fprintf(stderr, "[+] dirty_pipe: kernel %s is patched\n", v.release); + fprintf(stderr, "[*] dirty_pipe: running active sentinel probe (safe; /tmp only)\n"); + } + int probe = dirty_pipe_active_probe(); + if (probe == 1) { + if (!ctx->json) { + fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands " + "(version %s)\n", v.release); + } + return IAMROOT_VULNERABLE; + } + if (probe == 0) { + if (!ctx->json) { + fprintf(stderr, "[+] dirty_pipe: active probe sentinel did NOT land — " + "primitive blocked (likely patched%s)\n", + patched_by_version ? "" : ", or distro silently backported"); + } + return IAMROOT_OK; + } + /* probe < 0: probe machinery failed (mkstemp/open/read) — fall + * back to version-only verdict and report TEST_ERROR caveat */ + if (!ctx->json) { + fprintf(stderr, "[?] dirty_pipe: active probe machinery failed; " + "falling back to version check\n"); + } + } + + if (patched_by_version) { + if (!ctx->json) { + fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; " + "use --active to confirm empirically)\n", v.release); } return IAMROOT_OK; } if (!ctx->json) { - fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE\n" - " (caveat: distro may have backported below threshold —\n" - " confirm by checking /proc/version for fix references or\n" - " by running the active exploit primitive once the Phase 1.5\n" - " helpers land in core/)\n", + fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n" + " Confirm empirically: re-run with --scan --active\n", v.release); } return IAMROOT_VULNERABLE; diff --git a/modules/nf_tables_cve_2024_1086/iamroot_modules.c b/modules/nf_tables_cve_2024_1086/iamroot_modules.c new file mode 100644 index 0000000..d1f4ca9 --- /dev/null +++ b/modules/nf_tables_cve_2024_1086/iamroot_modules.c @@ -0,0 +1,241 @@ +/* + * nf_tables_cve_2024_1086 — IAMROOT module + * + * Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist + * with NFT_DROP/NFT_QUEUE. Triggers a double-free → cross-cache UAF + * exploitable to arbitrary kernel R/W. Discovered and exploited in + * January 2024; widely known as "Pumpkin's pipapo UAF" or just + * "CVE-2024-1086". + * + * STATUS: 🔵 DETECT-ONLY (2026-05-16). Full exploit is a public PoC + * by Notselwyn — porting it into the iamroot_module form is a + * follow-up commit. + * + * Affected kernel ranges: + * Bug introduced in commit f1a2e44 (5.14) "netfilter: nf_tables: + * introduce nf_chain..." + * Fixed mainline 6.8-rc1 in commit f342de4 ("netfilter: nf_tables: + * reject QUEUE/DROP verdict parameters") + * Stable backports landed in 6.7.2, 6.6.13, 6.1.74, 5.15.149, + * 5.10.210, 5.4.269 + * So vulnerable if: + * - 5.14 <= K < 5.15 (no backport) — vulnerable + * - 5.15.x: K <= 5.15.148 — vulnerable + * - 5.10.x: K <= 5.10.209 — vulnerable + * - 5.4.x: K <= 5.4.268 — vulnerable + * - 6.0/6.1.x: K <= 6.1.73 — vulnerable + * - 6.2-6.5: no backport tags — assume vulnerable + * - 6.6.x: K <= 6.6.12 — vulnerable + * - 6.7.x: K <= 6.7.1 — vulnerable + * - 6.8+: patched + * + * Exploitation preconditions (which detect should also check): + * - CONFIG_USER_NS=y AND sysctl unprivileged_userns_clone=1 (or + * kernel.unprivileged_userns_clone default=1) so an unprivileged + * user can create a userns and become CAP_NET_ADMIN inside it + * - nf_tables module loaded or autoload-able (CONFIG_NF_TABLES=y/m) + * + * If user_ns is locked down (modern Ubuntu's + * apparmor_restrict_unprivileged_userns), the trigger is unreachable + * for unprivileged users even on a kernel-vulnerable host. + */ + +#include "iamroot_modules.h" +#include "../../core/registry.h" +#include "../../core/kernel_range.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Stable-branch backport thresholds — host is patched if on these + * branches at or above the threshold patch, or on mainline >= 6.8. */ +static const struct kernel_patched_from nf_tables_patched_branches[] = { + {5, 4, 269}, /* 5.4.x */ + {5, 10, 210}, /* 5.10.x */ + {5, 15, 149}, /* 5.15.x */ + {6, 1, 74}, /* 6.1.x */ + {6, 6, 13}, /* 6.6.x */ + {6, 7, 2}, /* 6.7.x */ + {6, 8, 0}, /* mainline fix */ +}; + +static const struct kernel_range nf_tables_range = { + .patched_from = nf_tables_patched_branches, + .n_patched_from = sizeof(nf_tables_patched_branches) / + sizeof(nf_tables_patched_branches[0]), +}; + +/* Best-effort check: can an unprivileged process clone a user + * namespace? This is the gating capability for the exploit's + * CAP_NET_ADMIN-in-userns trigger. Fork+unshare+exit to avoid + * polluting our own namespace state. */ +static int can_unshare_userns(void) +{ + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + /* try */ + if (unshare(CLONE_NEWUSER) == 0) _exit(0); + _exit(1); + } + int status; + waitpid(pid, &status, 0); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +/* Check whether the nf_tables module is loaded OR can be auto-loaded. + * /proc/modules tells us about loaded modules. For modules that aren't + * loaded but are buildable, we rely on the kernel autoload via + * setsockopt(SOL_NETLINK, NETLINK_NF_TABLES). Conservative: if not + * loaded, assume autoload-able and report no info. */ +static bool nf_tables_loaded(void) +{ + FILE *f = fopen("/proc/modules", "r"); + if (!f) return false; + char line[512]; + bool found = false; + while (fgets(line, sizeof line, f)) { + /* /proc/modules format: " " */ + if (strncmp(line, "nf_tables ", 10) == 0) { found = true; break; } + } + fclose(f); + return found; +} + +static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx) +{ + struct kernel_version v; + if (!kernel_version_current(&v)) { + fprintf(stderr, "[!] nf_tables: could not parse kernel version\n"); + return IAMROOT_TEST_ERROR; + } + + /* Bug introduced in 5.14. Anything below predates it. */ + if (v.major < 5 || (v.major == 5 && v.minor < 14)) { + if (!ctx->json) { + fprintf(stderr, "[i] nf_tables: kernel %s predates the bug " + "(introduced in 5.14)\n", v.release); + } + return IAMROOT_OK; + } + + bool patched = kernel_range_is_patched(&nf_tables_range, &v); + if (patched) { + if (!ctx->json) { + fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release); + } + return IAMROOT_OK; + } + + /* Vulnerable by version. Now check preconditions that affect + * unprivileged reachability. */ + int userns_ok = can_unshare_userns(); + bool nft_loaded = nf_tables_loaded(); + + if (!ctx->json) { + fprintf(stderr, "[i] nf_tables: kernel %s is in the vulnerable range\n", + v.release); + fprintf(stderr, "[i] nf_tables: unprivileged user_ns clone: %s\n", + userns_ok == 1 ? "ALLOWED" : + userns_ok == 0 ? "DENIED" : + "could not test"); + fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n", + nft_loaded ? "yes" : "no (will autoload on first nft use)"); + } + + /* If user_ns is denied, the unprivileged-exploit path is closed. + * (A root attacker would still trigger the bug, but root LPE-of-root + * is not interesting.) */ + if (userns_ok == 0) { + if (!ctx->json) { + fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone " + "denied → unprivileged exploit unreachable\n"); + fprintf(stderr, "[i] nf_tables: still patch the kernel — a root " + "attacker can still trigger the bug\n"); + } + return IAMROOT_PRECOND_FAIL; + } + + if (!ctx->json) { + fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns " + "clone allowed\n"); + } + return IAMROOT_VULNERABLE; +} + +static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx) +{ + (void)ctx; + fprintf(stderr, + "[-] nf_tables: exploit not yet implemented in IAMROOT.\n" + " Status: 🔵 DETECT-ONLY (see CVES.md).\n" + " Reference: Notselwyn's CVE-2024-1086 public PoC. The exploit\n" + " uses double-free → cross-cache UAF → arbitrary kernel R/W →\n" + " overwrite modprobe_path or current task's cred. Porting that\n" + " into iamroot_module form (with the userns + nft_set + nft_pipapo\n" + " setup boilerplate) is the next nf_tables commit.\n"); + return IAMROOT_PRECOND_FAIL; +} + +/* ----- Embedded detection rules ----- */ + +static const char nf_tables_auditd[] = + "# nf_tables UAF (CVE-2024-1086) — auditd detection rules\n" + "# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n" + "# This is the canonical exploit shape; legitimate userns + nft use\n" + "# (e.g. firewalld, docker rootless) will also trip — tune per env.\n" + "-a always,exit -F arch=b64 -S unshare -k iamroot-nf-tables-userns\n" + "-a always,exit -F arch=b32 -S unshare -k iamroot-nf-tables-userns\n" + "# Also watch for the canonical post-exploit primitives: modprobe_path\n" + "# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n" + "-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nf-tables-priv\n"; + +static const char nf_tables_sigma[] = + "title: Possible CVE-2024-1086 nf_tables UAF exploitation\n" + "id: a72b5e91-iamroot-nf-tables\n" + "status: experimental\n" + "description: |\n" + " Detects the canonical exploit shape: unprivileged user creating a\n" + " user namespace, then issuing nft commands within it. False positives:\n" + " legitimate use of nft inside containers, podman/docker rootless,\n" + " firewalld. Combine with process-tree analysis: a previously-unpriv\n" + " process that suddenly has effective uid 0 is the smoking gun.\n" + "logsource: {product: linux, service: auditd}\n" + "detection:\n" + " userns_clone:\n" + " type: 'SYSCALL'\n" + " syscall: 'unshare'\n" + " a0: 0x10000000\n" + " uid_change:\n" + " type: 'SYSCALL'\n" + " syscall: 'setresuid'\n" + " auid|expression: '!= 0'\n" + " condition: userns_clone and uid_change\n" + "level: high\n" + "tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n"; + +const struct iamroot_module nf_tables_module = { + .name = "nf_tables", + .cve = "CVE-2024-1086", + .summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W", + .family = "nf_tables", + .kernel_range = "5.14 ≤ K, fixed mainline 6.8; backports: 6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269", + .detect = nf_tables_detect, + .exploit = nf_tables_exploit, + .mitigate = NULL, /* mitigation: upgrade kernel; OR set unprivileged_userns_clone=0 */ + .cleanup = NULL, + .detect_auditd = nf_tables_auditd, + .detect_sigma = nf_tables_sigma, + .detect_yara = NULL, + .detect_falco = NULL, +}; + +void iamroot_register_nf_tables(void) +{ + iamroot_register(&nf_tables_module); +} diff --git a/modules/nf_tables_cve_2024_1086/iamroot_modules.h b/modules/nf_tables_cve_2024_1086/iamroot_modules.h new file mode 100644 index 0000000..0238bf1 --- /dev/null +++ b/modules/nf_tables_cve_2024_1086/iamroot_modules.h @@ -0,0 +1,12 @@ +/* + * nf_tables_cve_2024_1086 — IAMROOT module registry hook + */ + +#ifndef NF_TABLES_IAMROOT_MODULES_H +#define NF_TABLES_IAMROOT_MODULES_H + +#include "../../core/module.h" + +extern const struct iamroot_module nf_tables_module; + +#endif