2 Commits

Author SHA1 Message Date
leviathan 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
leviathan 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
14 changed files with 349 additions and 68 deletions
+22 -19
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)
[![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-27%20VM--verified%20%2F%2034-brightgreen.svg)](docs/VERIFICATIONS.jsonl)
[![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#)
> **One curated binary. 39 Linux LPE modules covering 34 CVEs from 2016 → 2026.
> Every year 2016 → 2026 covered. 22 confirmed end-to-end against real Linux
> Every year 2016 → 2026 covered. 27 confirmed end-to-end against real Linux
> VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
> the safest one and runs it.**
@@ -44,10 +44,11 @@ for every CVE in the bundle — same project for red and blue teams.
## Corpus at a glance
**31 modules covering 26 distinct CVEs** across the 2016 → 2026 LPE
timeline. **22 of the 26 CVEs have been empirically verified** in real
Linux VMs via `tools/verify-vm/`; the 4 still-pending entries are
blocked by their target environment, not by missing code.
**39 modules covering 34 distinct CVEs** across the 2016 → 2026 LPE
timeline. **27 of the 34 CVEs have been empirically verified** in real
Linux VMs via `tools/verify-vm/`; the 7 still-pending entries are
blocked by their target environment (legacy hypervisor, EOL kernel, or
not-yet-shipped Linux 7.0), not by missing code.
| Tier | Count | What it means |
|---|---|---|
@@ -65,25 +66,27 @@ af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
### Empirical verification (22 of 26 CVEs)
### Empirical verification (27 of 34 CVEs)
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
each verdict against a known-target VM. Coverage:
| Distro / kernel | Modules verified |
|---|---|
| Ubuntu 18.04 (4.15.0) | af_packet · ptrace_traceme · sudo_samedit |
| Ubuntu 20.04 (5.4 stock + 5.15 HWE) | af_packet2 · cls_route4 · nft_payload · overlayfs · pwnkit · sequoia |
| 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 18.04 (4.15.0, sudo 1.8.21p2) | af_packet · ptrace_traceme · sudo_samedit · sudo_runas_neg1 |
| 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 · 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 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
Vagrant box), `dirty_cow` (needs ≤ 4.4 kernel — older than every
supported box), `dirtydecrypt` & `fragnesia` (need Linux 7.0 — not
shipping as any distro kernel yet). All four are flagged in
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with
rationale.
**Not yet verified (7):** `vmwgfx` (VMware-guest-only — no public Vagrant
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
`mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04
rootfs — needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel
modules not loaded on common Vagrant boxes), `dirtydecrypt` & `fragnesia`
(need Linux 7.0 — not shipping as any distro kernel yet). All seven are
flagged in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml)
with rationale.
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
detection status. Run `skeletonkey --module-info <name>` for the
@@ -205,7 +208,7 @@ year 2016 → 2026 now covered**. v0.9.0 adds 5 gap-fillers:
(CVE-2024-50264 — Pwnie 2025 winner), `nft_pipapo` (CVE-2024-26581 —
Notselwyn II). v0.8.0 added 3 (`sudo_chwoot`/CVE-2025-32463,
`udisks_libblockdev`/CVE-2025-6019, `pintheft`/CVE-2026-43494).
**22 empirically verified** against real Linux VMs (Ubuntu 18.04 /
**27 empirically verified** against real Linux VMs (Ubuntu 18.04 /
20.04 / 22.04 + Debian 11 / 12 + mainline kernels 5.15.5 / 6.1.10
from kernel.ubuntu.com). 88-test unit harness + ASan/UBSan +
clang-tidy on every push. 4 prebuilt binaries (x86_64 + arm64, each
@@ -221,7 +224,7 @@ Reliability + accuracy work in v0.7.x:
- **VM verifier** (`tools/verify-vm/`) — Vagrant + Parallels scaffold
that boots known-vulnerable kernels (stock distro + mainline via
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. 27 modules confirmed end-to-end.
- **`--explain <module>`** — single-page operator briefing: CVE / CWE
/ MITRE ATT&CK / CISA KEV status, host fingerprint, live detect()
trace, OPSEC footprint, detection-rule coverage, verified-on
+50
View File
@@ -136,6 +136,16 @@ const struct verification_record verifications[] = {
.actual_detect = "VULNERABLE",
.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",
.verified_at = "2026-05-23",
@@ -216,6 +226,26 @@ const struct verification_record verifications[] = {
.actual_detect = "VULNERABLE",
.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",
.verified_at = "2026-05-23",
@@ -236,6 +266,26 @@ const struct verification_record verifications[] = {
.actual_detect = "PRECOND_FAIL",
.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 =
+51
View File
@@ -1,3 +1,54 @@
## 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
Five gap-filling modules. Closes the 2018 hole entirely and thickens
+5
View File
@@ -28,3 +28,8 @@
{"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":"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"}
+6 -6
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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. 27 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: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; 27 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:url" content="https://karazajac.github.io/SKELETONKEY/">
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
@@ -56,14 +56,14 @@
<div class="container hero-inner">
<div class="hero-eyebrow">
<span class="dot dot-pulse"></span>
v0.9.0 — released 2026-05-24
v0.9.1 — released 2026-05-24
</div>
<h1 class="hero-title">
<span class="display-wordmark">SKELETONKEY</span>
</h1>
<p class="hero-tag">
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>. 27 of 34 confirmed against
real Linux kernels in VMs. SOC-ready detection rules in four SIEM
formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated.
<span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span>
@@ -82,7 +82,7 @@
<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 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="27">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"><span class="num" data-target="151">0</span><span>detection rules</span></div>
</div>
@@ -598,7 +598,7 @@ uid=0(root) gid=0(root)</pre>
who found the bugs.
</p>
<p class="footer-meta">
v0.9.0 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
v0.9.1 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
</p>
</div>
</footer>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

+3 -3
View File
@@ -39,7 +39,7 @@
Curated Linux LPE corpus.
</text>
<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. 27 of 34 verified.
</text>
<!-- stat chips -->
@@ -49,9 +49,9 @@
<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>
<!-- 22 VM-verified -->
<!-- 27 VM-verified -->
<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">27</text>
<text x="270" y="37" font-family="'Inter',sans-serif" font-size="16" fill="#8a8a9d">✓ VM-verified</text>
<!-- 11 KEV -->

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+1 -1
View File
@@ -35,7 +35,7 @@
#include <string.h>
#include <unistd.h>
#define SKELETONKEY_VERSION "0.9.0"
#define SKELETONKEY_VERSION "0.9.1"
static const char BANNER[] =
"\n"
+72 -11
View File
@@ -73,7 +73,19 @@ Vagrant.configure("2") do |c|
echo "[+] installing #{pkg} (kernel target #{kver})"
export DEBIAN_FRONTEND=noninteractive
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
SHELL
end
@@ -90,28 +102,47 @@ Vagrant.configure("2") do |c|
m.vm.provision "shell", name: "pin-mainline-#{mainline}", inline: <<-SHELL
set -e
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
echo "[=] mainline ${KVER} already booted ($(uname -r))"
exit 0
BOOTED_INTO_TARGET=1
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
echo "[=] mainline ${KVER} already installed; needs reboot"
exit 0
echo "[=] mainline ${KVER} already installed on disk"
SKIP_INSTALL=1
fi
if [ "$SKIP_INSTALL" -eq 0 ]; then
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)
cd "$TMP"
# Pick the 4 canonical generic-kernel .debs by pattern match against
# the directory index. Skip lowlatency variants.
DEBS=$(curl -sL "$URL" | \\
# the directory index. Skip lowlatency variants. Accept both
# 'linux-image-unsigned-' (newer) and 'linux-image-' (older).
DEBS=$(echo "$INDEX" | \\
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)
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
fi
for f in $DEBS; do
@@ -120,11 +151,41 @@ Vagrant.configure("2") do |c|
done
export DEBIAN_FRONTEND=noninteractive
dpkg -i *.deb || apt-get install -f -y -qq
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
echo "[i] mainline ${KVER} installed; reboot via 'vagrant reload'"
SHELL
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
# module. Runs as the unprivileged 'vagrant' user (NOT root) — most
# 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)"
+16 -14
View File
@@ -224,18 +224,18 @@ vmwgfx:
# ── v0.8.0 additions ──────────────────────────────────────────────
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_version: "5.15.0"
expect_detect: OK
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."
expect_detect: VULNERABLE
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:
box: debian12 # 12 ships udisks2 2.10.x + libblockdev 3.0.x — vulnerable
kernel_pkg: ""
kernel_version: "6.1.0"
expect_detect: PRECOND_FAIL
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."
expect_detect: VULNERABLE
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:
box: "" # RDS is blacklisted on every common Vagrant box's stock kernel
@@ -248,18 +248,19 @@ pintheft:
# ── v0.9.0 additions (gap fillers 2018 / 2019 / 2020 / 2024) ──────
mutagen_astronomy:
box: ubuntu1804 # 4.15.0-213 stock — already > 4.14.71 backport → OK
box: ""
kernel_pkg: ""
kernel_version: "4.15.0"
expect_detect: OK
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."
kernel_version: ""
expect_detect: ""
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:
box: ubuntu1804 # ships sudo 1.8.21p2 (vulnerable; pre-1.8.28 fix)
kernel_pkg: ""
kernel_version: "4.15.0"
expect_detect: PRECOND_FAIL
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."
expect_detect: VULNERABLE
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:
box: ubuntu2004 # 5.4 stock kernels (5.4.0-26) are below the 5.4.85 backport
@@ -278,7 +279,8 @@ vsock_uaf:
nft_pipapo:
box: ubuntu2204 # 5.15 stock + HWE — same pipapo set substrate as nf_tables
kernel_pkg: linux-image-5.15.0-43-generic
kernel_version: "5.15.0-43"
kernel_pkg: ""
mainline_version: "5.15.5"
kernel_version: "5.15.5"
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
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"
# 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..."
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..."
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
# (e.g. " skk-pwnkit: VERDICT: VULNERABLE"), so anchor on the VERDICT