Phase 7: nf_tables CVE-2024-1086 + active probe for dirty_pipe
dirty_pipe detect: active sentinel probe (Phase 1.5-ish improvement)
- New dirty_pipe_active_probe(): creates a /tmp probe file with known
sentinel bytes, fires the Dirty Pipe primitive against it, re-reads
via the page cache, returns true if the poisoning landed.
- detect() gated on ctx->active_probe: --scan does version-only check
(fast, no side effects); --scan --active fires the empirical probe
and overrides version inference with the empirical verdict. Catches
silent distro backports that don't bump uname() version.
- Three verdicts now distinguishable:
(a) version says patched, no active probe → 'patched (version-only)'
(b) version says vulnerable, --active fires + probe lands → CONFIRMED
(c) version says vulnerable, --active fires + probe blocked → 'likely
patched via distro backport'
- Probe is safe: only /tmp, no /etc/passwd.
nf_tables CVE-2024-1086 (detect-only, new module):
- Famous Notselwyn UAF in nft_verdict_init. Affects 5.14 ≤ K, fixed
mainline 6.8 with backports landing in 5.4.269 / 5.10.210 / 5.15.149
/ 6.1.74 / 6.6.13 / 6.7.2.
- detect() checks: kernel version range, AND unprivileged user_ns clone
availability (the exploit's reachability gate — kernel-vulnerable
but userns-locked-down hosts report PRECOND_FAIL, signalling that
the kernel still needs patching but unprivileged path is closed).
- Ships auditd + sigma detection rules: unshare(CLONE_NEWUSER) chained
with setresuid(0,0,0) on a previously-non-root process is the
exploit's canonical telltale.
- Full Notselwyn-style exploit (cross-cache UAF → arbitrary R/W → cred
overwrite or modprobe_path hijack) is the next commit.
9 modules total now. CVES.md and ROADMAP.md updated.
This commit is contained in:
@@ -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-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` | 🟢 | |
|
| 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 |
|
| (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-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-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-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. |
|
| 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
|
## Operations supported per module
|
||||||
|
|||||||
@@ -46,10 +46,15 @@ PK_DIR := modules/pwnkit_cve_2021_4034
|
|||||||
PK_SRCS := $(PK_DIR)/iamroot_modules.c
|
PK_SRCS := $(PK_DIR)/iamroot_modules.c
|
||||||
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
|
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-level dispatcher
|
||||||
TOP_OBJ := $(BUILD)/iamroot.o
|
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
|
.PHONY: all clean debug static help
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -147,7 +147,13 @@ Backfill of historical and recent LPEs as time allows:
|
|||||||
Falls back gracefully on hosts without cc.
|
Falls back gracefully on hosts without cc.
|
||||||
- [ ] **CVE-2022-2588** — net/sched route4 dead UAF
|
- [ ] **CVE-2022-2588** — net/sched route4 dead UAF
|
||||||
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
- [ ] **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)
|
- [ ] Fragnesia (if it lands as a CVE)
|
||||||
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
||||||
ships (responsible-disclosure-first)
|
ships (responsible-disclosure-first)
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ void iamroot_register_copy_fail_family(void);
|
|||||||
void iamroot_register_dirty_pipe(void);
|
void iamroot_register_dirty_pipe(void);
|
||||||
void iamroot_register_entrybleed(void);
|
void iamroot_register_entrybleed(void);
|
||||||
void iamroot_register_pwnkit(void);
|
void iamroot_register_pwnkit(void);
|
||||||
|
void iamroot_register_nf_tables(void);
|
||||||
|
|
||||||
#endif /* IAMROOT_REGISTRY_H */
|
#endif /* IAMROOT_REGISTRY_H */
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ int main(int argc, char **argv)
|
|||||||
iamroot_register_dirty_pipe();
|
iamroot_register_dirty_pipe();
|
||||||
iamroot_register_entrybleed();
|
iamroot_register_entrybleed();
|
||||||
iamroot_register_pwnkit();
|
iamroot_register_pwnkit();
|
||||||
|
iamroot_register_nf_tables();
|
||||||
|
|
||||||
enum mode mode = MODE_SCAN;
|
enum mode mode = MODE_SCAN;
|
||||||
struct iamroot_ctx ctx = {0};
|
struct iamroot_ctx ctx = {0};
|
||||||
|
|||||||
@@ -212,10 +212,48 @@ static const struct kernel_range dirty_pipe_range = {
|
|||||||
sizeof(dirty_pipe_patched_branches[0]),
|
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)
|
static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
|
||||||
|
|
||||||
struct kernel_version v;
|
struct kernel_version v;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!kernel_version_current(&v)) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
|
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;
|
return IAMROOT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&dirty_pipe_range, &v);
|
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v);
|
||||||
if (patched) {
|
|
||||||
|
/* 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) {
|
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;
|
return IAMROOT_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE\n"
|
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
||||||
" (caveat: distro may have backported below threshold —\n"
|
" Confirm empirically: re-run with --scan --active\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",
|
|
||||||
v.release);
|
v.release);
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return IAMROOT_VULNERABLE;
|
||||||
|
|||||||
@@ -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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sched.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
/* 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: "<name> <size> <use_count> <by> <state> <addr>" */
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user