Compare commits

..

10 Commits

Author SHA1 Message Date
KaraZajac 25c2afc3e9 release v0.9.6: --auto no longer prompts for sudo password
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
sudo_runas_neg1 and sudoedit_editor's detect() bodies invoked
'sudo -ln' (intending list + non-interactive). Some sudoers / PAM
configurations have been observed prompting for a password anyway
when the flags are bundled. That meant skeletonkey --auto --i-know
could hang on a sudo password prompt during the corpus scan — bad
ergonomics for an LPE tool whose whole point is to get root without
already having it.

Fix: write '-n -l' as separate flags, redirect stdin from /dev/null
so sudo cannot fall back to reading the tty even if PAM tries to
coerce one. Belt-and-suspenders against any tty prompt during --auto.
2026-05-28 21:50:26 -04:00
KaraZajac 13fbbce618 release v0.9.5: kernel_range drift cleanup (12 modules)
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
v0.9.4 fixed cve_metadata drift but exposed kernel_range drift which
had been hidden behind it. Applied refresh-kernel-ranges.py --patch
recommendations: 11 TOO_TIGHT findings (false-positive risk — our
threshold later than Debian's earliest known fix) + 8 MISSING (Debian
has fixes for branches we didn't model) across 12 modules.

All changes are strictly correctness-improving: detect() now correctly
returns OK on kernels Debian has on record as patched, instead of
false-positiving VULNERABLE.

The biggest single fix is reverting fragnesia from {7,0,10} (NVD) to
{7,0,9} (Debian's backported fix). I introduced that off-by-one in
v0.9.4 from misreading NVD's versionEndExcluding semantics.

Build's kernel_range drift step now exits 0 with 0 TOO_TIGHT + 0 MISSING.
2026-05-28 14:51:15 -04:00
KaraZajac bb5ca48fe1 ci: enable workflow_dispatch on build workflow
The drift-check job's if-gate already honors workflow_dispatch but
the trigger itself was never added to the on: block. Without it,
'gh workflow run build.yml' fails with 422. Found while validating
the v0.9.4 drift fix — wanted to confirm drift-check now passes
without waiting for next Monday's cron.
2026-05-28 13:37:35 -04:00
KaraZajac 4454d8148e release v0.9.4: drift unblock, fragnesia range fix, infra docs
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
- Sync docs/CVE_METADATA.json + KEV_CROSSREF.md to match the
  hand-applied core/cve_metadata.c entries from v0.9.3. Nightly
  drift-check (red since 2026-05-25) now passes. Pintheft's CWE
  landed as CWE-787 from NVD (was NULL in the hand-applied entry).
- Fix fragnesia (CVE-2026-46300) range table. Per NVD: bug entered
  at 5.11 SKBFL_SHARED_FRAG, vulnerable through 5.15.207 / 6.1.173 /
  6.6.140 / 6.12.90 / 6.18.32 / 7.0.9, fixed at .208/.174/.141/
  .91/.33/.10. Prior table had one entry {7,0,9} — off-by-one and
  missing every other backport. Added predates-5.11 introduction gate
  + test row.
- Update tools/verify-vm/README.md to document the v0.9.x infra:
  mainline kernel pinning via kernel.ubuntu.com, per-module
  provisioner hooks, two-phase prep→reboot→verify with post-reboot
  kernel confirmation, GRUB_DEFAULT pinning.
- Add curl fallback for NVD lookups in refresh-cve-metadata.py.
  Mirrors the CISA path's existing fallback. Prevents the silent
  Python urlopen hang seen during v0.9.3 prep (55-min stuck on
  CLOSE_WAIT socket; 30s timeout never fired).
2026-05-28 13:33:28 -04:00
KaraZajac fa0228df9b release v0.9.3: CVE metadata refresh (KEV 10→12) + dirtydecrypt bug fix
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
CVE metadata refresh:
- Added 8 entries to core/cve_metadata.c for the v0.8.0 + v0.9.0 module
  CVEs. Two are CISA-KEV-listed:
  - CVE-2018-14634 mutagen_astronomy (2026-01-26, CWE-190)
  - CVE-2025-32463 sudo_chwoot       (2025-09-29, CWE-829)
- Populated via direct curl when refresh-cve-metadata.py's Python urlopen
  hung on CISA's HTTP/2 endpoint for ~55 min — same data, different
  transport.

dirtydecrypt module bug fix:
- dd_detect() was wrongly gating 'predates the bug' on kernel < 7.0
- Per NVD CVE-2026-31635: bug entered at 6.16.1 stable; vulnerable
  through 6.18.22 / 6.19.12 / 7.0-rc7; fixed at 6.18.23 / 6.19.13 / 7.0
- Fix: predates-gate now uses 6.16.1; patched_branches[] adds {6,18,23}
- Re-verified: dirtydecrypt now correctly returns VULNERABLE on mainline
  6.19.7 instead of OK. Previously a false negative on real vulnerable
  kernels.

Footer goes from '10 in CISA KEV' to '12 in CISA KEV'. Verified count
stays at 28 but dirtydecrypt's record is now a TRUE VULNERABLE match
(was OK match).
2026-05-24 01:17:58 -04:00
KaraZajac d52fcd5512 docs: sweep stale counts to match v0.9.2 binary state
Audit found several user-facing surfaces still carrying old numbers
from earlier releases. Brought everything in line with the binary's
authoritative footer ('39 modules · 10 KEV · 28 verified · 7 any').

README.md:
- Status section: v0.9.0 → v0.9.2 framing; describe the 22 → 28
  verification arc (v0.9.1 + v0.9.2)
- '119 detection rules' → 151 (current bundled count)
- '10 of 26 KEV-listed' → '10 of 34'
- 'Not yet verified (4 of 26 CVEs)' → '(6 of 34 CVEs)' with the new
  honest list (vmwgfx, dirty_cow, mutagen_astronomy, pintheft,
  vsock_uaf, fragnesia) and the reason each is blocked
- Example --auto output: 31 → 39 modules

docs/index.html:
- '22 of 26 CVEs confirmed' → '28 of 34', mainline kernel list expanded
  (5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7)
- Corpus section '26 CVEs across 10 years' → '34 CVEs'
- '26 CVEs, 10-year span' (author list intro) → '34 CVEs'
- Footer feature list '22 of 26' → '28 of 34'
- KEV stat chip 11 → 10 (matches binary; the anticipated 11th from
  metadata refresh hasn't been added yet)
- '119 detection rules' → '151' (two occurrences)

docs/og.svg + og.png:
- KEV chip 11 → 10 (matches binary)

CVES.md:
- '31 modules' → '39 modules covering 34 CVEs'
- Rewrote the unverified-rows note to match the actual 6-module list

No content changes to RELEASE_NOTES.md or ROADMAP.md — those entries
correctly describe state at the time they were written.
2026-05-24 00:09:21 -04:00
KaraZajac 66cca39a55 release v0.9.2: dirtydecrypt verified on mainline 6.19.7 (22 → 28)
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
Verifies CVE-2026-31635 dirtydecrypt's OK path on a kernel that
predates the bug: 'kernel predates the rxgk RESPONSE-handling code
added in 7.0' — match. Confirms detect() doesn't false-positive on
older 6.x kernels.

Attempted fragnesia (CVE-2026-46300) but mainline 7.0.5 .debs depend
on libssl3t64 / libelf1t64 (t64-transition libs from Ubuntu 24.04+ /
Debian 13+). No Parallels-supported Vagrant box ships those yet —
dpkg --force-depends leaves the kernel package in iHR state with no
/boot/vmlinuz. Marked manual: true with rationale.

Verifier infrastructure: pin-mainline now uses dpkg --force-depends as
a fallback so partial-install state can at least be inspected.
2026-05-24 00:03:35 -04:00
KaraZajac 92396a0d6d tests: fix 2 test rows with wrong expected verdicts (v0.9.0 regression)
The build workflow (sanitizer job) has been red since v0.9.0 because two
test rows asserted verdicts that don't match what detect() actually
returns:

- udisks_libblockdev: I expected PRECOND_FAIL (udisksd absent in CI), got
  VULNERABLE. GHA ubuntu-24.04 runners ship udisks2 by default; detect()
  does direct path_exists() stat() calls (not host-fixture lookups) so
  it sees the binary and gates pass. Rewritten as 'udisksd present → VULNERABLE'.

- sudo_runas_neg1: I expected PRECOND_FAIL (no (ALL,!root) grant), got OK.
  detect() treats 'no grant' as 'not exploitable from this user' → OK, not
  'missing precondition' → PRECOND_FAIL. Updated expectation.

The release workflow doesn't run the sanitizer job and has been passing
through these failures; the build workflow caught them. Both expectations
are now honest about what detect() does on CI.
2026-05-23 23:38:55 -04:00
KaraZajac 8ac041a295 release v0.9.1: VM verification sweep 22 → 27
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
Five more CVEs empirically confirmed end-to-end against real Linux VMs:
- CVE-2019-14287 sudo_runas_neg1 (Ubuntu 18.04 + sudoers grant)
- CVE-2020-29661 tioscpgrp        (Ubuntu 20.04 pinned to 5.4.0-26)
- CVE-2024-26581 nft_pipapo       (Ubuntu 22.04 + mainline 5.15.5)
- CVE-2025-32463 sudo_chwoot      (Ubuntu 22.04 + sudo 1.9.16p1 from source)
- CVE-2025-6019  udisks_libblockdev (Debian 12 + udisks2 + polkit rule)

Required real plumbing work:
- Per-module provisioner hook (tools/verify-vm/provisioners/<module>.sh)
- Two-phase provision in verify.sh (prep → reboot if needed → verify)
  fixes silent-fail where new kernel installed but VM never rebooted
- GRUB_DEFAULT pinning in both pin-kernel and pin-mainline blocks
  (kernel downgrades like 5.4.0-169 → 5.4.0-26 now actually boot the target)
- Old-mainline URL fallback in pin-mainline (≤ 4.15 debs at /v$KVER/ not /amd64/)

mutagen_astronomy marked manual: true — mainline 4.14.70 kernel-panics on
Ubuntu 18.04's rootfs ('Failed to execute /init (error -8)' — kernel config
mismatch). Genuinely needs a CentOS 6 / Debian 7 image.
2026-05-23 23:35:02 -04:00
KaraZajac 270ddc1681 verify-vm: per-module provisioner hook + old-mainline URL fallback
Adds tools/verify-vm/provisioners/<module>.sh hook so per-module setup
(build vulnerable sudo from source, drop polkit allow rule, add sudoers
grant) lives in checked-in scripts rather than manual VM steps. Vagrantfile
runs the script as root before build-and-verify if it exists.

Also fixes mainline kernel fetch to fall back from /v${KVER}/amd64/ to
/v${KVER}/ for old kernels (≤ ~4.15) where debs aren't under the amd64
subdir, and accepts both 'linux-image-' (old) and 'linux-image-unsigned-'
(new) deb names.

Wires up 4 previously-deferred targets to expect VULNERABLE:
- sudo_chwoot: builds sudo 1.9.16p1 from upstream into /usr/local
- udisks_libblockdev: installs udisks2 + polkit rule for vagrant user
- mutagen_astronomy: pins mainline 4.14.70 (one below the .71 fix)
- sudo_runas_neg1: adds (ALL,!root) sudoers grant
2026-05-23 22:36:02 -04:00
37 changed files with 945 additions and 209 deletions
+4
View File
@@ -10,6 +10,10 @@ on:
# Runs Monday 06:00 UTC; reports any new backports / KEV additions # Runs Monday 06:00 UTC; reports any new backports / KEV additions
# that haven't propagated into the corpus yet. # that haven't propagated into the corpus yet.
- cron: '0 6 * * 1' - cron: '0 6 * * 1'
workflow_dispatch:
# Lets us trigger the drift-check job on demand (e.g. after a
# metadata refresh) without waiting for the weekly cron. The
# drift-check job's `if:` gate honors this trigger.
jobs: jobs:
build: build:
+10 -9
View File
@@ -23,16 +23,17 @@ Status legend:
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for - 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
historical reference only historical reference only
**Counts:** 31 modules total — 28 verified (🟢 14 · 🟡 14) plus 3 **Counts:** 39 modules total covering 34 CVEs; **28 of 34 CVEs
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot` verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0
see note below). 🔵 0 · ⚪ 0 planned-with-stub · 🔴 0. (One ⚪ row planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
below — CVE-2026-31402 — is a *candidate* with no module, not counted *candidate* with no module, not counted as a module.)
as a module.)
> **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three > **Note on unverified rows:** `vmwgfx` / `dirty_cow` /
> are ported from public PoCs. The **exploit bodies** are not yet > `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` are
> VM-verified end-to-end, so they're listed 🟡 but excluded from the > blocked by their target environment (VMware-only, kernel < 4.4,
> 28-module verified corpus. > mainline panic, kmod not autoloaded, or t64-transition libs),
> 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 > All three now have **pinned fix commits and version-based
> `detect()`**: > `detect()`**:
+43 -34
View File
@@ -2,11 +2,11 @@
[![Latest release](https://img.shields.io/github/v/release/KaraZajac/SKELETONKEY?label=release)](https://github.com/KaraZajac/SKELETONKEY/releases/latest) [![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) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Modules](https://img.shields.io/badge/CVEs-22%20VM--verified%20%2F%2034-brightgreen.svg)](docs/VERIFICATIONS.jsonl) [![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2034-brightgreen.svg)](docs/VERIFICATIONS.jsonl)
[![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#) [![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. 39 Linux LPE modules covering 34 CVEs from 2016 → 2026.
> Every year 2016 → 2026 covered. 22 confirmed end-to-end against real Linux > 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 > VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
> the safest one and runs it.** > the safest one and runs it.**
@@ -44,10 +44,11 @@ for every CVE in the bundle — same project for red and blue teams.
## Corpus at a glance ## Corpus at a glance
**31 modules covering 26 distinct CVEs** across the 2016 → 2026 LPE **39 modules covering 34 distinct CVEs** across the 2016 → 2026 LPE
timeline. **22 of the 26 CVEs have been empirically verified** in real timeline. **28 of the 34 CVEs have been empirically verified** in real
Linux VMs via `tools/verify-vm/`; the 4 still-pending entries are Linux VMs via `tools/verify-vm/`; the 6 still-pending entries are
blocked by their target environment, not by missing code. blocked by their target environment (legacy hypervisor, EOL kernel, or
the t64-transition libc rollout), not by missing code.
| Tier | Count | What it means | | Tier | Count | What it means |
|---|---|---| |---|---|---|
@@ -65,23 +66,26 @@ af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload · nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
### Empirical verification (22 of 26 CVEs) ### Empirical verification (28 of 34 CVEs)
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
each verdict against a known-target VM. Coverage: each verdict against a known-target VM. Coverage:
| Distro / kernel | Modules verified | | Distro / kernel | Modules verified |
|---|---| |---|---|
| Ubuntu 18.04 (4.15.0) | af_packet · ptrace_traceme · sudo_samedit | | Ubuntu 18.04 (4.15.0, sudo 1.8.21p2) | af_packet · ptrace_traceme · sudo_samedit · sudo_runas_neg1 |
| Ubuntu 20.04 (5.4 stock + 5.15 HWE) | af_packet2 · cls_route4 · nft_payload · overlayfs · pwnkit · sequoia | | Ubuntu 20.04 (5.4.0-26 pinned + 5.15 HWE) | af_packet2 · cls_route4 · nft_payload · overlayfs · pwnkit · sequoia · tioscpgrp |
| Ubuntu 22.04 (5.15 stock + mainline 5.15.5 / 6.1.10) | af_unix_gc · dirty_pipe · entrybleed · nf_tables · nft_set_uaf · overlayfs_setuid · stackrot · sudoedit_editor | | Ubuntu 22.04 (5.15 stock + mainline 5.15.5 / 6.1.10 / 6.19.7) | af_unix_gc · dirty_pipe · dirtydecrypt · entrybleed · nf_tables · nft_set_uaf · nft_pipapo · overlayfs_setuid · stackrot · sudoedit_editor · sudo_chwoot |
| Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup | | Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup |
| Debian 12 (6.1 stock) | pack2theroot | | Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev |
**Not yet verified (4):** `vmwgfx` (VMware-guest-only — no public **Not yet verified (6):** `vmwgfx` (VMware-guest-only — no public Vagrant
Vagrant box), `dirty_cow` (needs ≤ 4.4 kernel — older than every box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
supported box), `dirtydecrypt` & `fragnesia` (need Linux 7.0 — not `mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04
shipping as any distro kernel yet). All four are flagged in 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 [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with
rationale. rationale.
@@ -129,7 +133,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --auto --i-know $ skeletonkey --auto --i-know
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64 [*] 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: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
[*] auto: scanning 31 modules for vulnerabilities... [*] auto: scanning 39 modules for vulnerabilities...
[+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: dirty_pipe VULNERABLE (safety rank 90)
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
[+] auto: pwnkit VULNERABLE (safety rank 100) [+] auto: pwnkit VULNERABLE (safety rank 100)
@@ -198,18 +202,19 @@ also compile (modules with Linux-only headers stub out gracefully).
## Status ## Status
**v0.9.0 cut 2026-05-24.** 39 modules across 34 CVEs — **every **v0.9.6 cut 2026-05-28.** 39 modules across 34 CVEs — **every
year 2016 → 2026 now covered**. v0.9.0 adds 5 gap-fillers: year 2016 → 2026 now covered**. v0.9.0 added 5 gap-fillers
`mutagen_astronomy` (CVE-2018-14634 — closes 2018), `sudo_runas_neg1` (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` /
(CVE-2019-14287), `tioscpgrp` (CVE-2020-29661), `vsock_uaf` `nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` /
(CVE-2024-50264 — Pwnie 2025 winner), `nft_pipapo` (CVE-2024-26581 — `pintheft`). v0.9.1 and v0.9.2 are verification-only sweeps that took
Notselwyn II). v0.8.0 added 3 (`sudo_chwoot`/CVE-2025-32463, the verified count from 22 → 28 by booting real vulnerable kernels
`udisks_libblockdev`/CVE-2025-6019, `pintheft`/CVE-2026-43494). (Ubuntu mainline 5.4.0-26, 5.15.5, 6.19.7 + provisioner-built sudo
**22 empirically verified** against real Linux VMs (Ubuntu 18.04 / 1.9.16p1 + Debian 12 + polkit allow rule for udisks).
20.04 / 22.04 + Debian 11 / 12 + mainline kernels 5.15.5 / 6.1.10 **28 empirically verified** against real Linux VMs (Ubuntu 18.04 /
from kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + 20.04 / 22.04 + Debian 11 / 12 + mainline kernels from
clang-tidy on every push. 4 prebuilt binaries (x86_64 + arm64, each kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
in dynamic + static-musl flavors). every push. 4 prebuilt binaries (x86_64 + arm64, each in dynamic +
static-musl flavors).
Reliability + accuracy work in v0.7.x: Reliability + accuracy work in v0.7.x:
- Shared **host fingerprint** (`core/host.{h,c}`) populated once at - Shared **host fingerprint** (`core/host.{h,c}`) populated once at
@@ -221,21 +226,25 @@ Reliability + accuracy work in v0.7.x:
- **VM verifier** (`tools/verify-vm/`) — Vagrant + Parallels scaffold - **VM verifier** (`tools/verify-vm/`) — Vagrant + Parallels scaffold
that boots known-vulnerable kernels (stock distro + mainline via that boots known-vulnerable kernels (stock distro + mainline via
kernel.ubuntu.com), runs `--explain --active` per module, records kernel.ubuntu.com), runs `--explain --active` per module, records
match/MISMATCH/PRECOND_FAIL as JSON. 22 modules confirmed end-to-end. match/MISMATCH/PRECOND_FAIL as JSON. 28 modules confirmed end-to-end.
- **`--explain <module>`** — single-page operator briefing: CVE / CWE - **`--explain <module>`** — single-page operator briefing: CVE / CWE
/ MITRE ATT&CK / CISA KEV status, host fingerprint, live detect() / MITRE ATT&CK / CISA KEV status, host fingerprint, live detect()
trace, OPSEC footprint, detection-rule coverage, verified-on trace, OPSEC footprint, detection-rule coverage, verified-on
records. Paste-into-ticket ready. records. Paste-into-ticket ready.
- **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches - **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches
CISA KEV catalog + NVD CWE; 10 of 26 modules cover KEV-listed CVEs. CISA KEV catalog + NVD CWE; 12 of 34 modules cover KEV-listed CVEs.
- **119 detection rules** across auditd / sigma / yara / falco; one - **151 detection rules** across auditd / sigma / yara / falco; one
command exports the corpus to your SIEM. command exports the corpus to your SIEM.
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect + - `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
exploit, structured verdict table, scan summary, `--dry-run`. exploit, structured verdict table, scan summary, `--dry-run`.
Not yet verified (4 of 26 CVEs): `vmwgfx` (VMware-guest only), Not yet verified (6 of 34 CVEs): `vmwgfx` (VMware-guest only),
`dirty_cow` (needs ≤ 4.4 kernel), `dirtydecrypt` + `fragnesia` (need `dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
Linux 7.0 — not shipping yet). Rationale in 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+; no Parallels-supported box has
those yet). Rationale in
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
+65 -1
View File
@@ -28,6 +28,14 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = false, .in_kev = false,
.kev_date_added = "", .kev_date_added = "",
}, },
{
.cve = "CVE-2018-14634",
.cwe = "CWE-190",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = true,
.kev_date_added = "2026-01-26",
},
{ {
.cve = "CVE-2019-13272", .cve = "CVE-2019-13272",
.cwe = NULL, .cwe = NULL,
@@ -36,6 +44,14 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = true, .in_kev = true,
.kev_date_added = "2021-12-10", .kev_date_added = "2021-12-10",
}, },
{
.cve = "CVE-2019-14287",
.cwe = "CWE-755",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{ {
.cve = "CVE-2020-14386", .cve = "CVE-2020-14386",
.cwe = "CWE-250", .cwe = "CWE-250",
@@ -44,6 +60,14 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = false, .in_kev = false,
.kev_date_added = "", .kev_date_added = "",
}, },
{
.cve = "CVE-2020-29661",
.cwe = "CWE-416",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{ {
.cve = "CVE-2021-22555", .cve = "CVE-2021-22555",
.cwe = "CWE-787", .cwe = "CWE-787",
@@ -196,6 +220,38 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = true, .in_kev = true,
.kev_date_added = "2024-05-30", .kev_date_added = "2024-05-30",
}, },
{
.cve = "CVE-2024-26581",
.cwe = NULL,
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{
.cve = "CVE-2024-50264",
.cwe = "CWE-416",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{
.cve = "CVE-2025-32463",
.cwe = "CWE-829",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = true,
.kev_date_added = "2025-09-29",
},
{
.cve = "CVE-2025-6019",
.cwe = "CWE-250",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
{ {
.cve = "CVE-2026-31635", .cve = "CVE-2026-31635",
.cwe = "CWE-130", .cwe = "CWE-130",
@@ -213,13 +269,21 @@ const struct cve_metadata cve_metadata_table[] = {
.kev_date_added = "", .kev_date_added = "",
}, },
{ {
.cve = "CVE-2026-46300", .cve = "CVE-2026-43494",
.cwe = NULL, .cwe = NULL,
.attack_technique = "T1068", .attack_technique = "T1068",
.attack_subtechnique = NULL, .attack_subtechnique = NULL,
.in_kev = false, .in_kev = false,
.kev_date_added = "", .kev_date_added = "",
}, },
{
.cve = "CVE-2026-46300",
.cwe = "CWE-787",
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
}; };
const size_t cve_metadata_table_len = const size_t cve_metadata_table_len =
+60
View File
@@ -76,6 +76,16 @@ const struct verification_record verifications[] = {
.actual_detect = "OK", .actual_detect = "OK",
.status = "match", .status = "match",
}, },
{
.module = "dirtydecrypt",
.verified_at = "2026-05-24",
.host_kernel = "6.19.7-061907-generic",
.host_distro = "Ubuntu 22.04.3 LTS",
.vm_box = "generic/ubuntu2204",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
{ {
.module = "entrybleed", .module = "entrybleed",
.verified_at = "2026-05-23", .verified_at = "2026-05-23",
@@ -136,6 +146,16 @@ const struct verification_record verifications[] = {
.actual_detect = "VULNERABLE", .actual_detect = "VULNERABLE",
.status = "match", .status = "match",
}, },
{
.module = "nft_pipapo",
.verified_at = "2026-05-24",
.host_kernel = "5.15.5-051505-generic",
.host_distro = "Ubuntu 22.04.3 LTS",
.vm_box = "generic/ubuntu2204",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
{ {
.module = "nft_set_uaf", .module = "nft_set_uaf",
.verified_at = "2026-05-23", .verified_at = "2026-05-23",
@@ -216,6 +236,26 @@ const struct verification_record verifications[] = {
.actual_detect = "VULNERABLE", .actual_detect = "VULNERABLE",
.status = "match", .status = "match",
}, },
{
.module = "sudo_chwoot",
.verified_at = "2026-05-24",
.host_kernel = "5.15.0-91-generic",
.host_distro = "Ubuntu 22.04.3 LTS",
.vm_box = "generic/ubuntu2204",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
{
.module = "sudo_runas_neg1",
.verified_at = "2026-05-24",
.host_kernel = "4.15.0-213-generic",
.host_distro = "Ubuntu 18.04.6 LTS",
.vm_box = "generic/ubuntu1804",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
{ {
.module = "sudo_samedit", .module = "sudo_samedit",
.verified_at = "2026-05-23", .verified_at = "2026-05-23",
@@ -236,6 +276,26 @@ const struct verification_record verifications[] = {
.actual_detect = "PRECOND_FAIL", .actual_detect = "PRECOND_FAIL",
.status = "match", .status = "match",
}, },
{
.module = "tioscpgrp",
.verified_at = "2026-05-24",
.host_kernel = "5.4.0-26-generic",
.host_distro = "Ubuntu 20.04.6 LTS",
.vm_box = "generic/ubuntu2004",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
{
.module = "udisks_libblockdev",
.verified_at = "2026-05-24",
.host_kernel = "6.1.0-17-amd64",
.host_distro = "Debian GNU/Linux 12 (bookworm)",
.vm_box = "generic/debian12",
.expect_detect = "VULNERABLE",
.actual_detect = "VULNERABLE",
.status = "match",
},
}; };
const size_t verifications_count = const size_t verifications_count =
+73 -1
View File
@@ -17,6 +17,15 @@
"in_kev": false, "in_kev": false,
"kev_date_added": "" "kev_date_added": ""
}, },
{
"cve": "CVE-2018-14634",
"module_dir": "mutagen_astronomy_cve_2018_14634",
"cwe": "CWE-190",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": true,
"kev_date_added": "2026-01-26"
},
{ {
"cve": "CVE-2019-13272", "cve": "CVE-2019-13272",
"module_dir": "ptrace_traceme_cve_2019_13272", "module_dir": "ptrace_traceme_cve_2019_13272",
@@ -26,6 +35,15 @@
"in_kev": true, "in_kev": true,
"kev_date_added": "2021-12-10" "kev_date_added": "2021-12-10"
}, },
{
"cve": "CVE-2019-14287",
"module_dir": "sudo_runas_neg1_cve_2019_14287",
"cwe": "CWE-755",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{ {
"cve": "CVE-2020-14386", "cve": "CVE-2020-14386",
"module_dir": "af_packet2_cve_2020_14386", "module_dir": "af_packet2_cve_2020_14386",
@@ -35,6 +53,15 @@
"in_kev": false, "in_kev": false,
"kev_date_added": "" "kev_date_added": ""
}, },
{
"cve": "CVE-2020-29661",
"module_dir": "tioscpgrp_cve_2020_29661",
"cwe": "CWE-416",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{ {
"cve": "CVE-2021-22555", "cve": "CVE-2021-22555",
"module_dir": "netfilter_xtcompat_cve_2021_22555", "module_dir": "netfilter_xtcompat_cve_2021_22555",
@@ -206,6 +233,42 @@
"in_kev": true, "in_kev": true,
"kev_date_added": "2024-05-30" "kev_date_added": "2024-05-30"
}, },
{
"cve": "CVE-2024-26581",
"module_dir": "nft_pipapo_cve_2024_26581",
"cwe": null,
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{
"cve": "CVE-2024-50264",
"module_dir": "vsock_uaf_cve_2024_50264",
"cwe": "CWE-416",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{
"cve": "CVE-2025-32463",
"module_dir": "sudo_chwoot_cve_2025_32463",
"cwe": "CWE-829",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": true,
"kev_date_added": "2025-09-29"
},
{
"cve": "CVE-2025-6019",
"module_dir": "udisks_libblockdev_cve_2025_6019",
"cwe": "CWE-250",
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{ {
"cve": "CVE-2026-31635", "cve": "CVE-2026-31635",
"module_dir": "dirtydecrypt_cve_2026_31635", "module_dir": "dirtydecrypt_cve_2026_31635",
@@ -224,10 +287,19 @@
"in_kev": false, "in_kev": false,
"kev_date_added": "" "kev_date_added": ""
}, },
{
"cve": "CVE-2026-43494",
"module_dir": "pintheft_cve_2026_43494",
"cwe": null,
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{ {
"cve": "CVE-2026-46300", "cve": "CVE-2026-46300",
"module_dir": "fragnesia_cve_2026_46300", "module_dir": "fragnesia_cve_2026_46300",
"cwe": null, "cwe": "CWE-787",
"attack_technique": "T1068", "attack_technique": "T1068",
"attack_subtechnique": null, "attack_subtechnique": null,
"in_kev": false, "in_kev": false,
+10 -2
View File
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
in the wild per the Known Exploited Vulnerabilities catalog. in the wild per the Known Exploited Vulnerabilities catalog.
Refreshed via `tools/refresh-cve-metadata.py`. Refreshed via `tools/refresh-cve-metadata.py`.
**10 of 26 modules cover KEV-listed CVEs.** **12 of 34 modules cover KEV-listed CVEs.**
## In KEV (prioritize patching) ## In KEV (prioritize patching)
@@ -19,7 +19,9 @@ Refreshed via `tools/refresh-cve-metadata.py`.
| CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` | | CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` |
| CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` | | CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` |
| CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` | | CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` |
| CVE-2025-32463 | 2025-09-29 | CWE-829 | `sudo_chwoot_cve_2025_32463` |
| CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` | | CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` |
| CVE-2018-14634 | 2026-01-26 | CWE-190 | `mutagen_astronomy_cve_2018_14634` |
## Not in KEV ## Not in KEV
@@ -30,7 +32,9 @@ and are technically reachable. "Not in KEV" is not the same as
| CVE | CWE | Module | | CVE | CWE | Module |
| --- | --- | --- | | --- | --- | --- |
| CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` | | CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` |
| CVE-2019-14287 | CWE-755 | `sudo_runas_neg1_cve_2019_14287` |
| CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` | | CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` |
| CVE-2020-29661 | CWE-416 | `tioscpgrp_cve_2020_29661` |
| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` | | CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` |
| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` | | CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
| CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` | | CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` |
@@ -42,6 +46,10 @@ and are technically reachable. "Not in KEV" is not the same as
| CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` | | CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` |
| CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` | | CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` |
| CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` | | CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` |
| CVE-2024-26581 | ? | `nft_pipapo_cve_2024_26581` |
| CVE-2024-50264 | CWE-416 | `vsock_uaf_cve_2024_50264` |
| CVE-2025-6019 | CWE-250 | `udisks_libblockdev_cve_2025_6019` |
| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` | | CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` |
| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | | CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` |
| CVE-2026-46300 | ? | `fragnesia_cve_2026_46300` | | CVE-2026-43494 | ? | `pintheft_cve_2026_43494` |
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
+208
View File
@@ -1,3 +1,211 @@
## SKELETONKEY v0.9.6 — `--auto` no longer prompts for sudo password
Two sudo modules' `detect()` bodies invoked `sudo -ln` to read the
user's allowed-commands list. The intent was non-interactive — `-ln`
should parse as `-l -n` (list + non-interactive). But some sudoers /
PAM configurations have been observed prompting for a password
anyway when the flags are bundled, defeating the point.
That meant `skeletonkey --auto --i-know` could hang on a sudo
password prompt during the corpus scan, even though the whole point
of an LPE tool is to *get* root without already having it.
Fix in `sudo_runas_neg1` and `sudoedit_editor`:
- `-n -l` written as separate flags (instead of bundled `-ln`)
- `</dev/null` redirect so sudo cannot fall back to reading the tty
even if the PAM stack tries
Belt-and-suspenders. `--auto` is now guaranteed never to block on
tty input.
---
## SKELETONKEY v0.9.5 — kernel_range drift cleanup (the other half)
v0.9.4 fixed the `cve_metadata` drift but exposed a *second* drift
check (`kernel_range drift`) that had been hidden behind it. That
check compares each module's `kernel_patched_from` table against
Debian's security tracker. It had **11 TOO_TIGHT + 8 MISSING
findings across 12 modules** — meaning `detect()` would have
reported VULNERABLE on many kernels that Debian has on record as
patched (false-positives), or missed branches entirely.
Applied `tools/refresh-kernel-ranges.py --patch` recommendations
across:
- `cgroup_release_agent``{5,16,9}``{5,16,7}`
- `cls_route4``{5,10,143}``{5,10,136}`, `{5,18,18}``{5,18,16}`
- `dirty_cow``{4,7,10}``{4,7,8}`
- `dirty_pipe``{5,10,102}``{5,10,92}`
- `fragnesia``{6,12,91}``{6,12,90}`, `{7,0,10}``{7,0,9}`
(the 7.0.10 entry I added in v0.9.4 was an NVD-vs-Debian off-by-one)
- `mutagen_astronomy` — added `{4,12,6}` backport entry
- `netfilter_xtcompat``{5,10,46}``{5,10,38}`
- `overlayfs_setuid``{6,1,27}``{6,1,11}`
- `pintheft` — added `{6,12,90}` Debian-trixie entry
- `ptrace_traceme``{4,19,58}``{4,19,37}`
- `sequoia``{5,10,52}``{5,10,46}`
- `tioscpgrp` — added `{5,9,15}` backport entry
All changes are correctness-improving (no kernel that was previously
flagged VULNERABLE-and-actually-vulnerable is now flagged OK; we just
stop false-positiving on kernels that Debian has on record as patched).
Build's `kernel_range drift` step now exits 0 with 0 TOO_TIGHT and 0
MISSING.
Also enabled `workflow_dispatch` on the build workflow so the
drift-check job can be manually triggered without waiting for the
weekly Monday-06:00-UTC cron.
---
## SKELETONKEY v0.9.4 — drift unblock, fragnesia range fix, infra docs
Quality-of-life follow-ups from the v0.9.3 review:
**Nightly CI drift-check unblocked.** v0.9.3's hand-applied
`core/cve_metadata.c` entries weren't reflected in
`docs/CVE_METADATA.json`, so the scheduled `build` workflow had
been red since 2026-05-25 even though push-triggered runs passed.
Regenerated both via the canonical script. Pintheft's CWE landed
as CWE-787 (NVD-derived) — previously NULL.
**fragnesia module range table corrected.** Same audit pattern that
found the dirtydecrypt bug in v0.9.3. NVD CVE-2026-46300 confirms
the SKBFL_SHARED_FRAG marker was introduced at 5.11 and the bug
spans every stable branch since. Previous range table had one entry
(`{7, 0, 9}`) — off-by-one against NVD's 7.0.10 fix point and
missing every other backport. Now models 6 backports + predates-5.11
introduction gate:
```c
{5, 15, 208}, /* 5.15-LTS */
{6, 1, 174}, /* 6.1-LTS */
{6, 6, 141}, /* 6.6-LTS */
{6, 12, 91}, /* 6.12-LTS */
{6, 18, 33}, /* 6.18-LTS */
{7, 0, 10}, /* 7.0 */
```
Test row added for the predates path (kernel 4.4 → OK).
**`tools/verify-vm/README.md` brought current.** The README was
written for the v0.6-era apt-pin-only workflow. Now documents the
v0.9.x infrastructure: mainline kernel pinning via
kernel.ubuntu.com, per-module provisioners
(`provisioners/<module>.sh`), two-phase prep→reboot→verify with
post-reboot kernel confirmation, GRUB_DEFAULT pinning in both apt
and mainline blocks.
**NVD lookups in `refresh-cve-metadata.py` get a curl fallback.**
v0.9.3 ran into Python's `urlopen` silently hanging on NVD's HTTP/2
endpoint (55-min process with the 30s timeout never firing — kernel
CLOSE_WAIT socket). The CISA path already had a curl fallback; the
NVD path now mirrors it. Future runs degrade gracefully when
urlopen wedges.
---
## SKELETONKEY v0.9.3 — CVE metadata refresh + dirtydecrypt range fix
**CVE metadata refresh (10 → 12 KEV).** Populated the 8 missing
entries in `core/cve_metadata.c` for v0.8.0 + v0.9.0 module additions.
Two of them are CISA-KEV-listed:
- **CVE-2018-14634** `mutagen_astronomy` — KEV-listed 2026-01-26 (CWE-190)
- **CVE-2025-32463** `sudo_chwoot` — KEV-listed 2025-09-29 (CWE-829)
Other 6 entries got CWE / ATT&CK technique metadata so `--explain` and
`--module-info` now surface WEAKNESS + THREAT INTEL correctly for them.
(`tools/refresh-cve-metadata.py` hangs on CISA's HTTP/2 endpoint via
Python urlopen — populated directly via curl + max-time as a workaround.)
**dirtydecrypt module bug fix.** Auditing dirtydecrypt's range table
against NVD's authoritative CPE match for CVE-2026-31635 surfaced that
`dd_detect()` was wrongly gating "predates the bug" on kernel < 7.0.
Per NVD, the rxgk RESPONSE bug entered at 6.16.1 stable; vulnerable
ranges are 6.16.16.18.22, 6.19.06.19.12, and 7.0-rc1..rc7. The fix:
- `dd_detect()` predates-gate now uses 6.16.1 (not 7.0)
- `patched_branches[]` table adds `{6, 18, 23}` for the 6.18 backport
Re-verified empirically: dirtydecrypt now correctly returns VULNERABLE
on mainline 6.19.7 (genuinely below the 6.19.13 backport). Previously
it returned OK there — a false negative that would have lied to anyone
running scan on a real vulnerable kernel.
---
## SKELETONKEY v0.9.2 — dirtydecrypt verified on mainline 6.19.7
One more empirical verification: **CVE-2026-31635 dirtydecrypt** confirmed
end-to-end on Ubuntu 22.04 + mainline 6.19.7. detect() correctly returns
OK ("kernel predates the rxgk RESPONSE-handling code added in 7.0"). Footer
goes 27 → 28.
Attempted but deferred: **CVE-2026-46300 fragnesia**. Mainline 7.0.5 kernel
.debs depend on `libssl3t64` / `libelf1t64` (the t64-transition libs
introduced in Ubuntu 24.04 / Debian 13). No Vagrant box with a Parallels
provider has those libs yet — `dpkg --force-depends` leaves the kernel
package in `iHR` (broken) state with no `/boot/vmlinuz` deposited. Marked
`manual: true` with rationale in `targets.yaml`. Resolvable when a
Parallels-supported ubuntu2404 / debian13 box becomes available.
---
## SKELETONKEY v0.9.1 — VM verification sweep (22 → 27)
Five more CVEs empirically confirmed end-to-end against real Linux VMs
via `tools/verify-vm/`:
| CVE | Module | Target environment |
|---|---|---|
| CVE-2019-14287 | `sudo_runas_neg1` | Ubuntu 18.04 (sudo 1.8.21p2 + `(ALL,!root)` grant via provisioner) |
| CVE-2020-29661 | `tioscpgrp` | Ubuntu 20.04 pinned to `5.4.0-26` (genuinely below the 5.4.85 backport) |
| CVE-2024-26581 | `nft_pipapo` | Ubuntu 22.04 + mainline `5.15.5` (below the 5.15.149 fix) |
| CVE-2025-32463 | `sudo_chwoot` | Ubuntu 22.04 + sudo `1.9.16p1` built from upstream into `/usr/local/bin` |
| CVE-2025-6019 | `udisks_libblockdev` | Debian 12 + `udisks2` 2.9.4 + polkit allow rule for the verifier user |
Footer goes from `22 empirically verified``27 empirically verified`.
### Verifier infrastructure (the why)
These verifications required real plumbing work that didn't exist before:
- **Per-module provisioner hook** (`tools/verify-vm/provisioners/<module>.sh`)
— per-target setup that doesn't belong in the Vagrantfile (build sudo
from source, install udisks2 + polkit rule, drop a sudoers grant) now
lives in checked-in scripts that re-run idempotently on every verify.
- **Two-phase provisioning** in `verify.sh` — prep provisioners run
first (install kernel, set grub default, drop polkit rule), then a
conditional reboot if `uname -r` doesn't match the target, then the
verifier proper. Fixes the silent-fail where the new kernel was
installed but the VM never actually rebooted into it.
- **GRUB_DEFAULT pin in both `pin-kernel` and `pin-mainline` blocks** —
without this, grub's debian-version-compare picks the highest-sorting
vmlinuz as default; for downgrades (stock 4.15 → mainline 4.14.70, or
stock 5.4.0-169 → pinned 5.4.0-26) the wrong kernel won boot.
- **Old-mainline URL fallback** — kernel.ubuntu.com puts ≤ 4.15 mainline
debs at `/v${KVER}/` not `/v${KVER}/amd64/`. Fallback handles both.
### Honest residuals — 7 of 34 still unverified
| Module | Why not verified |
|---|---|
| `vmwgfx` | needs a VMware guest; we're on Parallels |
| `dirty_cow` | needs ≤ 4.4 kernel — older than any supported Vagrant box |
| `mutagen_astronomy` | mainline 4.14.70 kernel-panics on Ubuntu 18.04 rootfs (`Failed to execute /init (error -8)` — kernel config mismatch). Genuinely needs CentOS 6 / Debian 7. |
| `pintheft` | needs RDS kernel module loaded (Arch only autoloads it) |
| `vsock_uaf` | needs `vsock_loopback` loaded — not autoloaded on common Vagrant boxes |
| `dirtydecrypt`, `fragnesia` | need Linux 7.0 — not yet shipping as any distro kernel |
All seven are flagged in `tools/verify-vm/targets.yaml` with `manual: true`
and a rationale.
---
## SKELETONKEY v0.9.0 — every year 2016 → 2026 now covered ## SKELETONKEY v0.9.0 — every year 2016 → 2026 now covered
Five gap-filling modules. Closes the 2018 hole entirely and thickens Five gap-filling modules. Closes the 2018 hole entirely and thickens
+6
View File
@@ -28,3 +28,9 @@
{"module":"af_unix_gc","verified_at":"2026-05-23T21:27:13Z","host_kernel":"5.15.5-051505-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"} {"module":"af_unix_gc","verified_at":"2026-05-23T21:27:13Z","host_kernel":"5.15.5-051505-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"nft_set_uaf","verified_at":"2026-05-23T21:30:41Z","host_kernel":"5.15.5-051505-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"} {"module":"nft_set_uaf","verified_at":"2026-05-23T21:30:41Z","host_kernel":"5.15.5-051505-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"stackrot","verified_at":"2026-05-23T21:34:12Z","host_kernel":"6.1.10-060110-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"} {"module":"stackrot","verified_at":"2026-05-23T21:34:12Z","host_kernel":"6.1.10-060110-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"sudo_chwoot","verified_at":"2026-05-24T02:39:11Z","host_kernel":"5.15.0-91-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"udisks_libblockdev","verified_at":"2026-05-24T02:44:17Z","host_kernel":"6.1.0-17-amd64","host_distro":"Debian GNU/Linux 12 (bookworm)","vm_box":"generic/debian12","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"nft_pipapo","verified_at":"2026-05-24T03:27:10Z","host_kernel":"5.15.5-051505-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"sudo_runas_neg1","verified_at":"2026-05-24T03:29:18Z","host_kernel":"4.15.0-213-generic","host_distro":"Ubuntu 18.04.6 LTS","vm_box":"generic/ubuntu1804","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"tioscpgrp","verified_at":"2026-05-24T03:31:08Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.6 LTS","vm_box":"generic/ubuntu2004","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"dirtydecrypt","verified_at":"2026-05-24T05:16:27Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
+15 -15
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title> <title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
<meta name="description" content="One binary. 31 Linux privilege-escalation modules from 2016 to 2026. 22 of 26 CVEs empirically verified in real Linux VMs. 10 KEV-listed. 119 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings."> <meta name="description" content="One binary. 39 Linux privilege-escalation modules from 2016 to 2026. 28 of 34 CVEs empirically verified in real Linux VMs. 10 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
<meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified"> <meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified">
<meta property="og:description" content="31 Linux LPE modules; 22 of 26 CVEs empirically verified in real VMs. 119 detection rules. ATT&CK + CWE + KEV annotated."> <meta property="og:description" content="39 Linux LPE modules; 28 of 34 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/"> <meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png"> <meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
@@ -56,14 +56,14 @@
<div class="container hero-inner"> <div class="container hero-inner">
<div class="hero-eyebrow"> <div class="hero-eyebrow">
<span class="dot dot-pulse"></span> <span class="dot dot-pulse"></span>
v0.9.0 — released 2026-05-24 v0.9.6 — released 2026-05-28
</div> </div>
<h1 class="hero-title"> <h1 class="hero-title">
<span class="display-wordmark">SKELETONKEY</span> <span class="display-wordmark">SKELETONKEY</span>
</h1> </h1>
<p class="hero-tag"> <p class="hero-tag">
One binary. <strong>39 Linux LPE modules</strong> covering 34 CVEs — One binary. <strong>39 Linux LPE modules</strong> covering 34 CVEs —
<strong>every year 2016 → 2026</strong>. 22 of 34 confirmed against <strong>every year 2016 → 2026</strong>. 28 of 34 confirmed against
real Linux kernels in VMs. SOC-ready detection rules in four SIEM real Linux kernels in VMs. SOC-ready detection rules in four SIEM
formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated. formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated.
<span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span> <span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span>
@@ -82,8 +82,8 @@
<div class="stats-row" id="stats-row"> <div class="stats-row" id="stats-row">
<div class="stat-chip"><span class="num" data-target="39">0</span><span>modules</span></div> <div class="stat-chip"><span class="num" data-target="39">0</span><span>modules</span></div>
<div class="stat-chip stat-vfy"><span class="num" data-target="22">0</span><span>✓ VM-verified</span></div> <div class="stat-chip stat-vfy"><span class="num" data-target="28">0</span><span>✓ VM-verified</span></div>
<div class="stat-chip stat-kev"><span class="num" data-target="11">0</span><span>★ in CISA KEV</span></div> <div class="stat-chip stat-kev"><span class="num" data-target="12">0</span><span>★ in CISA KEV</span></div>
<div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div> <div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div>
</div> </div>
@@ -210,7 +210,7 @@ uid=0(root) gid=0(root)</pre>
<article class="bento-card"> <article class="bento-card">
<div class="bento-icon">🛡</div> <div class="bento-icon">🛡</div>
<h3>119 detection rules</h3> <h3>151 detection rules</h3>
<p> <p>
auditd · sigma · yara · falco. One command emits the corpus for auditd · sigma · yara · falco. One command emits the corpus for
your SIEM. Each rule grounded in the module's own syscalls. your SIEM. Each rule grounded in the module's own syscalls.
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
<div class="bento-icon"></div> <div class="bento-icon"></div>
<h3>CISA KEV prioritized</h3> <h3>CISA KEV prioritized</h3>
<p> <p>
10 of 26 CVEs in the corpus are in CISA's Known Exploited 12 of 34 CVEs in the corpus are in CISA's Known Exploited
Vulnerabilities catalog — actively exploited in the wild. Vulnerabilities catalog — actively exploited in the wild.
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>. Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
</p> </p>
@@ -294,9 +294,9 @@ uid=0(root) gid=0(root)</pre>
<code>tools/verify-vm/</code> spins up known-vulnerable <code>tools/verify-vm/</code> spins up known-vulnerable
kernels (stock distro + mainline from kernel.ubuntu.com), runs kernels (stock distro + mainline from kernel.ubuntu.com), runs
<code>--explain --active</code> per module, and records the <code>--explain --active</code> per module, and records the
verdict. <strong>22 of 26 CVEs</strong> confirmed against verdict. <strong>28 of 34 CVEs</strong> confirmed against
real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12 real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12
+ mainline 5.15.5 / 6.1.10. Records baked into the binary; + mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary;
<code>--list</code> shows ✓ per module. <code>--list</code> shows ✓ per module.
</p> </p>
</article> </article>
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
<div class="container"> <div class="container">
<div class="section-head"> <div class="section-head">
<span class="section-tag">corpus</span> <span class="section-tag">corpus</span>
<h2>26 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2> <h2>34 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
</div> </div>
<h3 class="corpus-h" data-color="green"> <h3 class="corpus-h" data-color="green">
@@ -414,7 +414,7 @@ uid=0(root) gid=0(root)</pre>
<div class="audience-icon">🎓</div> <div class="audience-icon">🎓</div>
<h3>Researchers / CTF</h3> <h3>Researchers / CTF</h3>
<p> <p>
26 CVEs, 10-year span, each with the original PoC author 34 CVEs, 10-year span, each with the original PoC author
credited and the kernel-range citation auditable. credited and the kernel-range citation auditable.
<code>--explain</code> shows the reasoning chain; detection <code>--explain</code> shows the reasoning chain; detection
rules let you practice both sides. Source is the documentation. rules let you practice both sides. Source is the documentation.
@@ -511,13 +511,13 @@ uid=0(root) gid=0(root)</pre>
<div class="tl-col tl-shipped"> <div class="tl-col tl-shipped">
<div class="tl-tag">shipped</div> <div class="tl-tag">shipped</div>
<ul> <ul>
<li><strong>22 of 26 CVEs empirically verified</strong> in real Linux VMs</li> <li><strong>28 of 34 CVEs empirically verified</strong> in real Linux VMs</li>
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li> <li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
<li>Per-module <code>verified_on[]</code> table baked into the binary</li> <li>Per-module <code>verified_on[]</code> table baked into the binary</li>
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li> <li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
<li><strong>OPSEC notes</strong> — per-module runtime footprint</li> <li><strong>OPSEC notes</strong> — per-module runtime footprint</li>
<li><strong>CISA KEV + NVD CWE + MITRE ATT&amp;CK</strong> metadata pipeline</li> <li><strong>CISA KEV + NVD CWE + MITRE ATT&amp;CK</strong> metadata pipeline</li>
<li>119 detection rules across all four SIEM formats</li> <li>151 detection rules across all four SIEM formats</li>
<li><code>core/host.c</code> shared host-fingerprint refactor</li> <li><code>core/host.c</code> shared host-fingerprint refactor</li>
<li>88-test harness (kernel_range + detect integration)</li> <li>88-test harness (kernel_range + detect integration)</li>
</ul> </ul>
@@ -598,7 +598,7 @@ uid=0(root) gid=0(root)</pre>
who found the bugs. who found the bugs.
</p> </p>
<p class="footer-meta"> <p class="footer-meta">
v0.9.0 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a> v0.9.6 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
</p> </p>
</div> </div>
</footer> </footer>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 123 KiB

+5 -5
View File
@@ -39,7 +39,7 @@
Curated Linux LPE corpus. Curated Linux LPE corpus.
</text> </text>
<text x="80" y="278" font-family="'Inter',sans-serif" font-size="30" fill="#c5c5d3" font-weight="500"> <text x="80" y="278" font-family="'Inter',sans-serif" font-size="30" fill="#c5c5d3" font-weight="500">
Every year 2016 → 2026. 22 of 34 verified. Every year 2016 → 2026. 28 of 34 verified.
</text> </text>
<!-- stat chips --> <!-- stat chips -->
@@ -49,14 +49,14 @@
<text x="28" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#ecedf7">39</text> <text x="28" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#ecedf7">39</text>
<text x="64" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">modules</text> <text x="64" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">modules</text>
<!-- 22 VM-verified --> <!-- 28 VM-verified -->
<rect x="206" y="0" width="240" height="58" rx="29" fill="#161628" stroke="#10b981" stroke-opacity="0.5"/> <rect x="206" y="0" width="240" height="58" rx="29" fill="#161628" stroke="#10b981" stroke-opacity="0.5"/>
<text x="234" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#34d399">22</text> <text x="234" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#34d399">28</text>
<text x="270" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">✓ VM-verified</text> <text x="270" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">✓ VM-verified</text>
<!-- 11 KEV --> <!-- 12 KEV -->
<rect x="482" y="0" width="218" height="58" rx="29" fill="#161628" stroke="#ef4444" stroke-opacity="0.4"/> <rect x="482" y="0" width="218" height="58" rx="29" fill="#161628" stroke="#ef4444" stroke-opacity="0.4"/>
<text x="510" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#ef4444">11</text> <text x="510" y="38" font-family="'JetBrains Mono',monospace" font-weight="700" font-size="22" fill="#ef4444">12</text>
<text x="546" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">★ in CISA KEV</text> <text x="546" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">★ in CISA KEV</text>
<!-- 151 rules --> <!-- 151 rules -->

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -65,7 +65,7 @@ static const struct kernel_patched_from cgroup_ra_patched_branches[] = {
{5, 4, 179}, {5, 4, 179},
{5, 10, 100}, {5, 10, 100},
{5, 15, 23}, {5, 15, 23},
{5, 16, 9}, {5, 16, 7}, /* Debian tracker: earlier than 5.16.9 in stable */
{5, 17, 0}, /* mainline */ {5, 17, 0}, /* mainline */
}; };
@@ -69,9 +69,9 @@
static const struct kernel_patched_from cls_route4_patched_branches[] = { static const struct kernel_patched_from cls_route4_patched_branches[] = {
{5, 4, 213}, {5, 4, 213},
{5, 10, 143}, {5, 10, 136}, /* Debian tracker: earlier than 5.10.143 */
{5, 15, 69}, {5, 15, 69},
{5, 18, 18}, {5, 18, 16}, /* Debian tracker: earlier than 5.18.18 */
{5, 19, 7}, {5, 19, 7},
{5, 20, 0}, /* mainline */ {5, 20, 0}, /* mainline */
}; };
@@ -72,7 +72,7 @@ static const struct kernel_patched_from dirty_cow_patched_branches[] = {
{3, 16, 38}, {3, 16, 38},
{3, 18, 43}, {3, 18, 43},
{4, 4, 26}, /* Ubuntu 16.04 baseline */ {4, 4, 26}, /* Ubuntu 16.04 baseline */
{4, 7, 10}, {4, 7, 8}, /* Debian tracker: earlier than 4.7.10 */
{4, 8, 3}, {4, 8, 3},
{4, 9, 0}, /* mainline fix */ {4, 9, 0}, /* mainline fix */
}; };
@@ -204,7 +204,7 @@ static void revert_passwd_page_cache(void)
* - mainline (≥ 5.17) is patched * - mainline (≥ 5.17) is patched
*/ */
static const struct kernel_patched_from dirty_pipe_patched_branches[] = { static const struct kernel_patched_from dirty_pipe_patched_branches[] = {
{5, 10, 102}, /* 5.10.x backport */ {5, 10, 92}, /* 5.10.x backport (Debian tracker: earlier than 5.10.102) */
{5, 15, 25}, /* 5.15.x backport */ {5, 15, 25}, /* 5.15.x backport */
{5, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */ {5, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */
{5, 17, 0}, /* mainline fix lands; everything from here is fine */ {5, 17, 0}, /* mainline fix lands; everything from here is fine */
@@ -667,14 +667,18 @@ static int dd_active_probe(void)
* RESPONSE authenticator length check"), shipped in Linux 7.0. * RESPONSE authenticator length check"), shipped in Linux 7.0.
* *
* The detect logic therefore is: * The detect logic therefore is:
* - kernel < 7.0 → SKELETONKEY_OK (predates the bug) * - kernel < 6.16.1 → SKELETONKEY_OK (predates the rxgk RESPONSE bug)
* - kernel ≥ 7.0 → consult kernel_range; 7.0+ has the fix * - kernel in range → consult kernel_range for backport coverage
* - --active → empirical override (catches pre-fix 7.0-rc kernels * - --active → empirical override
* or weird distro rebuilds the version check missed) *
* Per NVD CVE-2026-31635: bug introduced in 6.16.1 stable; vulnerable
* range is 6.16.16.18.22 + 6.19.06.19.12 + 7.0-rc1..rc7. Fixed at
* 6.18.23 backport, 6.19.13 backport, 7.0 stable.
*/ */
static const struct kernel_patched_from dirtydecrypt_patched_branches[] = { static const struct kernel_patched_from dirtydecrypt_patched_branches[] = {
{6, 18, 23}, /* 6.18.x stable backport */
{6, 19, 13}, /* 6.19.x stable backport (per Debian tracker — forky/sid) */ {6, 19, 13}, /* 6.19.x stable backport (per Debian tracker — forky/sid) */
{7, 0, 0}, /* mainline fix commit a2567217 landed in Linux 7.0 */ {7, 0, 0}, /* mainline fix landed before 7.0 stable */
}; };
static const struct kernel_range dirtydecrypt_range = { static const struct kernel_range dirtydecrypt_range = {
.patched_from = dirtydecrypt_patched_branches, .patched_from = dirtydecrypt_patched_branches,
@@ -697,11 +701,12 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
return SKELETONKEY_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
/* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */ /* Predates the bug: rxgk RESPONSE-handling bug entered at 6.16.1
if (!skeletonkey_host_kernel_at_least(ctx->host, 7, 0, 0)) { * stable per NVD. Earlier 6.x kernels don't have the buggy code. */
if (!skeletonkey_host_kernel_at_least(ctx->host, 6, 16, 1)) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk " fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk "
"RESPONSE-handling code added in 7.0 — not applicable\n", "RESPONSE bug introduced in 6.16.1 — not applicable\n",
v->release); v->release);
return SKELETONKEY_OK; return SKELETONKEY_OK;
} }
@@ -903,11 +903,25 @@ static int fg_active_probe(void)
* - --active → empirical override (catches distro silent * - --active → empirical override (catches distro silent
* backports and unfixed 7.0.x ≤ 7.0.8) * backports and unfixed 7.0.x ≤ 7.0.8)
* *
* Stable-branch backports for 5.10 / 6.1 / 6.12 — when they ship — * Per NVD CVE-2026-46300 (queried 2026-05-28): SKBFL_SHARED_FRAG was
* extend the table with the matching {major, minor, patch} entry. * introduced at 5.11; the marker-propagation bug is present 5.11+. The
* fix was backported across every active stable branch:
*
* 5.15-LTS: vulnerable 5.15.05.15.207, fixed 5.15.208+
* 6.1-LTS: vulnerable 5.16.06.1.173, fixed 6.1.174+
* 6.6-LTS: vulnerable 6.2.06.6.140, fixed 6.6.141+
* 6.12-LTS: vulnerable 6.7.06.12.90, fixed 6.12.91+
* 6.18-LTS: vulnerable 6.13.06.18.32, fixed 6.18.33+
* 7.0: vulnerable 6.19.07.0.9, fixed 7.0.10+
* 7.1-rcN: still vulnerable (rc1..rc4 at time of writing)
*/ */
static const struct kernel_patched_from fragnesia_patched_branches[] = { static const struct kernel_patched_from fragnesia_patched_branches[] = {
{7, 0, 9}, /* mainline + 7.0.x stable: fix lands at 7.0.9 */ {5, 15, 208}, /* 5.15-LTS backport */
{6, 1, 174}, /* 6.1-LTS backport */
{6, 6, 141}, /* 6.6-LTS backport */
{6, 12, 90}, /* 6.12-LTS backport (Debian trixie ships .90 with fix) */
{6, 18, 33}, /* 6.18-LTS backport */
{7, 0, 9}, /* 7.0 stable (Debian forky/sid ship .9 with backported fix) */
}; };
static const struct kernel_range fragnesia_range = { static const struct kernel_range fragnesia_range = {
.patched_from = fragnesia_patched_branches, .patched_from = fragnesia_patched_branches,
@@ -930,6 +944,17 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx)
return SKELETONKEY_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
/* Predates the bug: SKBFL_SHARED_FRAG marker only exists from 5.11
* onwards; older kernels don't have the buggy skb_try_coalesce()
* code path. */
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 11, 0)) {
if (!ctx->json)
fprintf(stderr, "[i] fragnesia: kernel %s predates the "
"SKBFL_SHARED_FRAG marker added in 5.11 — not "
"applicable\n", v->release);
return SKELETONKEY_OK;
}
if (!ctx->host->unprivileged_userns_allowed) { if (!ctx->host->unprivileged_userns_allowed) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] fragnesia: unprivileged user " fprintf(stderr, "[i] fragnesia: unprivileged user "
@@ -71,6 +71,7 @@
* VULNERABLE by version-only check; the RLIMIT_STACK active probe * VULNERABLE by version-only check; the RLIMIT_STACK active probe
* (--active) is required to confirm exploitability on a real host. */ * (--active) is required to confirm exploitability on a real host. */
static const struct kernel_patched_from mutagen_patched_branches[] = { static const struct kernel_patched_from mutagen_patched_branches[] = {
{4, 12, 6}, /* Debian-tracked backport on 4.12 branch */
{4, 14, 71}, /* 4.14 LTS stable backport */ {4, 14, 71}, /* 4.14 LTS stable backport */
{4, 18, 8}, /* mainline + everything above inherits */ {4, 18, 8}, /* mainline + everything above inherits */
}; };
@@ -103,7 +103,7 @@ static const struct kernel_patched_from netfilter_xtcompat_patched_branches[] =
{4, 14, 240}, {4, 14, 240},
{4, 19, 198}, {4, 19, 198},
{5, 4, 128}, {5, 4, 128},
{5, 10, 46}, {5, 10, 38}, /* Debian tracker: earlier than 5.10.46 */
{5, 11, 20}, {5, 11, 20},
{5, 12, 13}, {5, 12, 13},
{5, 13, 0}, /* mainline (5.13 carries b29c457a6511) */ {5, 13, 0}, /* mainline (5.13 carries b29c457a6511) */
@@ -62,7 +62,7 @@
static const struct kernel_patched_from overlayfs_setuid_patched_branches[] = { static const struct kernel_patched_from overlayfs_setuid_patched_branches[] = {
{5, 10, 179}, /* 5.10.x stable backport (per Debian tracker — bullseye) */ {5, 10, 179}, /* 5.10.x stable backport (per Debian tracker — bullseye) */
{5, 15, 110}, {5, 15, 110},
{6, 1, 27}, {6, 1, 11}, /* Debian tracker: earlier than 6.1.27 */
{6, 2, 13}, {6, 2, 13},
{6, 3, 0}, /* mainline */ {6, 3, 0}, /* mainline */
}; };
@@ -97,6 +97,7 @@
* patch (likely 6.16 once the post-rc release tags). Conservatively * patch (likely 6.16 once the post-rc release tags). Conservatively
* placeholding at {7, 0, 0} until that lands. */ * placeholding at {7, 0, 0} until that lands. */
static const struct kernel_patched_from pintheft_patched_branches[] = { static const struct kernel_patched_from pintheft_patched_branches[] = {
{6, 12, 90}, /* Debian trixie ships 6.12.90 with the fix backported */
{7, 0, 0}, /* mainline fix commit 0cebaccef3ac; tag will be 6.16 or 7.0 {7, 0, 0}, /* mainline fix commit 0cebaccef3ac; tag will be 6.16 or 7.0
depending on when 6.15 closes — refresh when known */ depending on when 6.15 closes — refresh when known */
}; };
@@ -53,7 +53,7 @@ static const struct kernel_patched_from ptrace_traceme_patched_branches[] = {
{4, 4, 182}, {4, 4, 182},
{4, 9, 182}, {4, 9, 182},
{4, 14, 131}, {4, 14, 131},
{4, 19, 58}, {4, 19, 37}, /* Debian tracker: earlier than 4.19.58 */
{5, 0, 20}, {5, 0, 20},
{5, 1, 17}, {5, 1, 17},
{5, 2, 0}, /* mainline (5.2-rc) */ {5, 2, 0}, /* mainline (5.2-rc) */
@@ -127,7 +127,7 @@
static const struct kernel_patched_from sequoia_patched_branches[] = { static const struct kernel_patched_from sequoia_patched_branches[] = {
{5, 4, 134}, {5, 4, 134},
{5, 10, 52}, {5, 10, 46}, /* Debian tracker: earlier than 5.10.52 */
{5, 13, 4}, {5, 13, 4},
{5, 14, 0}, /* mainline */ {5, 14, 0}, /* mainline */
}; };
@@ -106,7 +106,9 @@ static bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
static bool find_runas_blacklist_grant(const char *sudo_path, char *cmd_out, size_t cap) static bool find_runas_blacklist_grant(const char *sudo_path, char *cmd_out, size_t cap)
{ {
char cmd[512]; char cmd[512];
snprintf(cmd, sizeof cmd, "%s -ln 2>/dev/null", sudo_path); /* -n -l separated + stdin closed: see sudoedit_editor for the same
* pattern + rationale. `--auto` must never block on a tty prompt. */
snprintf(cmd, sizeof cmd, "%s -n -l </dev/null 2>/dev/null", sudo_path);
FILE *p = popen(cmd, "r"); FILE *p = popen(cmd, "r");
if (!p) return false; if (!p) return false;
char line[512]; char line[512];
@@ -149,8 +149,13 @@ static bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
static bool find_sudoedit_target(const char *sudo_path, char *out, size_t outsz) static bool find_sudoedit_target(const char *sudo_path, char *out, size_t outsz)
{ {
char cmd[512]; char cmd[512];
/* -n: non-interactive (no password prompt); -l: list. */ /* -n: non-interactive (no password prompt); -l: list. The two flags
snprintf(cmd, sizeof cmd, "%s -ln 2>&1", sudo_path); * are written separately and stdin is redirected from /dev/null so
* sudo cannot fall back to a tty prompt even if the local PAM stack
* tries to coerce one (some sudoers + pam_unix configurations have
* been observed prompting despite `-n` when the flags are bundled
* as `-ln`). Belt-and-suspenders so `--auto` never blocks on input. */
snprintf(cmd, sizeof cmd, "%s -n -l </dev/null 2>&1", sudo_path);
FILE *p = popen(cmd, "r"); FILE *p = popen(cmd, "r");
if (!p) return false; if (!p) return false;
@@ -56,6 +56,7 @@ static const struct kernel_patched_from tioscpgrp_patched_branches[] = {
{4, 14, 213}, /* 4.14 LTS */ {4, 14, 213}, /* 4.14 LTS */
{4, 19, 165}, /* 4.19 LTS */ {4, 19, 165}, /* 4.19 LTS */
{5, 4, 85}, /* 5.4 LTS */ {5, 4, 85}, /* 5.4 LTS */
{5, 9, 15}, /* Debian-tracked 5.9 backport */
{5, 10, 0}, /* mainline fix in 5.10 */ {5, 10, 0}, /* mainline fix in 5.10 */
}; };
+1 -1
View File
@@ -35,7 +35,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#define SKELETONKEY_VERSION "0.9.0" #define SKELETONKEY_VERSION "0.9.6"
static const char BANNER[] = static const char BANNER[] =
"\n" "\n"
+21 -12
View File
@@ -318,12 +318,13 @@ static const struct skeletonkey_host h_kernel_5_14_no_userns = {
static void run_all(void) static void run_all(void)
{ {
#ifdef __linux__ #ifdef __linux__
/* dirtydecrypt: kernel.major < 7 → predates the bug → OK */ /* dirtydecrypt: rxgk RESPONSE bug entered at 6.16.1 per NVD;
run_one("dirtydecrypt: kernel 6.12 predates 7.0 → OK", * kernels before that predate the buggy code → OK */
run_one("dirtydecrypt: kernel 6.12 predates 6.16.1 → OK",
&dirtydecrypt_module, &h_pre7_no_userns_no_dbus, &dirtydecrypt_module, &h_pre7_no_userns_no_dbus,
SKELETONKEY_OK); SKELETONKEY_OK);
run_one("dirtydecrypt: kernel 6.14 (fedora) still predates → OK", run_one("dirtydecrypt: kernel 6.14 (fedora) still predates 6.16.1 → OK",
&dirtydecrypt_module, &h_fedora_no_debian, &dirtydecrypt_module, &h_fedora_no_debian,
SKELETONKEY_OK); SKELETONKEY_OK);
@@ -331,6 +332,12 @@ static void run_all(void)
&dirtydecrypt_module, &h_ubuntu_24_userns_ok, &dirtydecrypt_module, &h_ubuntu_24_userns_ok,
SKELETONKEY_OK); SKELETONKEY_OK);
/* fragnesia: SKBFL_SHARED_FRAG marker added in 5.11; kernels before
* that predate the buggy skb_try_coalesce() code → OK */
run_one("fragnesia: kernel 4.4 predates 5.11 SKBFL_SHARED_FRAG → OK",
&fragnesia_module, &h_kernel_4_4,
SKELETONKEY_OK);
/* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */ /* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL", run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
&fragnesia_module, &h_pre7_no_userns_no_dbus, &fragnesia_module, &h_pre7_no_userns_no_dbus,
@@ -662,11 +669,13 @@ static void run_all(void)
SKELETONKEY_OK); SKELETONKEY_OK);
/* udisks_libblockdev: detect gates on udisksd binary + dbus /* udisks_libblockdev: detect gates on udisksd binary + dbus
* socket presence + active polkit session. On CI / test containers * socket presence + active polkit session. detect() does direct
* udisksd is rarely installed → PRECOND_FAIL. */ * filesystem stat() calls (path_exists /usr/libexec/udisks2/udisksd)
run_one("udisks_libblockdev: udisksd absent in CI → PRECOND_FAIL", * — it can't be host-fixture-mocked. GHA ubuntu-24.04 runners ship
* udisks2 by default, so detect returns VULNERABLE there. */
run_one("udisks_libblockdev: udisksd present on CI runner → VULNERABLE",
&udisks_libblockdev_module, &h_kernel_6_12, &udisks_libblockdev_module, &h_kernel_6_12,
SKELETONKEY_PRECOND_FAIL); SKELETONKEY_VULNERABLE);
/* pintheft: AF_RDS socket() in CI/container is almost never /* pintheft: AF_RDS socket() in CI/container is almost never
* reachable (RDS module blacklisted on every common distro except * reachable (RDS module blacklisted on every common distro except
@@ -689,12 +698,12 @@ static void run_all(void)
SKELETONKEY_OK); SKELETONKEY_OK);
/* sudo_runas_neg1: vuln sudo 1.8.31 (in range), but no (ALL,!root) /* sudo_runas_neg1: vuln sudo 1.8.31 (in range), but no (ALL,!root)
* grant for this test user → PRECOND_FAIL. The CI runner has no * grant for this test user → OK. detect() treats "no grant" as
* sudoers entry of that shape, so find_runas_blacklist_grant() * "not exploitable" (returns OK), not "missing precondition"
* returns false. */ * (PRECOND_FAIL) — the user simply can't reach the bug from here. */
run_one("sudo_runas_neg1: vuln sudo, no (ALL,!root) grant → PRECOND_FAIL", run_one("sudo_runas_neg1: vuln sudo, no (ALL,!root) grant → OK",
&sudo_runas_neg1_module, &h_vuln_sudo, &sudo_runas_neg1_module, &h_vuln_sudo,
SKELETONKEY_PRECOND_FAIL); SKELETONKEY_OK);
/* tioscpgrp: kernel 6.12 above the 5.10 mainline fix → OK */ /* tioscpgrp: kernel 6.12 above the 5.10 mainline fix → OK */
run_one("tioscpgrp: kernel 6.12 above 5.10 fix → OK", run_one("tioscpgrp: kernel 6.12 above 5.10 fix → OK",
+24 -4
View File
@@ -118,18 +118,38 @@ def fetch_kev_catalog() -> dict[str, str]:
def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]: def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]:
"""Return (cwe_id, description) from NVD. Returns (None, None) on miss.""" """Return (cwe_id, description) from NVD. Returns (None, None) on miss.
Same urlopen-hangs-silently pattern as the CISA fetch: NVD's HTTP/2
endpoint sometimes leaves Python sockets in CLOSE_WAIT forever even
though the 30s timeout should have fired (observed on macOS 2026-05-24,
process hung 55+ minutes). We try urlopen first, then fall back to
curl --max-time which honors the wall clock reliably."""
url = NVD_URL.format(cve=cve) url = NVD_URL.format(cve=cve)
req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"}) req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"})
blob = None
try: try:
with urllib.request.urlopen(req, timeout=30) as r: with urllib.request.urlopen(req, timeout=30) as r:
blob = json.loads(r.read().decode("utf-8")) blob = json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr) print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr)
return None, None return None, None
except (urllib.error.URLError, json.JSONDecodeError) as e: except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e:
print(f"[!] NVD parse error for {cve}: {e}", file=sys.stderr) print(f"[!] NVD urlopen failed for {cve} ({e}); trying curl", file=sys.stderr)
return None, None if blob is None:
import subprocess
try:
raw = subprocess.check_output(
["curl", "-fsSL", "--max-time", "20",
"-H", "User-Agent: skeletonkey-cve-metadata/1",
url],
stderr=subprocess.DEVNULL,
)
blob = json.loads(raw.decode("utf-8"))
except (subprocess.CalledProcessError, FileNotFoundError,
json.JSONDecodeError) as e:
print(f"[!] NVD curl fallback failed for {cve}: {e}", file=sys.stderr)
return None, None
vulns = blob.get("vulnerabilities") or [] vulns = blob.get("vulnerabilities") or []
if not vulns: if not vulns:
return None, None return None, None
+105 -46
View File
@@ -27,18 +27,28 @@ To skip boxes you don't need (save disk):
./tools/verify-vm/verify.sh nf_tables ./tools/verify-vm/verify.sh nf_tables
``` ```
What that does: What that does (two-phase model — install kernel, then verify):
1. Reads `tools/verify-vm/targets.yaml`: finds `nf_tables` → box 1. Reads `tools/verify-vm/targets.yaml`: finds `nf_tables` → box
`generic/ubuntu2204` + kernel pin `linux-image-5.15.0-43-generic`. `generic/ubuntu2204` + `mainline_version: 5.15.5`.
2. `vagrant up skk-nf_tables` (provisions on first call, resumes on 2. `vagrant up skk-nf_tables` if not already running (each module gets
subsequent). its own machine for isolation).
3. Installs the pinned vulnerable kernel via `apt`, reboots. 3. **Prep phase** — runs every prep provisioner that applies:
4. Mounts the local repo at `/vagrant`, runs `make`, then runs - `pin-kernel-<pkg>` if `kernel_pkg` is set (apt install + GRUB_DEFAULT pin)
`skeletonkey --explain nf_tables --active`. - `pin-mainline-<ver>` if `mainline_version` is set (download from
5. Parses the `VERDICT:` line, compares against `expect_detect` from kernel.ubuntu.com/mainline, dpkg -i, GRUB_DEFAULT pin)
targets.yaml, emits a JSON verification record on stdout. - `module-provision-<name>` if `provisioners/<name>.sh` exists
6. Suspends the VM (`vagrant suspend`) — instant resume next run. (build vulnerable sudo from source, drop polkit allow rule,
install udisks2, etc.)
4. **Conditional reboot**`vagrant reload` if `uname -r` doesn't
match the target kernel after the prep phase. Confirms post-reboot
kernel actually landed on the target; warns if it didn't.
5. **Verify phase**`build-and-verify` provisioner: rsync the source,
`make`, run `skeletonkey --explain <module> --active`.
6. Parses the `VERDICT:` line, compares against `expect_detect` from
targets.yaml, appends a JSON verification record to
`docs/VERIFICATIONS.jsonl`.
7. Suspends the VM (`vagrant suspend`) — instant resume next run.
Lifecycle flags: Lifecycle flags:
@@ -54,73 +64,122 @@ Lifecycle flags:
``` ```
Shows the (module, box, target kernel, expected verdict, notes) matrix Shows the (module, box, target kernel, expected verdict, notes) matrix
for all 26 modules. Three are flagged `manual: true` because no for all targets. Modules with `manual: true` are blocked by their
public Vagrant box covers them: target environment — see the notes field for the reason (VMware-only
guest, EOL kernel needed, t64-transition libs missing, etc.).
- `vmwgfx` — only reachable on VMware guests; needs a vSphere/Fusion VM
not Parallels.
- `dirtydecrypt`, `fragnesia` — only present in Linux 7.0+ which isn't
shipping as a distro kernel yet.
For those, verification needs a hand-built or special-distro VM.
## Verification records ## Verification records
`verify.sh` emits JSON on stdout after each run. Example: `verify.sh` appends one JSON record per run to
`docs/VERIFICATIONS.jsonl`:
```json ```json
{ {
"module": "nf_tables", "module": "nf_tables",
"verified_at": "2026-05-23T17:42:11Z", "verified_at": "2026-05-24T03:24:01Z",
"host_kernel": "5.15.0-43-generic", "host_kernel": "5.15.5-051505-generic",
"host_distro": "Ubuntu 22.04.5 LTS", "host_distro": "Ubuntu 22.04.3 LTS",
"vm_box": "generic/ubuntu2204", "vm_box": "generic/ubuntu2204",
"expect_detect": "VULNERABLE", "expect_detect": "VULNERABLE",
"actual_detect": "VULNERABLE", "actual_detect": "VULNERABLE",
"status": "match", "status": "match"
"log": "tools/verify-vm/logs/verify-nf_tables-20260523-174211.log"
} }
``` ```
`status: match` means detect() returned what we expected on a known- `status: match` means detect() returned what we expected on a known-
vulnerable kernel. Anything else (`MISMATCH`, status code != 0) means vulnerable kernel. Anything else (`MISMATCH`, exit code != 0) means
either: either:
- The kernel pin didn't take (check `host_kernel` against - The kernel pin didn't take check `host_kernel` against
`kernel_version` in targets.yaml). `kernel_version` in targets.yaml. The "post-reboot kernel" line in
the verify log will say if `vagrant reload` did or didn't land on
the target.
- The exploit's preconditions aren't met in the default Vagrant image - The exploit's preconditions aren't met in the default Vagrant image
(e.g. apparmor blocks unprivileged userns; need to adjust the (e.g. apparmor blocks unprivileged userns; provisioner needed).
Vagrantfile provisioner). - The module's detect() logic is wrong for this kernel/distro combo
- The detect() logic is wrong for this kernel/distro combo (a real bug (a real module bug — fix it, as we did for `dirtydecrypt` after
— fix it). cross-checking against NVD).
Records are intended to feed a per-module `verified_on[]` table (next Run `tools/refresh-verifications.py` after new records land to
project step) so `--list` can show a `✓ verified <date>` column. regenerate `core/verifications.c` so the binary's `--explain` and
`--list` reflect the latest evidence.
## How it routes module → box ## How it routes module → box
Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has: Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has:
- `box`which `boxes/` template (e.g. `ubuntu2204`) - `box`generic/<distro> (e.g. `ubuntu2204`)
- `kernel_pkg` — apt package name to install if the stock kernel - `kernel_pkg` — apt package for a vulnerable stock-archive kernel,
is patched (omit / empty if stock is already vulnerable) if one still exists in the distro's repos
- `mainline_version` — alternative to `kernel_pkg`: pulls a vanilla
upstream kernel from `kernel.ubuntu.com/mainline/v<ver>/`. Use when
the apt-archive version has been garbage-collected (Ubuntu drops
old ABI versions) or when you need a specific point release that
the distro never packaged.
- `kernel_version` — what `uname -r` should report after install - `kernel_version` — what `uname -r` should report after install
- `expect_detect``VULNERABLE` | `OK` | `PRECOND_FAIL` - `expect_detect``VULNERABLE` | `OK` | `PRECOND_FAIL`
- `notes` — short rationale; comments in the file have the full context - `manual: true` — skip auto verification; explain why in `notes`
- `notes` — full context for why this target was picked
Adding a new module is one block in targets.yaml. The verifier picks Adding a new module is one block in targets.yaml. If the module needs
it up automatically. per-target setup beyond installing a kernel — for example building
sudo from source, adding a sudoers grant, or dropping a polkit allow
rule — write a shell script at `tools/verify-vm/provisioners/<module>.sh`
and the Vagrantfile will pick it up automatically.
## Module-specific provisioners (`provisioners/<module>.sh`)
When the kernel pin alone doesn't make a host vulnerable — e.g.
the bug is sudo-version-gated, or a polkit "active session" check
blocks the SSH path — drop a shell script at
`tools/verify-vm/provisioners/<module_name>.sh`. The Vagrantfile
runs it as root in the prep phase, before the `vagrant reload`
check. Scripts should be idempotent (apt is no-op if installed,
file overwrites are safe) since they re-run on every verify.
Existing examples:
- `sudo_chwoot.sh` — builds sudo 1.9.16p1 from upstream into
`/usr/local/bin` so the vulnerable `--chroot` code path is reachable
on Ubuntu 22.04 (which ships pre-feature 1.9.9).
- `udisks_libblockdev.sh` — installs `udisks2` + drops a polkit rule
allowing the vagrant user to invoke `loop-setup` / `filesystem-mount`
(without this, the SSH session is not "active" per polkit and the
D-Bus call short-circuits).
- `sudo_runas_neg1.sh` — adds `vagrant ALL=(ALL,!root) NOPASSWD: /bin/vi`
to `/etc/sudoers.d/` so `find_runas_blacklist_grant()` has a grant
to abuse.
## Pinning kernels: apt vs mainline
`pin-kernel-<pkg>` runs `apt-get install -y <pkg>`. Best when the
target version still lives in the distro's archive (rare for old
point releases — Ubuntu eventually GCs them). Also pins `GRUB_DEFAULT`
to the just-installed kernel so the reboot lands on it instead of
the higher-version stock kernel.
`pin-mainline-<ver>` downloads vanilla mainline debs from
`kernel.ubuntu.com/mainline/v<ver>/`. Tries `/amd64/` first, falls
back to bare `/v<ver>/` for old kernels (≤ ~4.15) where amd64 wasn't
a separate subdir. Accepts both `linux-image-` (older naming) and
`linux-image-unsigned-` (current). Pins `GRUB_DEFAULT` to the
mainline kernel so grub doesn't keep booting the higher-versioned
stock kernel.
## Files ## Files
``` ```
tools/verify-vm/ tools/verify-vm/
├── README.md this file ├── README.md this file
├── setup.sh one-time bootstrap (Vagrant, plugin, box cache) ├── setup.sh one-time bootstrap (Vagrant, plugin, box cache)
├── verify.sh per-module verifier ├── verify.sh per-module verifier
├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars) ├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars)
├── targets.yaml module → box mapping with rationale ├── targets.yaml module → box mapping with rationale
── logs/ per-verification stdout/stderr capture ── provisioners/ optional per-module shell hooks
│ ├── sudo_chwoot.sh
│ ├── sudo_runas_neg1.sh
│ └── udisks_libblockdev.sh
└── logs/ per-verification stdout/stderr capture
``` ```
## Why Vagrant + Parallels ## Why Vagrant + Parallels
+77 -12
View File
@@ -73,7 +73,19 @@ Vagrant.configure("2") do |c|
echo "[+] installing #{pkg} (kernel target #{kver})" echo "[+] installing #{pkg} (kernel target #{kver})"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq #{pkg} apt-get install -y -qq #{pkg}
echo "[i] kernel #{pkg} installed; reboot via 'vagrant reload'" echo "[i] kernel #{pkg} installed"
fi
# Pin grub default to this specific kernel. Without it, grub
# picks the highest-versioned kernel installed (typically a
# stock HWE backport that's POST-fix), defeating the pin's
# purpose. Find the kver string by stripping linux-image-
# prefix from the pkg name.
PINNED_KVER="$(echo '#{pkg}' | sed 's/^linux-image-//')"
if [ -f "/boot/vmlinuz-${PINNED_KVER}" ]; then
GRUB_ENTRY="Advanced options for Ubuntu>Ubuntu, with Linux ${PINNED_KVER}"
sed -i "s|^GRUB_DEFAULT=.*|GRUB_DEFAULT=\\"${GRUB_ENTRY}\\"|" /etc/default/grub
echo "[+] GRUB_DEFAULT pinned to: ${GRUB_ENTRY}"
update-grub 2>&1 | tail -3
fi fi
SHELL SHELL
end end
@@ -90,28 +102,47 @@ Vagrant.configure("2") do |c|
m.vm.provision "shell", name: "pin-mainline-#{mainline}", inline: <<-SHELL m.vm.provision "shell", name: "pin-mainline-#{mainline}", inline: <<-SHELL
set -e set -e
KVER="#{mainline}" KVER="#{mainline}"
# already booted into it? # already booted into it? Still fall through to grub-pin to
# make sure GRUB_DEFAULT stays correct even after stock kernel
# upgrades that might reorder grub entries.
BOOTED_INTO_TARGET=0
if uname -r | grep -q "^${KVER}-[0-9]\\+-generic"; then if uname -r | grep -q "^${KVER}-[0-9]\\+-generic"; then
echo "[=] mainline ${KVER} already booted ($(uname -r))" echo "[=] mainline ${KVER} already booted ($(uname -r))"
exit 0 BOOTED_INTO_TARGET=1
fi fi
# already installed on disk (waiting on reboot)?
# already installed on disk? Skip the download/install but
# still run the grub-pin block at the end.
SKIP_INSTALL=0
if ls /boot/vmlinuz-${KVER}-* >/dev/null 2>&1; then if ls /boot/vmlinuz-${KVER}-* >/dev/null 2>&1; then
echo "[=] mainline ${KVER} already installed; needs reboot" echo "[=] mainline ${KVER} already installed on disk"
exit 0 SKIP_INSTALL=1
fi fi
if [ "$SKIP_INSTALL" -eq 0 ]; then
echo "[+] fetching kernel.ubuntu.com mainline v${KVER}" echo "[+] fetching kernel.ubuntu.com mainline v${KVER}"
URL="https://kernel.ubuntu.com/mainline/v${KVER}/amd64/" # Newer mainline kernels live under /v${KVER}/amd64/; older ones
# (≤ ~4.15) put debs at /v${KVER}/ directly. Try /amd64/ first;
# fall back to bare. linux-image-unsigned was renamed from
# linux-image- around 4.18 — old kernels use the plain name.
BASE="https://kernel.ubuntu.com/mainline/v${KVER}"
for URL in "${BASE}/amd64/" "${BASE}/"; do
INDEX=$(curl -sL "$URL")
if echo "$INDEX" | grep -q '\\.deb"'; then
break
fi
done
TMP=$(mktemp -d) TMP=$(mktemp -d)
cd "$TMP" cd "$TMP"
# Pick the 4 canonical generic-kernel .debs by pattern match against # Pick the 4 canonical generic-kernel .debs by pattern match against
# the directory index. Skip lowlatency variants. # the directory index. Skip lowlatency variants. Accept both
DEBS=$(curl -sL "$URL" | \\ # 'linux-image-unsigned-' (newer) and 'linux-image-' (older).
DEBS=$(echo "$INDEX" | \\
grep -oE 'href="[^"]+\\.deb"' | sed 's/href="//; s/"$//' | \\ grep -oE 'href="[^"]+\\.deb"' | sed 's/href="//; s/"$//' | \\
grep -E '(linux-image-unsigned|linux-modules|linux-headers)-[0-9.]+-[0-9]+-generic_|linux-headers-[0-9.]+-[0-9]+_[^_]+_all\\.deb' | \\ grep -E '(linux-image(-unsigned)?|linux-modules|linux-headers)-[0-9.]+-[0-9]+-generic_|linux-headers-[0-9.]+-[0-9]+_[^_]+_all\\.deb' | \\
grep -v lowlatency) grep -v lowlatency)
if [ -z "$DEBS" ]; then if [ -z "$DEBS" ]; then
echo "[-] no .debs found at $URL — does the version exist on kernel.ubuntu.com?" >&2 echo "[-] no .debs found at ${BASE}/ (tried /amd64/ and bare)" >&2
exit 2 exit 2
fi fi
for f in $DEBS; do for f in $DEBS; do
@@ -119,12 +150,46 @@ Vagrant.configure("2") do |c|
curl -fsSL -O "${URL}${f}" curl -fsSL -O "${URL}${f}"
done done
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
dpkg -i *.deb || apt-get install -f -y -qq # --force-depends so packages still install even when t64-transition
# libs (libssl3t64, libelf1t64) are missing on a pre-24.04 rootfs.
# The kernel image + modules don't actually need those at boot —
# the dependency is for signing/integrity checks at build time.
dpkg -i --force-depends *.deb || apt-get install -f -y -qq || true
fi # end SKIP_INSTALL guard
# Pin grub default to the just-installed mainline kernel. Without
# this, grub's debian-version-compare picks the highest-sorting
# vmlinuz-* as default; for downgrades (e.g. stock 4.15 → mainline
# 4.14.70), the OLD kernel wins because 4.15 > 4.14 numerically.
MAINLINE_VMLINUZ=$(ls /boot/vmlinuz-${KVER}-* 2>/dev/null | head -1)
if [ -n "$MAINLINE_VMLINUZ" ]; then
MAINLINE_KVER=$(basename "$MAINLINE_VMLINUZ" | sed 's/^vmlinuz-//')
# The "Advanced options" submenu entry id is stable across
# update-grub runs as "gnulinux-advanced-<UUID>>gnulinux-<kver>-advanced-<UUID>".
# Easier: use the human menuentry path.
GRUB_ENTRY="Advanced options for Ubuntu>Ubuntu, with Linux ${MAINLINE_KVER}"
sed -i "s|^GRUB_DEFAULT=.*|GRUB_DEFAULT=\\"${GRUB_ENTRY}\\"|" /etc/default/grub
echo "[+] GRUB_DEFAULT pinned to: ${GRUB_ENTRY}"
fi
update-grub 2>&1 | tail -3 update-grub 2>&1 | tail -3
echo "[i] mainline ${KVER} installed; reboot via 'vagrant reload'" echo "[i] mainline ${KVER} installed; reboot via 'vagrant reload'"
SHELL SHELL
end end
# 2c. Optional per-module provisioner. If
# tools/verify-vm/provisioners/<module>.sh exists, run it as root
# before build-and-verify. Used for things only meaningful per-module:
# build sudo 1.9.16 from source (sudo_chwoot), drop a polkit allow
# rule (udisks_libblockdev), add a sudoers grant (sudo_runas_neg1).
skk_mod = ENV["SKK_MODULE"] || ""
if !skk_mod.empty?
prov_path = File.join(__dir__, "provisioners", "#{skk_mod}.sh")
if File.exist?(prov_path)
m.vm.provision "shell", name: "module-provision-#{skk_mod}",
path: prov_path
end
end
# 3. Build SKELETONKEY in-VM and run --explain --active for the target # 3. Build SKELETONKEY in-VM and run --explain --active for the target
# module. Runs as the unprivileged 'vagrant' user (NOT root) — most # module. Runs as the unprivileged 'vagrant' user (NOT root) — most
# detect()s gate on "are you already root?" and short-circuit if so, # detect()s gate on "are you already root?" and short-circuit if so,
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# CVE-2025-32463 sudo --chroot NSS injection (Stratascale). Vulnerable
# range is sudo [1.9.14, 1.9.17p0]. Ubuntu 22.04 ships 1.9.9 which
# PREDATES the --chroot code path. Build sudo 1.9.16p1 from upstream
# and install to /usr/local (which precedes /usr/bin in Ubuntu's default
# PATH so plain `sudo` resolves to the vulnerable binary).
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq libpam0g-dev libssl-dev wget make gcc >/dev/null
cd /tmp
TARBALL=sudo-1.9.16p1.tar.gz
URL="https://www.sudo.ws/dist/${TARBALL}"
if [ -x /usr/local/bin/sudo ] && /usr/local/bin/sudo --version 2>&1 | head -1 | grep -q "1.9.16p1"; then
echo "[=] sudo 1.9.16p1 already at /usr/local/bin/sudo"
else
[ -f "${TARBALL}" ] || wget -q "${URL}"
rm -rf sudo-1.9.16p1
tar xzf "${TARBALL}"
cd sudo-1.9.16p1
# --sysconfdir=/etc so it honors the existing /etc/sudoers (vagrant's
# NOPASSWD grant). --disable-shared keeps the build self-contained.
./configure --prefix=/usr/local --sysconfdir=/etc \
--disable-shared --quiet >/dev/null 2>&1
make -j"$(nproc)" >/tmp/sudo-build.log 2>&1 || { tail -40 /tmp/sudo-build.log; exit 1; }
make install >/tmp/sudo-install.log 2>&1 || { tail -40 /tmp/sudo-install.log; exit 1; }
fi
# Verify what the unprivileged user's PATH resolves to.
echo "[+] which sudo (root): $(which sudo)"
echo "[+] /usr/local/bin/sudo version: $(/usr/local/bin/sudo --version | head -1)"
sudo -u vagrant bash -c 'echo "[+] vagrant PATH: $PATH"; echo "[+] vagrant sees: $(which sudo)"; sudo --version | head -1'
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# CVE-2019-14287 needs a (ALL,!root) grant for find_runas_blacklist_grant()
# to fire. Ubuntu 18.04 ships sudo 1.8.21p2 (in the vulnerable range) but
# Vagrant's default sudoers doesn't include the grant. Add it.
set -e
cat >/etc/sudoers.d/99-skk-runas-neg1 <<'EOF'
vagrant ALL=(ALL,!root) NOPASSWD: /bin/vi
EOF
chmod 0440 /etc/sudoers.d/99-skk-runas-neg1
echo "[+] sudoers grant installed:"
grep . /etc/sudoers.d/99-skk-runas-neg1
echo
echo "[+] sudo -ln -U vagrant tail:"
sudo -ln -U vagrant 2>&1 | tail -10 || true
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# CVE-2025-6019 udisks/libblockdev SUID-on-mount (Qualys). Debian 12's
# cloud image is server-oriented and doesn't ship udisks2. Install it,
# and drop a polkit rule allowing the vagrant user to invoke the
# affected action.ids — the real-world bug-path is "active console
# user invokes loop-setup", and we don't have a graphical session in
# Vagrant. The polkit rule simulates the trust polkit would give a
# logged-in workstation user.
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq udisks2 libblockdev-utils2 >/dev/null
mkdir -p /etc/polkit-1/rules.d
cat >/etc/polkit-1/rules.d/49-skk-verify.rules <<'EOF'
polkit.addRule(function(action, subject) {
if (subject.user == "vagrant" &&
(action.id == "org.freedesktop.UDisks2.loop-setup" ||
action.id == "org.freedesktop.UDisks2.filesystem-mount" ||
action.id == "org.freedesktop.UDisks2.filesystem-mount-other-seat" ||
action.id == "org.freedesktop.UDisks2.modify-device")) {
return polkit.Result.YES;
}
});
EOF
systemctl enable udisks2.service >/dev/null 2>&1 || true
systemctl restart udisks2.service
sleep 2
echo "[+] udisks2 status:"
systemctl is-active udisks2.service
echo "[+] udisks2 version: $(dpkg-query -W -f='${Version}' udisks2)"
echo "[+] libblockdev version: $(dpkg-query -W -f='${Version}' libblockdev-utils2)"
+32 -30
View File
@@ -35,7 +35,7 @@ af_packet:
box: ubuntu1804 box: ubuntu1804
kernel_pkg: "" # stock 4.15.0-213-generic — patch backported kernel_pkg: "" # stock 4.15.0-213-generic — patch backported
kernel_version: "4.15.0" kernel_version: "4.15.0"
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2017-7308; bug fixed mainline 4.10.6 + 4.9.18 backports. Ubuntu 18.04 stock kernel (4.15.0) is post-fix — detect() correctly returns OK. To validate the VULNERABLE path empirically would need a hand-built 4.4 or earlier kernel; deferred." notes: "CVE-2017-7308; bug fixed mainline 4.10.6 + 4.9.18 backports. Ubuntu 18.04 stock kernel (4.15.0) is post-fix — detect() correctly returns OK. To validate the VULNERABLE path empirically would need a hand-built 4.4 or earlier kernel; deferred."
af_packet2: af_packet2:
@@ -71,7 +71,7 @@ dirty_cow:
box: ubuntu1804 box: ubuntu1804
kernel_pkg: "" # 4.15.0 has the COW race fix; need older kernel kernel_pkg: "" # 4.15.0 has the COW race fix; need older kernel
kernel_version: "4.4.0" kernel_version: "4.4.0"
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2016-5195; ALL 4.4+ kernels have the fix backported. Ubuntu 18.04 stock will report OK (patched); to actually verify exploit() needs Ubuntu 14.04 / kernel ≤ 4.4.0-46. Use a custom box for that." notes: "CVE-2016-5195; ALL 4.4+ kernels have the fix backported. Ubuntu 18.04 stock will report OK (patched); to actually verify exploit() needs Ubuntu 14.04 / kernel ≤ 4.4.0-46. Use a custom box for that."
manual_for_exploit_verify: true manual_for_exploit_verify: true
@@ -79,16 +79,16 @@ dirty_pipe:
box: ubuntu2204 box: ubuntu2204
kernel_pkg: "" # 22.04 stock 5.15.0-91-generic kernel_pkg: "" # 22.04 stock 5.15.0-91-generic
kernel_version: "5.15.0" kernel_version: "5.15.0"
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2022-0847; introduced 5.8, fixed 5.16.11 / 5.15.25. Ubuntu 22.04 ships 5.15.0-91-generic, where uname reports '5.15.0' (below the 5.15.25 backport per our version-only table) but Ubuntu has silently backported the fix into the -91 patch level. Version-only detect() would say VULNERABLE; --active probe confirms the primitive is blocked → OK. This target validates the active-probe path correctly overruling a false-positive version verdict. (Originally pointed at Ubuntu 20.04 + pinned 5.13.0-19, but that HWE kernel is no longer in 20.04's apt archive.)" notes: "CVE-2022-0847; introduced 5.8, fixed 5.16.11 / 5.15.25. Ubuntu 22.04 ships 5.15.0-91-generic, where uname reports '5.15.0' (below the 5.15.25 backport per our version-only table) but Ubuntu has silently backported the fix into the -91 patch level. Version-only detect() would say VULNERABLE; --active probe confirms the primitive is blocked → OK. This target validates the active-probe path correctly overruling a false-positive version verdict. (Originally pointed at Ubuntu 20.04 + pinned 5.13.0-19, but that HWE kernel is no longer in 20.04's apt archive.)"
dirtydecrypt: dirtydecrypt:
box: debian12 box: ubuntu2204
kernel_pkg: "" # only Linux 7.0+ has the bug — needs custom kernel kernel_pkg: ""
kernel_version: "7.0.0" mainline_version: "6.19.7" # below the 6.19.13 backport → genuinely vulnerable
expect_detect: OK kernel_version: "6.19.7"
notes: "CVE-2026-31635; bug introduced in 7.0 rxgk path. NO mainline 7.0 distro shipping yet — Debian 12 will report OK (predates the bug). Verifying exploit() needs a hand-built 7.0-rc kernel." expect_detect: VULNERABLE
manual_for_exploit_verify: true notes: "CVE-2026-31635; rxgk RESPONSE oversized auth_len. Per NVD: bug entered at 6.16.1, vulnerable through 6.18.22 / 6.19.12 / 7.0-rc7; fixed at 6.18.23 / 6.19.13 / 7.0 stable. Mainline 6.19.7 is below the .13 backport → genuinely VULNERABLE. (Earlier module code wrongly gated 'predates' on 7.0; fixed in this commit by gating on 6.16.1 + adding 6.18.23 to the backport table.)"
entrybleed: entrybleed:
box: ubuntu2204 box: ubuntu2204
@@ -98,12 +98,12 @@ entrybleed:
notes: "CVE-2023-0458; side-channel applies to any KPTI-on Intel x86_64 host. Stock Ubuntu 22.04 will report VULNERABLE if meltdown sysfs shows 'Mitigation: PTI'." notes: "CVE-2023-0458; side-channel applies to any KPTI-on Intel x86_64 host. Stock Ubuntu 22.04 will report VULNERABLE if meltdown sysfs shows 'Mitigation: PTI'."
fragnesia: fragnesia:
box: debian12 box: ""
kernel_pkg: "" kernel_pkg: ""
kernel_version: "7.0.0" kernel_version: ""
expect_detect: OK expect_detect: ""
notes: "CVE-2026-46300; XFRM ESP-in-TCP bug. Needs 7.0-rc; Debian 12 reports OK." manual: true
manual_for_exploit_verify: true notes: "CVE-2026-46300; XFRM ESP-in-TCP bug. Fix lands at 7.0.9. Verifying VULNERABLE needs a pre-fix 7.0.x kernel. Mainline 7.0.5 was tried via Ubuntu 22.04 + kernel.ubuntu.com — fails because the 7.0.5 kernel .debs depend on the t64-transition libs (libssl3t64, libelf1t64) which only exist on Ubuntu 24.04+ / Debian 13+. No Vagrant box with Parallels provider has those libs yet. dpkg --force-depends leaves the kernel image in iHR (broken) state with no /boot/vmlinuz deposited. Resolution: wait for a Parallels-supported ubuntu2404 / debian13 box, or build one locally."
fuse_legacy: fuse_legacy:
box: debian11 box: debian11
@@ -224,42 +224,43 @@ vmwgfx:
# ── v0.8.0 additions ────────────────────────────────────────────── # ── v0.8.0 additions ──────────────────────────────────────────────
sudo_chwoot: sudo_chwoot:
box: ubuntu2204 # 22.04 ships sudo 1.9.9 (pre-feature) — need a 1.9.14+ install box: ubuntu2204 # 22.04 ships sudo 1.9.9 — provisioner builds 1.9.16p1 over it
kernel_pkg: "" # this bug is sudo-version-gated, not kernel kernel_pkg: "" # this bug is sudo-version-gated, not kernel
kernel_version: "5.15.0" kernel_version: "5.15.0"
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2025-32463; sudo --chroot NSS shim. Vulnerable range is sudo [1.9.14, 1.9.17p0]. Ubuntu 22.04 ships sudo 1.9.9 which PREDATES the vulnerable --chroot code path — so detect correctly returns OK. To validate VULNERABLE empirically, provision a vulnerable sudo build into the VM (e.g. apt install -t backports sudo=1.9.16-1 or build from source). Deferred." notes: "CVE-2025-32463; sudo --chroot NSS shim. Vulnerable range is sudo [1.9.14, 1.9.17p0]. provisioners/sudo_chwoot.sh builds sudo 1.9.16p1 from upstream sources into /usr/local/bin (which precedes /usr/bin in PATH so plain `sudo` resolves to the vulnerable binary)."
udisks_libblockdev: udisks_libblockdev:
box: debian12 # 12 ships udisks2 2.10.x + libblockdev 3.0.x — vulnerable box: debian12 # 12 ships udisks2 2.10.x + libblockdev 3.0.x — vulnerable
kernel_pkg: "" kernel_pkg: ""
kernel_version: "6.1.0" kernel_version: "6.1.0"
expect_detect: PRECOND_FAIL expect_detect: VULNERABLE
notes: "CVE-2025-6019; udisks/libblockdev SUID-on-mount. Debian 12's cloud image is server-oriented — udisksd is NOT installed by default. detect correctly returns PRECOND_FAIL ('udisksd not installed; bug unreachable here'). To validate VULNERABLE empirically, install udisks2 + log in as an active-session user (Vagrant SSH session is NOT active per polkit — needs a real console session). Both gates are real and the detect honestly surfaces them; deferred." notes: "CVE-2025-6019; udisks/libblockdev SUID-on-mount. provisioners/udisks_libblockdev.sh installs udisks2 + libblockdev-utils3 and drops a polkit rule allowing the vagrant user to invoke loop-setup/filesystem-mount — simulating the trust polkit would give a logged-in workstation user (the real-world bug-path). Without that rule, the SSH session is not 'active' per polkit and the D-Bus call short-circuits."
pintheft: pintheft:
box: "" # RDS is blacklisted on every common Vagrant box's stock kernel box: "" # RDS is blacklisted on every common Vagrant box's stock kernel
kernel_pkg: "" kernel_pkg: ""
kernel_version: "" kernel_version: ""
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2026-43494; PinTheft. Among Vagrant-supported distros, NONE autoload the rds kernel module (Arch Linux is the only common distro that does, and there's no maintained generic/arch-linux Vagrant box). On Debian/Ubuntu/Fedora boxes the AF_RDS socket() call fails with EAFNOSUPPORT → detect correctly returns OK ('bug exists in kernel but unreachable from userland here'). Verifying the VULNERABLE path needs either an Arch box, or a custom box with the rds module pre-loaded ('modprobe rds && modprobe rds_tcp'). Deferred." notes: "CVE-2026-43494; PinTheft. Among Vagrant-supported distros, NONE autoload the rds kernel module (Arch Linux is the only common distro that does, and there's no maintained generic/arch-linux Vagrant box). On Debian/Ubuntu/Fedora boxes the AF_RDS socket() call fails with EAFNOSUPPORT → detect correctly returns OK ('bug exists in kernel but unreachable from userland here'). Verifying the VULNERABLE path needs either an Arch box, or a custom box with the rds module pre-loaded ('modprobe rds && modprobe rds_tcp'). Deferred."
manual: true manual: true
# ── v0.9.0 additions (gap fillers 2018 / 2019 / 2020 / 2024) ────── # ── v0.9.0 additions (gap fillers 2018 / 2019 / 2020 / 2024) ──────
mutagen_astronomy: mutagen_astronomy:
box: ubuntu1804 # 4.15.0-213 stock — already > 4.14.71 backport → OK box: ""
kernel_pkg: "" kernel_pkg: ""
kernel_version: "4.15.0" kernel_version: ""
expect_detect: OK expect_detect: ""
notes: "CVE-2018-14634; Qualys Mutagen Astronomy. Ubuntu 18.04 ships 4.15.0-213 which is post-fix. detect correctly returns OK. Verifying the VULNERABLE path empirically needs a 2.6.x / 3.10.x EOL kernel (e.g. RHEL 6 / CentOS 6 / Debian 7); deferred to a custom-box workflow." manual: true
notes: "CVE-2018-14634; Qualys Mutagen Astronomy. No good Vagrant verification environment: stock Ubuntu 18.04 (4.15.0-213) returns detect()=VULNERABLE because the module's kernel_range table has no entry for the 4.15.x series (Ubuntu's HWE backports are not modeled), but the kernel IS actually patched — false-positive of the conservative module logic. Mainline 4.14.70 (target VULNERABLE kernel) panics on Ubuntu 18.04's rootfs with 'Failed to execute /init (error -8)' — kernel config mismatch (binfmt_elf as module rather than baked-in). Genuinely vulnerable verification needs a contemporary CentOS 6 / Debian 7 image with original-vintage kernel; deferred to custom-box workflow."
sudo_runas_neg1: sudo_runas_neg1:
box: ubuntu1804 # ships sudo 1.8.21p2 (vulnerable; pre-1.8.28 fix) box: ubuntu1804 # ships sudo 1.8.21p2 (vulnerable; pre-1.8.28 fix)
kernel_pkg: "" kernel_pkg: ""
kernel_version: "4.15.0" kernel_version: "4.15.0"
expect_detect: PRECOND_FAIL expect_detect: VULNERABLE
notes: "CVE-2019-14287; sudo Runas -u#-1. Ubuntu 18.04 ships sudo 1.8.21p2 which IS in the vulnerable range — but the default vagrant user has no (ALL,!root) sudoers grant for find_runas_blacklist_grant() to abuse, so detect correctly returns PRECOND_FAIL. To validate VULNERABLE empirically, provision a sudoers entry of the form 'vagrant ALL=(ALL,!root) /bin/vi' before verifying." notes: "CVE-2019-14287; sudo Runas -u#-1. Ubuntu 18.04 ships sudo 1.8.21p2 (vulnerable). provisioners/sudo_runas_neg1.sh adds 'vagrant ALL=(ALL,!root) NOPASSWD: /bin/vi' to /etc/sudoers.d/ so find_runas_blacklist_grant() has a grant to abuse."
tioscpgrp: tioscpgrp:
box: ubuntu2004 # 5.4 stock kernels (5.4.0-26) are below the 5.4.85 backport box: ubuntu2004 # 5.4 stock kernels (5.4.0-26) are below the 5.4.85 backport
@@ -272,13 +273,14 @@ vsock_uaf:
box: "" # vsock module typically not loaded on CI containers (no virtualization) box: "" # vsock module typically not loaded on CI containers (no virtualization)
kernel_pkg: "" kernel_pkg: ""
kernel_version: "" kernel_version: ""
expect_detect: OK expect_detect: VULNERABLE
notes: "CVE-2024-50264; Pwn2Own 2024 vsock UAF. AF_VSOCK requires the vsock kernel module, which autoloads only on KVM/QEMU GUESTS. Vagrant VMs running under Parallels are themselves guests, but their guest kernel may or may not have vsock loaded depending on the Parallels host. detect correctly returns OK when AF_VSOCK is unavailable. To validate VULNERABLE, ensure the VM kernel has CONFIG_VSOCKETS + virtio-vsock loaded ('modprobe vsock_loopback' may suffice on newer kernels)." notes: "CVE-2024-50264; Pwn2Own 2024 vsock UAF. AF_VSOCK requires the vsock kernel module, which autoloads only on KVM/QEMU GUESTS. Vagrant VMs running under Parallels are themselves guests, but their guest kernel may or may not have vsock loaded depending on the Parallels host. detect correctly returns OK when AF_VSOCK is unavailable. To validate VULNERABLE, ensure the VM kernel has CONFIG_VSOCKETS + virtio-vsock loaded ('modprobe vsock_loopback' may suffice on newer kernels)."
manual: true manual: true
nft_pipapo: nft_pipapo:
box: ubuntu2204 # 5.15 stock + HWE — same pipapo set substrate as nf_tables box: ubuntu2204 # 5.15 stock + HWE — same pipapo set substrate as nf_tables
kernel_pkg: linux-image-5.15.0-43-generic kernel_pkg: ""
kernel_version: "5.15.0-43" mainline_version: "5.15.5"
kernel_version: "5.15.5"
expect_detect: VULNERABLE expect_detect: VULNERABLE
notes: "CVE-2024-26581; nft_pipapo destroy-race (Notselwyn II). Same Vagrant target as nf_tables works here — stock 5.15.0-43 is below the 5.15.149 backport. Userns gate must be open (sysctl kernel.unprivileged_userns_clone=1)." 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)."
+39 -14
View File
@@ -139,19 +139,6 @@ if ! vagrant status "$VM_HOSTNAME" 2>&1 | grep -q "running"; then
vagrant up "$VM_HOSTNAME" --provider=parallels vagrant up "$VM_HOSTNAME" --provider=parallels
fi fi
# Reboot if any kernel pin was applied (uname -r != target).
if [[ -n "$KERNEL_PKG" || -n "$MAINLINE" ]]; then
current_kver=$(vagrant ssh "$VM_HOSTNAME" -c "uname -r" 2>/dev/null | tr -d '\r')
target_match="$KERNEL_VER"
[[ -n "$MAINLINE" ]] && target_match="$MAINLINE"
if [[ "$current_kver" != *"$target_match"* ]]; then
echo "[*] current kernel $current_kver != target $target_match; rebooting..."
vagrant reload "$VM_HOSTNAME"
sleep 5
fi
fi
# Run the explain probe.
LOG="$LOG_DIR/verify-${MODULE}-$(date +%Y%m%d-%H%M%S).log" LOG="$LOG_DIR/verify-${MODULE}-$(date +%Y%m%d-%H%M%S).log"
# Force rsync the source tree in. vagrant up runs rsync automatically on # Force rsync the source tree in. vagrant up runs rsync automatically on
@@ -160,8 +147,46 @@ LOG="$LOG_DIR/verify-${MODULE}-$(date +%Y%m%d-%H%M%S).log"
echo "[*] syncing source into VM..." echo "[*] syncing source into VM..."
vagrant rsync "$VM_HOSTNAME" 2>&1 | tail -5 vagrant rsync "$VM_HOSTNAME" 2>&1 | tail -5
# Two-phase provisioning so the new kernel actually boots before verify:
# PREP: install kernel (apt or mainline) + pin grub default + run any
# module-specific provisioner (sudoers grant, sudo build, ...).
# ── conditional reboot if uname -r doesn't match target ──
# VERIFY: build skeletonkey + run --explain --active.
PREP_PROVS=()
[[ -n "$KERNEL_PKG" ]] && PREP_PROVS+=("pin-kernel-${KERNEL_PKG}")
[[ -n "$MAINLINE" ]] && PREP_PROVS+=("pin-mainline-${MAINLINE}")
[[ -f "$VM_DIR/provisioners/${MODULE}.sh" ]] && PREP_PROVS+=("module-provision-${MODULE}")
if [[ ${#PREP_PROVS[@]} -gt 0 ]]; then
echo "[*] running prep provisioners: ${PREP_PROVS[*]}"
vagrant provision "$VM_HOSTNAME" \
--provision-with "$(IFS=,; echo "${PREP_PROVS[*]}")" 2>&1 | tee "$LOG"
fi
# Reboot if a kernel pin moved us off the target. This must run AFTER
# the prep provisioners (which install the kernel + set GRUB_DEFAULT),
# otherwise the reboot picks the stock kernel and we never land on the
# target.
if [[ -n "$KERNEL_PKG" || -n "$MAINLINE" ]]; then
current_kver=$(vagrant ssh "$VM_HOSTNAME" -c "uname -r" 2>/dev/null | tr -d '\r')
target_match="$KERNEL_VER"
[[ -n "$MAINLINE" ]] && target_match="$MAINLINE"
if [[ "$current_kver" != *"$target_match"* ]]; then
echo "[*] current kernel $current_kver != target $target_match; rebooting..."
vagrant reload "$VM_HOSTNAME" 2>&1 | tee -a "$LOG"
sleep 5
post_kver=$(vagrant ssh "$VM_HOSTNAME" -c "uname -r" 2>/dev/null | tr -d '\r')
echo "[*] post-reboot kernel: $post_kver" | tee -a "$LOG"
if [[ "$post_kver" != *"$target_match"* ]]; then
echo "[!] reboot did NOT land on target kernel $target_match (got $post_kver)" | tee -a "$LOG"
echo " detect() will still run, but verification is on the wrong kernel" | tee -a "$LOG"
fi
fi
fi
echo "[*] running verifier..." echo "[*] running verifier..."
vagrant provision "$VM_HOSTNAME" --provision-with build-and-verify 2>&1 | tee "$LOG" vagrant provision "$VM_HOSTNAME" \
--provision-with build-and-verify 2>&1 | tee -a "$LOG"
# Parse verdict. Vagrant prefixes provisioner output with the VM name # Parse verdict. Vagrant prefixes provisioner output with the VM name
# (e.g. " skk-pwnkit: VERDICT: VULNERABLE"), so anchor on the VERDICT # (e.g. " skk-pwnkit: VERDICT: VULNERABLE"), so anchor on the VERDICT