Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25c2afc3e9 | |||
| 13fbbce618 | |||
| bb5ca48fe1 | |||
| 4454d8148e | |||
| fa0228df9b | |||
| d52fcd5512 |
@@ -10,6 +10,10 @@ on:
|
||||
# Runs Monday 06:00 UTC; reports any new backports / KEV additions
|
||||
# that haven't propagated into the corpus yet.
|
||||
- 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:
|
||||
build:
|
||||
|
||||
@@ -23,16 +23,17 @@ Status legend:
|
||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||
historical reference only
|
||||
|
||||
**Counts:** 31 modules total — 28 verified (🟢 14 · 🟡 14) plus 3
|
||||
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot` —
|
||||
see note below). 🔵 0 · ⚪ 0 planned-with-stub · 🔴 0. (One ⚪ row
|
||||
below — CVE-2026-31402 — is a *candidate* with no module, not counted
|
||||
as a module.)
|
||||
**Counts:** 39 modules total covering 34 CVEs; **28 of 34 CVEs
|
||||
verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0
|
||||
planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
|
||||
*candidate* with no module, not counted as a module.)
|
||||
|
||||
> **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three
|
||||
> are ported from public PoCs. The **exploit bodies** are not yet
|
||||
> VM-verified end-to-end, so they're listed 🟡 but excluded from the
|
||||
> 28-module verified corpus.
|
||||
> **Note on unverified rows:** `vmwgfx` / `dirty_cow` /
|
||||
> `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` are
|
||||
> blocked by their target environment (VMware-only, kernel < 4.4,
|
||||
> mainline panic, kmod not autoloaded, or t64-transition libs),
|
||||
> not by missing code. See
|
||||
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
||||
>
|
||||
> All three now have **pinned fix commits and version-based
|
||||
> `detect()`**:
|
||||
|
||||
@@ -133,7 +133,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||
$ skeletonkey --auto --i-know
|
||||
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
||||
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
||||
[*] auto: scanning 31 modules for vulnerabilities...
|
||||
[*] auto: scanning 39 modules for vulnerabilities...
|
||||
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
||||
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
||||
@@ -202,18 +202,19 @@ also compile (modules with Linux-only headers stub out gracefully).
|
||||
|
||||
## Status
|
||||
|
||||
**v0.9.0 cut 2026-05-24.** 39 modules across 34 CVEs — **every
|
||||
year 2016 → 2026 now covered**. v0.9.0 adds 5 gap-fillers:
|
||||
`mutagen_astronomy` (CVE-2018-14634 — closes 2018), `sudo_runas_neg1`
|
||||
(CVE-2019-14287), `tioscpgrp` (CVE-2020-29661), `vsock_uaf`
|
||||
(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).
|
||||
**v0.9.6 cut 2026-05-28.** 39 modules across 34 CVEs — **every
|
||||
year 2016 → 2026 now covered**. v0.9.0 added 5 gap-fillers
|
||||
(`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` /
|
||||
`nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` /
|
||||
`pintheft`). v0.9.1 and v0.9.2 are verification-only sweeps that took
|
||||
the verified count from 22 → 28 by booting real vulnerable kernels
|
||||
(Ubuntu mainline 5.4.0-26, 5.15.5, 6.19.7 + provisioner-built sudo
|
||||
1.9.16p1 + Debian 12 + polkit allow rule for udisks).
|
||||
**28 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
|
||||
in dynamic + static-musl flavors).
|
||||
20.04 / 22.04 + Debian 11 / 12 + mainline kernels from
|
||||
kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
|
||||
every push. 4 prebuilt binaries (x86_64 + arm64, each in dynamic +
|
||||
static-musl flavors).
|
||||
|
||||
Reliability + accuracy work in v0.7.x:
|
||||
- Shared **host fingerprint** (`core/host.{h,c}`) populated once at
|
||||
@@ -231,15 +232,19 @@ Reliability + accuracy work in v0.7.x:
|
||||
trace, OPSEC footprint, detection-rule coverage, verified-on
|
||||
records. Paste-into-ticket ready.
|
||||
- **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches
|
||||
CISA KEV catalog + NVD CWE; 10 of 26 modules cover KEV-listed CVEs.
|
||||
- **119 detection rules** across auditd / sigma / yara / falco; one
|
||||
CISA KEV catalog + NVD CWE; 12 of 34 modules cover KEV-listed CVEs.
|
||||
- **151 detection rules** across auditd / sigma / yara / falco; one
|
||||
command exports the corpus to your SIEM.
|
||||
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
|
||||
exploit, structured verdict table, scan summary, `--dry-run`.
|
||||
|
||||
Not yet verified (4 of 26 CVEs): `vmwgfx` (VMware-guest only),
|
||||
`dirty_cow` (needs ≤ 4.4 kernel), `dirtydecrypt` + `fragnesia` (need
|
||||
Linux 7.0 — not shipping yet). Rationale in
|
||||
Not yet verified (6 of 34 CVEs): `vmwgfx` (VMware-guest only),
|
||||
`dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
|
||||
4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7),
|
||||
`pintheft` + `vsock_uaf` (kernel modules not autoloaded on common
|
||||
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).
|
||||
|
||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
|
||||
|
||||
+65
-1
@@ -28,6 +28,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = false,
|
||||
.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",
|
||||
.cwe = NULL,
|
||||
@@ -36,6 +44,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = true,
|
||||
.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",
|
||||
.cwe = "CWE-250",
|
||||
@@ -44,6 +60,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = false,
|
||||
.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",
|
||||
.cwe = "CWE-787",
|
||||
@@ -196,6 +220,38 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = true,
|
||||
.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",
|
||||
.cwe = "CWE-130",
|
||||
@@ -213,13 +269,21 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.kev_date_added = "",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2026-46300",
|
||||
.cve = "CVE-2026-43494",
|
||||
.cwe = NULL,
|
||||
.attack_technique = "T1068",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
.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 =
|
||||
|
||||
@@ -82,8 +82,8 @@ const struct verification_record verifications[] = {
|
||||
.host_kernel = "6.19.7-061907-generic",
|
||||
.host_distro = "Ubuntu 22.04.3 LTS",
|
||||
.vm_box = "generic/ubuntu2204",
|
||||
.expect_detect = "OK",
|
||||
.actual_detect = "OK",
|
||||
.expect_detect = "VULNERABLE",
|
||||
.actual_detect = "VULNERABLE",
|
||||
.status = "match",
|
||||
},
|
||||
{
|
||||
|
||||
+73
-1
@@ -17,6 +17,15 @@
|
||||
"in_kev": false,
|
||||
"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",
|
||||
"module_dir": "ptrace_traceme_cve_2019_13272",
|
||||
@@ -26,6 +35,15 @@
|
||||
"in_kev": true,
|
||||
"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",
|
||||
"module_dir": "af_packet2_cve_2020_14386",
|
||||
@@ -35,6 +53,15 @@
|
||||
"in_kev": false,
|
||||
"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",
|
||||
"module_dir": "netfilter_xtcompat_cve_2021_22555",
|
||||
@@ -206,6 +233,42 @@
|
||||
"in_kev": true,
|
||||
"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",
|
||||
"module_dir": "dirtydecrypt_cve_2026_31635",
|
||||
@@ -224,10 +287,19 @@
|
||||
"in_kev": false,
|
||||
"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",
|
||||
"module_dir": "fragnesia_cve_2026_46300",
|
||||
"cwe": null,
|
||||
"cwe": "CWE-787",
|
||||
"attack_technique": "T1068",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
|
||||
+10
-2
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
|
||||
in the wild per the Known Exploited Vulnerabilities catalog.
|
||||
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)
|
||||
|
||||
@@ -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-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-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-2018-14634 | 2026-01-26 | CWE-190 | `mutagen_astronomy_cve_2018_14634` |
|
||||
|
||||
## Not in KEV
|
||||
|
||||
@@ -30,7 +32,9 @@ and are technically reachable. "Not in KEV" is not the same as
|
||||
| CVE | CWE | Module |
|
||||
| --- | --- | --- |
|
||||
| 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-29661 | CWE-416 | `tioscpgrp_cve_2020_29661` |
|
||||
| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` |
|
||||
| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
|
||||
| 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-3269 | CWE-416 | `stackrot_cve_2023_3269` |
|
||||
| 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-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` |
|
||||
|
||||
@@ -1,3 +1,143 @@
|
||||
## 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.1–6.18.22, 6.19.0–6.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
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
{"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-24T03:55:18Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"OK","actual_detect":"OK","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"}
|
||||
|
||||
+11
-11
@@ -56,7 +56,7 @@
|
||||
<div class="container hero-inner">
|
||||
<div class="hero-eyebrow">
|
||||
<span class="dot dot-pulse"></span>
|
||||
v0.9.2 — released 2026-05-24
|
||||
v0.9.6 — released 2026-05-28
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="display-wordmark">SKELETONKEY</span>
|
||||
@@ -83,7 +83,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="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>
|
||||
|
||||
@@ -210,7 +210,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
|
||||
<article class="bento-card">
|
||||
<div class="bento-icon">🛡</div>
|
||||
<h3>119 detection rules</h3>
|
||||
<h3>151 detection rules</h3>
|
||||
<p>
|
||||
auditd · sigma · yara · falco. One command emits the corpus for
|
||||
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>
|
||||
<h3>CISA KEV prioritized</h3>
|
||||
<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.
|
||||
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
|
||||
</p>
|
||||
@@ -294,9 +294,9 @@ uid=0(root) gid=0(root)</pre>
|
||||
<code>tools/verify-vm/</code> spins up known-vulnerable
|
||||
kernels (stock distro + mainline from kernel.ubuntu.com), runs
|
||||
<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
|
||||
+ 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.
|
||||
</p>
|
||||
</article>
|
||||
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<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>
|
||||
|
||||
<h3 class="corpus-h" data-color="green">
|
||||
@@ -414,7 +414,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="audience-icon">🎓</div>
|
||||
<h3>Researchers / CTF</h3>
|
||||
<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.
|
||||
<code>--explain</code> shows the reasoning chain; detection
|
||||
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-tag">shipped</div>
|
||||
<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>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>OPSEC notes</strong> — per-module runtime footprint</li>
|
||||
<li><strong>CISA KEV + NVD CWE + MITRE ATT&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>88-test harness (kernel_range + detect integration)</li>
|
||||
</ul>
|
||||
@@ -598,7 +598,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
who found the bugs.
|
||||
</p>
|
||||
<p class="footer-meta">
|
||||
v0.9.2 · 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>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 123 KiB |
+2
-2
@@ -54,9 +54,9 @@
|
||||
<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>
|
||||
|
||||
<!-- 11 KEV -->
|
||||
<!-- 12 KEV -->
|
||||
<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>
|
||||
|
||||
<!-- 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, 10, 100},
|
||||
{5, 15, 23},
|
||||
{5, 16, 9},
|
||||
{5, 16, 7}, /* Debian tracker: earlier than 5.16.9 in stable */
|
||||
{5, 17, 0}, /* mainline */
|
||||
};
|
||||
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
|
||||
static const struct kernel_patched_from cls_route4_patched_branches[] = {
|
||||
{5, 4, 213},
|
||||
{5, 10, 143},
|
||||
{5, 10, 136}, /* Debian tracker: earlier than 5.10.143 */
|
||||
{5, 15, 69},
|
||||
{5, 18, 18},
|
||||
{5, 18, 16}, /* Debian tracker: earlier than 5.18.18 */
|
||||
{5, 19, 7},
|
||||
{5, 20, 0}, /* mainline */
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ static const struct kernel_patched_from dirty_cow_patched_branches[] = {
|
||||
{3, 16, 38},
|
||||
{3, 18, 43},
|
||||
{4, 4, 26}, /* Ubuntu 16.04 baseline */
|
||||
{4, 7, 10},
|
||||
{4, 7, 8}, /* Debian tracker: earlier than 4.7.10 */
|
||||
{4, 8, 3},
|
||||
{4, 9, 0}, /* mainline fix */
|
||||
};
|
||||
|
||||
@@ -204,7 +204,7 @@ static void revert_passwd_page_cache(void)
|
||||
* - mainline (≥ 5.17) is patched
|
||||
*/
|
||||
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, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */
|
||||
{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.
|
||||
*
|
||||
* The detect logic therefore is:
|
||||
* - kernel < 7.0 → SKELETONKEY_OK (predates the bug)
|
||||
* - kernel ≥ 7.0 → consult kernel_range; 7.0+ has the fix
|
||||
* - --active → empirical override (catches pre-fix 7.0-rc kernels
|
||||
* or weird distro rebuilds the version check missed)
|
||||
* - kernel < 6.16.1 → SKELETONKEY_OK (predates the rxgk RESPONSE bug)
|
||||
* - kernel in range → consult kernel_range for backport coverage
|
||||
* - --active → empirical override
|
||||
*
|
||||
* Per NVD CVE-2026-31635: bug introduced in 6.16.1 stable; vulnerable
|
||||
* range is 6.16.1–6.18.22 + 6.19.0–6.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[] = {
|
||||
{6, 18, 23}, /* 6.18.x stable backport */
|
||||
{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 = {
|
||||
.patched_from = dirtydecrypt_patched_branches,
|
||||
@@ -697,11 +701,12 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 7, 0, 0)) {
|
||||
/* Predates the bug: rxgk RESPONSE-handling bug entered at 6.16.1
|
||||
* 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)
|
||||
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);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
@@ -903,11 +903,25 @@ static int fg_active_probe(void)
|
||||
* - --active → empirical override (catches distro silent
|
||||
* backports and unfixed 7.0.x ≤ 7.0.8)
|
||||
*
|
||||
* Stable-branch backports for 5.10 / 6.1 / 6.12 — when they ship —
|
||||
* extend the table with the matching {major, minor, patch} entry.
|
||||
* Per NVD CVE-2026-46300 (queried 2026-05-28): SKBFL_SHARED_FRAG was
|
||||
* 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.0–5.15.207, fixed 5.15.208+
|
||||
* 6.1-LTS: vulnerable 5.16.0–6.1.173, fixed 6.1.174+
|
||||
* 6.6-LTS: vulnerable 6.2.0–6.6.140, fixed 6.6.141+
|
||||
* 6.12-LTS: vulnerable 6.7.0–6.12.90, fixed 6.12.91+
|
||||
* 6.18-LTS: vulnerable 6.13.0–6.18.32, fixed 6.18.33+
|
||||
* 7.0: vulnerable 6.19.0–7.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[] = {
|
||||
{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 = {
|
||||
.patched_from = fragnesia_patched_branches,
|
||||
@@ -930,6 +944,17 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx)
|
||||
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->json)
|
||||
fprintf(stderr, "[i] fragnesia: unprivileged user "
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
* VULNERABLE by version-only check; the RLIMIT_STACK active probe
|
||||
* (--active) is required to confirm exploitability on a real host. */
|
||||
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, 18, 8}, /* mainline + everything above inherits */
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ static const struct kernel_patched_from netfilter_xtcompat_patched_branches[] =
|
||||
{4, 14, 240},
|
||||
{4, 19, 198},
|
||||
{5, 4, 128},
|
||||
{5, 10, 46},
|
||||
{5, 10, 38}, /* Debian tracker: earlier than 5.10.46 */
|
||||
{5, 11, 20},
|
||||
{5, 12, 13},
|
||||
{5, 13, 0}, /* mainline (5.13 carries b29c457a6511) */
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
static const struct kernel_patched_from overlayfs_setuid_patched_branches[] = {
|
||||
{5, 10, 179}, /* 5.10.x stable backport (per Debian tracker — bullseye) */
|
||||
{5, 15, 110},
|
||||
{6, 1, 27},
|
||||
{6, 1, 11}, /* Debian tracker: earlier than 6.1.27 */
|
||||
{6, 2, 13},
|
||||
{6, 3, 0}, /* mainline */
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
* patch (likely 6.16 once the post-rc release tags). Conservatively
|
||||
* placeholding at {7, 0, 0} until that lands. */
|
||||
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
|
||||
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, 9, 182},
|
||||
{4, 14, 131},
|
||||
{4, 19, 58},
|
||||
{4, 19, 37}, /* Debian tracker: earlier than 4.19.58 */
|
||||
{5, 0, 20},
|
||||
{5, 1, 17},
|
||||
{5, 2, 0}, /* mainline (5.2-rc) */
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
static const struct kernel_patched_from sequoia_patched_branches[] = {
|
||||
{5, 4, 134},
|
||||
{5, 10, 52},
|
||||
{5, 10, 46}, /* Debian tracker: earlier than 5.10.52 */
|
||||
{5, 13, 4},
|
||||
{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)
|
||||
{
|
||||
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");
|
||||
if (!p) return false;
|
||||
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)
|
||||
{
|
||||
char cmd[512];
|
||||
/* -n: non-interactive (no password prompt); -l: list. */
|
||||
snprintf(cmd, sizeof cmd, "%s -ln 2>&1", sudo_path);
|
||||
/* -n: non-interactive (no password prompt); -l: list. The two flags
|
||||
* 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");
|
||||
if (!p) return false;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ static const struct kernel_patched_from tioscpgrp_patched_branches[] = {
|
||||
{4, 14, 213}, /* 4.14 LTS */
|
||||
{4, 19, 165}, /* 4.19 LTS */
|
||||
{5, 4, 85}, /* 5.4 LTS */
|
||||
{5, 9, 15}, /* Debian-tracked 5.9 backport */
|
||||
{5, 10, 0}, /* mainline fix in 5.10 */
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SKELETONKEY_VERSION "0.9.2"
|
||||
#define SKELETONKEY_VERSION "0.9.6"
|
||||
|
||||
static const char BANNER[] =
|
||||
"\n"
|
||||
|
||||
+10
-3
@@ -318,12 +318,13 @@ static const struct skeletonkey_host h_kernel_5_14_no_userns = {
|
||||
static void run_all(void)
|
||||
{
|
||||
#ifdef __linux__
|
||||
/* dirtydecrypt: kernel.major < 7 → predates the bug → OK */
|
||||
run_one("dirtydecrypt: kernel 6.12 predates 7.0 → OK",
|
||||
/* dirtydecrypt: rxgk RESPONSE bug entered at 6.16.1 per NVD;
|
||||
* 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,
|
||||
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,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
@@ -331,6 +332,12 @@ static void run_all(void)
|
||||
&dirtydecrypt_module, &h_ubuntu_24_userns_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 */
|
||||
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
|
||||
&fragnesia_module, &h_pre7_no_userns_no_dbus,
|
||||
|
||||
@@ -118,17 +118,37 @@ def fetch_kev_catalog() -> dict[str, str]:
|
||||
|
||||
|
||||
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)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"})
|
||||
blob = None
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
blob = json.loads(r.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr)
|
||||
return None, None
|
||||
except (urllib.error.URLError, json.JSONDecodeError) as e:
|
||||
print(f"[!] NVD parse error for {cve}: {e}", file=sys.stderr)
|
||||
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e:
|
||||
print(f"[!] NVD urlopen failed for {cve} ({e}); trying curl", file=sys.stderr)
|
||||
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 []
|
||||
if not vulns:
|
||||
|
||||
+99
-40
@@ -27,18 +27,28 @@ To skip boxes you don't need (save disk):
|
||||
./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
|
||||
`generic/ubuntu2204` + kernel pin `linux-image-5.15.0-43-generic`.
|
||||
2. `vagrant up skk-nf_tables` (provisions on first call, resumes on
|
||||
subsequent).
|
||||
3. Installs the pinned vulnerable kernel via `apt`, reboots.
|
||||
4. Mounts the local repo at `/vagrant`, runs `make`, then runs
|
||||
`skeletonkey --explain nf_tables --active`.
|
||||
5. Parses the `VERDICT:` line, compares against `expect_detect` from
|
||||
targets.yaml, emits a JSON verification record on stdout.
|
||||
6. Suspends the VM (`vagrant suspend`) — instant resume next run.
|
||||
`generic/ubuntu2204` + `mainline_version: 5.15.5`.
|
||||
2. `vagrant up skk-nf_tables` if not already running (each module gets
|
||||
its own machine for isolation).
|
||||
3. **Prep phase** — runs every prep provisioner that applies:
|
||||
- `pin-kernel-<pkg>` if `kernel_pkg` is set (apt install + GRUB_DEFAULT pin)
|
||||
- `pin-mainline-<ver>` if `mainline_version` is set (download from
|
||||
kernel.ubuntu.com/mainline, dpkg -i, GRUB_DEFAULT pin)
|
||||
- `module-provision-<name>` if `provisioners/<name>.sh` exists
|
||||
(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:
|
||||
|
||||
@@ -54,62 +64,107 @@ Lifecycle flags:
|
||||
```
|
||||
|
||||
Shows the (module, box, target kernel, expected verdict, notes) matrix
|
||||
for all 26 modules. Three are flagged `manual: true` because no
|
||||
public Vagrant box covers them:
|
||||
|
||||
- `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.
|
||||
for all targets. Modules with `manual: true` are blocked by their
|
||||
target environment — see the notes field for the reason (VMware-only
|
||||
guest, EOL kernel needed, t64-transition libs missing, etc.).
|
||||
|
||||
## 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
|
||||
{
|
||||
"module": "nf_tables",
|
||||
"verified_at": "2026-05-23T17:42:11Z",
|
||||
"host_kernel": "5.15.0-43-generic",
|
||||
"host_distro": "Ubuntu 22.04.5 LTS",
|
||||
"verified_at": "2026-05-24T03:24:01Z",
|
||||
"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",
|
||||
"log": "tools/verify-vm/logs/verify-nf_tables-20260523-174211.log"
|
||||
"status": "match"
|
||||
}
|
||||
```
|
||||
|
||||
`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:
|
||||
|
||||
- The kernel pin didn't take (check `host_kernel` against
|
||||
`kernel_version` in targets.yaml).
|
||||
- The kernel pin didn't take — check `host_kernel` against
|
||||
`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
|
||||
(e.g. apparmor blocks unprivileged userns; need to adjust the
|
||||
Vagrantfile provisioner).
|
||||
- The detect() logic is wrong for this kernel/distro combo (a real bug
|
||||
— fix it).
|
||||
(e.g. apparmor blocks unprivileged userns; provisioner needed).
|
||||
- The module's detect() logic is wrong for this kernel/distro combo
|
||||
(a real module bug — fix it, as we did for `dirtydecrypt` after
|
||||
cross-checking against NVD).
|
||||
|
||||
Records are intended to feed a per-module `verified_on[]` table (next
|
||||
project step) so `--list` can show a `✓ verified <date>` column.
|
||||
Run `tools/refresh-verifications.py` after new records land to
|
||||
regenerate `core/verifications.c` so the binary's `--explain` and
|
||||
`--list` reflect the latest evidence.
|
||||
|
||||
## How it routes module → box
|
||||
|
||||
Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has:
|
||||
|
||||
- `box` — which `boxes/` template (e.g. `ubuntu2204`)
|
||||
- `kernel_pkg` — apt package name to install if the stock kernel
|
||||
is patched (omit / empty if stock is already vulnerable)
|
||||
- `box` — generic/<distro> (e.g. `ubuntu2204`)
|
||||
- `kernel_pkg` — apt package for a vulnerable stock-archive kernel,
|
||||
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
|
||||
- `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
|
||||
it up automatically.
|
||||
Adding a new module is one block in targets.yaml. If the module needs
|
||||
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
|
||||
|
||||
@@ -120,6 +175,10 @@ tools/verify-vm/
|
||||
├── verify.sh per-module verifier
|
||||
├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars)
|
||||
├── targets.yaml module → box mapping with rationale
|
||||
├── provisioners/ optional per-module shell hooks
|
||||
│ ├── sudo_chwoot.sh
|
||||
│ ├── sudo_runas_neg1.sh
|
||||
│ └── udisks_libblockdev.sh
|
||||
└── logs/ per-verification stdout/stderr capture
|
||||
```
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ af_packet:
|
||||
box: ubuntu1804
|
||||
kernel_pkg: "" # stock 4.15.0-213-generic — patch backported
|
||||
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."
|
||||
|
||||
af_packet2:
|
||||
@@ -71,7 +71,7 @@ dirty_cow:
|
||||
box: ubuntu1804
|
||||
kernel_pkg: "" # 4.15.0 has the COW race fix; need older kernel
|
||||
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."
|
||||
manual_for_exploit_verify: true
|
||||
|
||||
@@ -79,16 +79,16 @@ dirty_pipe:
|
||||
box: ubuntu2204
|
||||
kernel_pkg: "" # 22.04 stock 5.15.0-91-generic
|
||||
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.)"
|
||||
|
||||
dirtydecrypt:
|
||||
box: ubuntu2204
|
||||
kernel_pkg: ""
|
||||
mainline_version: "6.19.7" # below the 7.0 introduction point → 'predates the bug' OK path
|
||||
mainline_version: "6.19.7" # below the 6.19.13 backport → genuinely vulnerable
|
||||
kernel_version: "6.19.7"
|
||||
expect_detect: OK
|
||||
notes: "CVE-2026-31635; rxgk RESPONSE-handling bug. Module's range table says fix lands at 7.0.0 mainline (commit a2567217) — meaning the bug only existed in 7.0-rcN pre-release. No shipping stable kernel is VULNERABLE. We verify the 'kernel predates rxgk code added in 7.0' OK path via mainline 6.19.7. To test VULNERABLE would require building from a 7.0-rcN tag pre-a2567217, deferred."
|
||||
expect_detect: VULNERABLE
|
||||
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:
|
||||
box: ubuntu2204
|
||||
@@ -241,7 +241,7 @@ pintheft:
|
||||
box: "" # RDS is blacklisted on every common Vagrant box's stock kernel
|
||||
kernel_pkg: ""
|
||||
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."
|
||||
manual: true
|
||||
|
||||
@@ -273,7 +273,7 @@ vsock_uaf:
|
||||
box: "" # vsock module typically not loaded on CI containers (no virtualization)
|
||||
kernel_pkg: ""
|
||||
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)."
|
||||
manual: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user