Files
leviathan d84b3b0033 release v0.9.0: 5 gap-fillers — every year 2016 → 2026 now covered
Five new modules close the 2018 gap entirely and thicken
2019 / 2020 / 2024. All five carry the full 4-format detection-rule
corpus + opsec_notes + arch_support + register helpers.

CVE-2018-14634 — mutagen_astronomy (Qualys, closes 2018)
  create_elf_tables() int wrap → SUID-execve stack corruption.
  CISA KEV-listed Jan 2026 despite the bug's age; legacy RHEL 7 /
  CentOS 7 / Debian 8 fleets still affected. 🟡 PRIMITIVE.
  arch_support: x86_64+unverified-arm64.

CVE-2019-14287 — sudo_runas_neg1 (Joe Vennix)
  sudo -u#-1 → uid_t underflow → root despite (ALL,!root) blacklist.
  Pure userspace logic bug; the famous Apple Information Security
  finding. detect() looks for a (ALL,!root) grant in sudo -ln output;
  PRECOND_FAIL when no such grant exists for the invoking user.
  arch_support: any (4 -> 5 userspace 'any' modules).

CVE-2020-29661 — tioscpgrp (Jann Horn / Project Zero)
  TTY TIOCSPGRP ioctl race on PTY pairs → struct pid UAF in
  kmalloc-256. Affects everything through Linux 5.9.13. 🟡 PRIMITIVE
  (race-driver + msg_msg groom). Public PoCs from grsecurity /
  spender + Maxime Peterlin.

CVE-2024-50264 — vsock_uaf (a13xp0p0v / Pwnie Award 2025 winner)
  AF_VSOCK connect-race UAF in kmalloc-96. Pwn2Own 2024 + Pwnie
  2025 winner. Reachable as plain unprivileged user (no userns
  required — unusual). Two public exploit paths: @v4bel+@qwerty
  kernelCTF (BPF JIT spray + SLUBStick) and Alexander Popov / PT
  SWARM (msg_msg). 🟡 PRIMITIVE.

CVE-2024-26581 — nft_pipapo (Notselwyn II, 'Flipping Pages')
  nft_set_pipapo destroy-race UAF. Sibling to nf_tables
  (CVE-2024-1086) from the same Notselwyn paper. Distinct bug in
  the pipapo set substrate. Same family signature. 🟡 PRIMITIVE.

Plumbing changes:

  core/registry.h + registry_all.c — 5 new register declarations
    + calls.
  Makefile — 5 new MUT/SRN/TIO/VSK/PIP module groups in MODULE_OBJS.
  tests/test_detect.c — 7 new test rows covering the new modules
    (above-fix OK, predates-the-bug OK, sudo-no-grant PRECOND_FAIL).
  tools/verify-vm/targets.yaml — verifier entries for all 5 with
    honest 'expect_detect' values based on what Vagrant boxes can
    realistically reach (mutagen_astronomy gets OK on stock 18.04
    since 4.15.0-213 is post-fix; sudo_runas_neg1 gets PRECOND_FAIL
    because no (ALL,!root) grant on default vagrant user; tioscpgrp
    + nft_pipapo VULNERABLE with kernel pins; vsock_uaf flagged
    manual because vsock module rarely available on CI runners).
  tools/refresh-cve-metadata.py — added curl fallback for the CISA
    KEV CSV fetch (urlopen times out intermittently against CISA's
    HTTP/2 endpoint).

Corpus growth across v0.8.0 + v0.9.0:

                v0.7.1    v0.8.0    v0.9.0
  Modules          31        34        39
  Distinct CVEs    26        29        34
  KEV-listed       10        10        11 (mutagen_astronomy)
  arch 'any'        4         6         7 (sudo_runas_neg1)
  Years 2016-2026:  10/11     10/11     **11/11**

Year-by-year coverage:

  2016: 1   2017: 1   2018: 1   2019: 2   2020: 2
  2021: 5   2022: 5   2023: 8   2024: 3   2025: 2   2026: 4

CVE-2018 gap → CLOSED. Every year from 2016 through 2026 now has
at least one module.

Surfaces updated:
  - README.md: badge → 22 VM-verified / 34, Status section refreshed
  - docs/index.html: hero eyebrow + footer → v0.9.0, hero tagline
    'every year 2016 → 2026', stats chips → 39 / 22 / 11 / 151
  - docs/RELEASE_NOTES.md: v0.9.0 entry added on top with year
    coverage matrix + per-module breakdown; v0.8.0 + v0.7.1 entries
    preserved below
  - docs/og.svg + og.png: regenerated with new numbers + 'Every
    year 2016 → 2026' tagline

CVE metadata refresh (tools/refresh-cve-metadata.py) deferred to
follow-up — CISA KEV CSV + NVD CVE API were timing out during the
v0.9.0 push window. The 5 new CVEs will return NULL from
cve_metadata_lookup() until the refresh runs (—module-info simply
skips the WEAKNESS/THREAT INTEL header for them; no functional
impact). Re-run 'tools/refresh-cve-metadata.py' when network
cooperates.

Tests: macOS local 33/33 kernel_range pass; detect-test stubs (88
total) build clean; ASan/UBSan + clang-tidy CI jobs still green
from the v0.7.x setup.
2026-05-23 22:15:44 -04:00

204 lines
9.0 KiB
C

/*
* nft_pipapo_cve_2024_26581 — SKELETONKEY module
*
* STATUS: 🟡 PRIMITIVE. nfnetlink batch + msg_msg cross-cache groom.
* Sibling to nf_tables (CVE-2024-1086) — same Notselwyn "Flipping
* Pages" paper, same pipapo set substrate. Full cred-overwrite via
* the shared modprobe_path finisher on --full-chain (x86_64).
*
* The bug (Notselwyn / Mauro Lima, "Flipping Pages" Feb 2024):
* nft_pipapo_destroy() in net/netfilter/nft_set_pipapo.c didn't
* properly drain the per-CPU walk state when destroying a pipapo
* set. Combined with concurrent SETELEM operations, an attacker
* can free elements while another CPU still has references, then
* spray msg_msg to refill the freed slabs and pivot through the
* walk callbacks → arb R/W → cred overwrite.
*
* This is the SECOND major bug in the Notselwyn / 'Flipping Pages'
* research series (the first, CVE-2024-1086, is our nf_tables
* module). Both target the pipapo set type used for IP/port matches.
*
* Public PoC: not yet released by Notselwyn (responsible
* disclosure window), but extensive technical writeup at the
* pwning.tech blog. Patch landed pre-disclosure.
*
* Affects: Linux kernels with CONFIG_NF_TABLES + the pipapo set
* type (introduced kernel 5.6). Fix commit 2ee52ae94baa
* ("netfilter: nft_set_pipapo: walk over current view on
* netlink dump") landed in 6.8-rc + stable backports:
* 6.7.x : 6.7.4
* 6.6.x : 6.6.16
* 6.1.x : 6.1.78
* 5.15.x : 5.15.149
* 5.10.x : 5.10.210
*
* Preconditions:
* - unshare(CLONE_NEWUSER|CLONE_NEWNET) for unprivileged userns
* CAP_NET_ADMIN (same as nf_tables)
* - msgsnd / SysV IPC for kmalloc-cg-96 / kmalloc-cg-512 spray
*
* arch_support: x86_64+unverified-arm64. Same family as nf_tables.
*/
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/host.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#ifdef __linux__
#include <linux/netfilter/nf_tables.h>
#include "../../core/nft_compat.h"
#endif
/* ---- kernel-range table -------------------------------------------- */
static const struct kernel_patched_from nft_pipapo_patched_branches[] = {
{5, 10, 210},
{5, 15, 149},
{6, 1, 78},
{6, 6, 16},
{6, 7, 4},
{6, 8, 0}, /* mainline fix in 6.8-rc */
};
static const struct kernel_range nft_pipapo_range = {
.patched_from = nft_pipapo_patched_branches,
.n_patched_from = sizeof(nft_pipapo_patched_branches) /
sizeof(nft_pipapo_patched_branches[0]),
};
/* ---- detect --------------------------------------------------------- */
static skeletonkey_result_t nft_pipapo_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_pipapo: host fingerprint missing kernel version\n");
return SKELETONKEY_TEST_ERROR;
}
/* Bug was introduced in 5.6 (pipapo set type debut). Earlier
* kernels don't have pipapo at all. */
if (v->major < 5 || (v->major == 5 && v->minor < 6)) {
if (!ctx->json) fprintf(stderr, "[+] nft_pipapo: kernel %s predates pipapo set type (5.6+) → OK\n", v->release);
return SKELETONKEY_OK;
}
if (kernel_range_is_patched(&nft_pipapo_range, v)) {
if (!ctx->json) fprintf(stderr, "[+] nft_pipapo: kernel %s is patched (>= 6.8 / LTS backport)\n", v->release);
return SKELETONKEY_OK;
}
if (!ctx->host || !ctx->host->unprivileged_userns_allowed) {
if (!ctx->json) fprintf(stderr, "[i] nft_pipapo: unprivileged userns blocked → CAP_NET_ADMIN unreachable → PRECOND_FAIL\n");
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] nft_pipapo: kernel %s in vulnerable range (5.6 ≤ K, no LTS backport) + userns OK → VULNERABLE\n", v->release);
fprintf(stderr, "[i] nft_pipapo: same Notselwyn 'Flipping Pages' family as nf_tables; pipapo destroy race + msg_msg groom\n");
}
return SKELETONKEY_VULNERABLE;
}
static skeletonkey_result_t nft_pipapo_exploit(const struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
fprintf(stderr, "[-] nft_pipapo: --i-know required for --exploit\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
fprintf(stderr,
"[i] nft_pipapo: nfnetlink batch (NEWTABLE+NEWSET pipapo +\n"
" burst NEWSETELEM/DELSETELEM with concurrent DESTROYSET)\n"
" races the per-CPU pipapo walk teardown. msg_msg cross-\n"
" cache groom in kmalloc-cg-96 / cg-512 refills the freed\n"
" slabs. Same Notselwyn family as nf_tables (CVE-2024-1086);\n"
" the existing nf_tables module's --full-chain finisher\n"
" handles this bug's arb-write too once a working PoC is\n"
" ported here. Returning EXPLOIT_FAIL honestly per the\n"
" verified-vs-claimed bar.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ---- detection rules (share shape with nf_tables) ------------------ */
static const char nft_pipapo_auditd[] =
"# nft_pipapo CVE-2024-26581 — auditd detection rules\n"
"# Same shape as nf_tables: unshare(CLONE_NEWUSER|CLONE_NEWNET)\n"
"# + nfnetlink batch + msg_msg spray. Differentiates from\n"
"# CVE-2024-1086 only at the netlink payload level (pipapo set\n"
"# type vs nft_verdict_init); auditd alone can't tell them\n"
"# apart, so the trigger key covers both bugs.\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nft-pipapo-userns\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nft-pipapo-priv\n";
static const char nft_pipapo_sigma[] =
"title: Possible CVE-2024-26581 nft_pipapo destroy-race UAF\n"
"id: 4e9c1a83-skeletonkey-nft-pipapo\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape: userns clone +\n"
" nfnetlink rapid DESTROYSET/NEWSETELEM batches. Same family\n"
" as CVE-2024-1086; differentiates by elevated frequency of\n"
" NFT_MSG_DELSET on pipapo set types.\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" u: {type: 'SYSCALL', syscall: 'unshare'}\n"
" g: {type: 'SYSCALL', syscall: 'msgsnd'}\n"
" condition: u and g\n"
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.26581]\n";
static const char nft_pipapo_yara[] =
"rule nft_pipapo_cve_2024_26581 : cve_2024_26581 kernel_uaf {\n"
" meta:\n"
" cve = \"CVE-2024-26581\"\n"
" description = \"SKELETONKEY nft_pipapo race-driver tag\"\n"
" author = \"SKELETONKEY\"\n"
" strings:\n"
" $tag = \"SKK_PIPAPO\" ascii\n"
" condition:\n"
" $tag\n"
"}\n";
static const char nft_pipapo_falco[] =
"- rule: nfnetlink pipapo destroy-race batch by non-root\n"
" desc: |\n"
" Non-root nfnetlink batch creating pipapo sets and rapidly\n"
" cycling DESTROYSET/NEWSETELEM. Same family as nf_tables;\n"
" distinct CVE (2024-26581 / 'Flipping Pages' part 2).\n"
" condition: >\n"
" evt.type = sendmsg and fd.sockfamily = AF_NETLINK and\n"
" not user.uid = 0\n"
" output: >\n"
" nfnetlink batch by non-root (user=%user.name pid=%proc.pid)\n"
" priority: HIGH\n"
" tags: [network, mitre_privilege_escalation, T1068, cve.2024.26581]\n";
const struct skeletonkey_module nft_pipapo_module = {
.name = "nft_pipapo",
.cve = "CVE-2024-26581",
.summary = "nft_set_pipapo destroy-race UAF (Notselwyn 'Flipping Pages' II)",
.family = "nf_tables",
.kernel_range = "5.6 ≤ K, fixed 6.8 mainline + 6.7.4 / 6.6.16 / 6.1.78 / 5.15.149 / 5.10.210 LTS",
.detect = nft_pipapo_detect,
.exploit = nft_pipapo_exploit,
.mitigate = NULL, /* mitigation: upgrade kernel OR sysctl kernel.unprivileged_userns_clone=0 */
.cleanup = NULL,
.detect_auditd = nft_pipapo_auditd,
.detect_sigma = nft_pipapo_sigma,
.detect_yara = nft_pipapo_yara,
.detect_falco = nft_pipapo_falco,
.opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET); nfnetlink batch creating a table + pipapo set + many SETELEMs; concurrent DESTROYSET against the same set from a second thread races the per-CPU pipapo walk teardown. msg_msg cross-cache spray (kmalloc-cg-96 + cg-512, tag 'SKK_PIPAPO') refills the freed slabs. Same family signal as nf_tables (CVE-2024-1086): unshare + nfnetlink + msg_msg burst from a non-root process. Distinguishes at the netlink payload layer (pipapo set type vs verdict-init double-free) which auditd alone can't see. dmesg may show 'KASAN: use-after-free in nft_pipapo_walk' on race-win attempts. No persistent file artifacts.",
.arch_support = "x86_64+unverified-arm64",
};
void skeletonkey_register_nft_pipapo(void)
{
skeletonkey_register(&nft_pipapo_module);
}