Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25c2afc3e9 | |||
| 13fbbce618 | |||
| bb5ca48fe1 | |||
| 4454d8148e |
@@ -10,6 +10,10 @@ on:
|
|||||||
# Runs Monday 06:00 UTC; reports any new backports / KEV additions
|
# Runs Monday 06:00 UTC; reports any new backports / KEV additions
|
||||||
# that haven't propagated into the corpus yet.
|
# that haven't propagated into the corpus yet.
|
||||||
- cron: '0 6 * * 1'
|
- cron: '0 6 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
# Lets us trigger the drift-check job on demand (e.g. after a
|
||||||
|
# metadata refresh) without waiting for the weekly cron. The
|
||||||
|
# drift-check job's `if:` gate honors this trigger.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ also compile (modules with Linux-only headers stub out gracefully).
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**v0.9.3 cut 2026-05-24.** 39 modules across 34 CVEs — **every
|
**v0.9.6 cut 2026-05-28.** 39 modules across 34 CVEs — **every
|
||||||
year 2016 → 2026 now covered**. v0.9.0 added 5 gap-fillers
|
year 2016 → 2026 now covered**. v0.9.0 added 5 gap-fillers
|
||||||
(`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` /
|
(`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` /
|
||||||
`nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` /
|
`nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` /
|
||||||
|
|||||||
+50
-53
@@ -28,6 +28,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
.kev_date_added = "",
|
.kev_date_added = "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2018-14634",
|
||||||
|
.cwe = "CWE-190",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = true,
|
||||||
|
.kev_date_added = "2026-01-26",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.cve = "CVE-2019-13272",
|
.cve = "CVE-2019-13272",
|
||||||
.cwe = NULL,
|
.cwe = NULL,
|
||||||
@@ -36,6 +44,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = true,
|
.in_kev = true,
|
||||||
.kev_date_added = "2021-12-10",
|
.kev_date_added = "2021-12-10",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2019-14287",
|
||||||
|
.cwe = "CWE-755",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = false,
|
||||||
|
.kev_date_added = "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.cve = "CVE-2020-14386",
|
.cve = "CVE-2020-14386",
|
||||||
.cwe = "CWE-250",
|
.cwe = "CWE-250",
|
||||||
@@ -44,6 +60,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
.kev_date_added = "",
|
.kev_date_added = "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2020-29661",
|
||||||
|
.cwe = "CWE-416",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = false,
|
||||||
|
.kev_date_added = "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.cve = "CVE-2021-22555",
|
.cve = "CVE-2021-22555",
|
||||||
.cwe = "CWE-787",
|
.cwe = "CWE-787",
|
||||||
@@ -196,60 +220,9 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = true,
|
.in_kev = true,
|
||||||
.kev_date_added = "2024-05-30",
|
.kev_date_added = "2024-05-30",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.cve = "CVE-2026-31635",
|
|
||||||
.cwe = "CWE-130",
|
|
||||||
.attack_technique = "T1068",
|
|
||||||
.attack_subtechnique = NULL,
|
|
||||||
.in_kev = false,
|
|
||||||
.kev_date_added = "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
.cve = "CVE-2026-41651",
|
|
||||||
.cwe = "CWE-367",
|
|
||||||
.attack_technique = "T1068",
|
|
||||||
.attack_subtechnique = NULL,
|
|
||||||
.in_kev = false,
|
|
||||||
.kev_date_added = "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
.cve = "CVE-2026-46300",
|
|
||||||
.cwe = NULL,
|
|
||||||
.attack_technique = "T1068",
|
|
||||||
.attack_subtechnique = NULL,
|
|
||||||
.in_kev = false,
|
|
||||||
.kev_date_added = "",
|
|
||||||
},
|
|
||||||
/* v0.8.0 / v0.9.0 module additions — populated via direct CISA KEV
|
|
||||||
* + NVD curl on 2026-05-24 when refresh-cve-metadata.py's urlopen
|
|
||||||
* hung on CISA's HTTP/2 endpoint. Same data, different transport. */
|
|
||||||
{
|
|
||||||
.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-14287",
|
|
||||||
.cwe = "CWE-755",
|
|
||||||
.attack_technique = "T1068",
|
|
||||||
.attack_subtechnique = NULL,
|
|
||||||
.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-2024-26581",
|
.cve = "CVE-2024-26581",
|
||||||
.cwe = NULL, /* NVD: no CWE assigned */
|
.cwe = NULL,
|
||||||
.attack_technique = "T1068",
|
.attack_technique = "T1068",
|
||||||
.attack_subtechnique = NULL,
|
.attack_subtechnique = NULL,
|
||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
@@ -279,9 +252,33 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
.kev_date_added = "",
|
.kev_date_added = "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2026-31635",
|
||||||
|
.cwe = "CWE-130",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = false,
|
||||||
|
.kev_date_added = "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2026-41651",
|
||||||
|
.cwe = "CWE-367",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = false,
|
||||||
|
.kev_date_added = "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.cve = "CVE-2026-43494",
|
.cve = "CVE-2026-43494",
|
||||||
.cwe = NULL, /* NVD: no CWE assigned */
|
.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_technique = "T1068",
|
||||||
.attack_subtechnique = NULL,
|
.attack_subtechnique = NULL,
|
||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
|
|||||||
+73
-1
@@ -17,6 +17,15 @@
|
|||||||
"in_kev": false,
|
"in_kev": false,
|
||||||
"kev_date_added": ""
|
"kev_date_added": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2018-14634",
|
||||||
|
"module_dir": "mutagen_astronomy_cve_2018_14634",
|
||||||
|
"cwe": "CWE-190",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": true,
|
||||||
|
"kev_date_added": "2026-01-26"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cve": "CVE-2019-13272",
|
"cve": "CVE-2019-13272",
|
||||||
"module_dir": "ptrace_traceme_cve_2019_13272",
|
"module_dir": "ptrace_traceme_cve_2019_13272",
|
||||||
@@ -26,6 +35,15 @@
|
|||||||
"in_kev": true,
|
"in_kev": true,
|
||||||
"kev_date_added": "2021-12-10"
|
"kev_date_added": "2021-12-10"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2019-14287",
|
||||||
|
"module_dir": "sudo_runas_neg1_cve_2019_14287",
|
||||||
|
"cwe": "CWE-755",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cve": "CVE-2020-14386",
|
"cve": "CVE-2020-14386",
|
||||||
"module_dir": "af_packet2_cve_2020_14386",
|
"module_dir": "af_packet2_cve_2020_14386",
|
||||||
@@ -35,6 +53,15 @@
|
|||||||
"in_kev": false,
|
"in_kev": false,
|
||||||
"kev_date_added": ""
|
"kev_date_added": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2020-29661",
|
||||||
|
"module_dir": "tioscpgrp_cve_2020_29661",
|
||||||
|
"cwe": "CWE-416",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cve": "CVE-2021-22555",
|
"cve": "CVE-2021-22555",
|
||||||
"module_dir": "netfilter_xtcompat_cve_2021_22555",
|
"module_dir": "netfilter_xtcompat_cve_2021_22555",
|
||||||
@@ -206,6 +233,42 @@
|
|||||||
"in_kev": true,
|
"in_kev": true,
|
||||||
"kev_date_added": "2024-05-30"
|
"kev_date_added": "2024-05-30"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2024-26581",
|
||||||
|
"module_dir": "nft_pipapo_cve_2024_26581",
|
||||||
|
"cwe": null,
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2024-50264",
|
||||||
|
"module_dir": "vsock_uaf_cve_2024_50264",
|
||||||
|
"cwe": "CWE-416",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2025-32463",
|
||||||
|
"module_dir": "sudo_chwoot_cve_2025_32463",
|
||||||
|
"cwe": "CWE-829",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": true,
|
||||||
|
"kev_date_added": "2025-09-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2025-6019",
|
||||||
|
"module_dir": "udisks_libblockdev_cve_2025_6019",
|
||||||
|
"cwe": "CWE-250",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cve": "CVE-2026-31635",
|
"cve": "CVE-2026-31635",
|
||||||
"module_dir": "dirtydecrypt_cve_2026_31635",
|
"module_dir": "dirtydecrypt_cve_2026_31635",
|
||||||
@@ -224,10 +287,19 @@
|
|||||||
"in_kev": false,
|
"in_kev": false,
|
||||||
"kev_date_added": ""
|
"kev_date_added": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2026-43494",
|
||||||
|
"module_dir": "pintheft_cve_2026_43494",
|
||||||
|
"cwe": null,
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cve": "CVE-2026-46300",
|
"cve": "CVE-2026-46300",
|
||||||
"module_dir": "fragnesia_cve_2026_46300",
|
"module_dir": "fragnesia_cve_2026_46300",
|
||||||
"cwe": null,
|
"cwe": "CWE-787",
|
||||||
"attack_technique": "T1068",
|
"attack_technique": "T1068",
|
||||||
"attack_subtechnique": null,
|
"attack_subtechnique": null,
|
||||||
"in_kev": false,
|
"in_kev": false,
|
||||||
|
|||||||
+10
-2
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
|
|||||||
in the wild per the Known Exploited Vulnerabilities catalog.
|
in the wild per the Known Exploited Vulnerabilities catalog.
|
||||||
Refreshed via `tools/refresh-cve-metadata.py`.
|
Refreshed via `tools/refresh-cve-metadata.py`.
|
||||||
|
|
||||||
**10 of 26 modules cover KEV-listed CVEs.**
|
**12 of 34 modules cover KEV-listed CVEs.**
|
||||||
|
|
||||||
## In KEV (prioritize patching)
|
## In KEV (prioritize patching)
|
||||||
|
|
||||||
@@ -19,7 +19,9 @@ Refreshed via `tools/refresh-cve-metadata.py`.
|
|||||||
| CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` |
|
| CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` |
|
||||||
| CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` |
|
| CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` |
|
||||||
| CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` |
|
| CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` |
|
||||||
|
| CVE-2025-32463 | 2025-09-29 | CWE-829 | `sudo_chwoot_cve_2025_32463` |
|
||||||
| CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` |
|
| CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` |
|
||||||
|
| CVE-2018-14634 | 2026-01-26 | CWE-190 | `mutagen_astronomy_cve_2018_14634` |
|
||||||
|
|
||||||
## Not in KEV
|
## Not in KEV
|
||||||
|
|
||||||
@@ -30,7 +32,9 @@ and are technically reachable. "Not in KEV" is not the same as
|
|||||||
| CVE | CWE | Module |
|
| CVE | CWE | Module |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` |
|
| CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` |
|
||||||
|
| CVE-2019-14287 | CWE-755 | `sudo_runas_neg1_cve_2019_14287` |
|
||||||
| CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` |
|
| CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` |
|
||||||
|
| CVE-2020-29661 | CWE-416 | `tioscpgrp_cve_2020_29661` |
|
||||||
| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` |
|
| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` |
|
||||||
| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
|
| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
|
||||||
| CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` |
|
| CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` |
|
||||||
@@ -42,6 +46,10 @@ and are technically reachable. "Not in KEV" is not the same as
|
|||||||
| CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` |
|
| CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` |
|
||||||
| CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` |
|
| CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` |
|
||||||
| CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` |
|
| CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` |
|
||||||
|
| CVE-2024-26581 | ? | `nft_pipapo_cve_2024_26581` |
|
||||||
|
| CVE-2024-50264 | CWE-416 | `vsock_uaf_cve_2024_50264` |
|
||||||
|
| CVE-2025-6019 | CWE-250 | `udisks_libblockdev_cve_2025_6019` |
|
||||||
| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` |
|
| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` |
|
||||||
| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` |
|
| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` |
|
||||||
| CVE-2026-46300 | ? | `fragnesia_cve_2026_46300` |
|
| CVE-2026-43494 | ? | `pintheft_cve_2026_43494` |
|
||||||
|
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
|
||||||
|
|||||||
@@ -1,3 +1,113 @@
|
|||||||
|
## 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
|
## SKELETONKEY v0.9.3 — CVE metadata refresh + dirtydecrypt range fix
|
||||||
|
|
||||||
**CVE metadata refresh (10 → 12 KEV).** Populated the 8 missing
|
**CVE metadata refresh (10 → 12 KEV).** Populated the 8 missing
|
||||||
|
|||||||
+2
-2
@@ -56,7 +56,7 @@
|
|||||||
<div class="container hero-inner">
|
<div class="container hero-inner">
|
||||||
<div class="hero-eyebrow">
|
<div class="hero-eyebrow">
|
||||||
<span class="dot dot-pulse"></span>
|
<span class="dot dot-pulse"></span>
|
||||||
v0.9.3 — released 2026-05-24
|
v0.9.6 — released 2026-05-28
|
||||||
</div>
|
</div>
|
||||||
<h1 class="hero-title">
|
<h1 class="hero-title">
|
||||||
<span class="display-wordmark">SKELETONKEY</span>
|
<span class="display-wordmark">SKELETONKEY</span>
|
||||||
@@ -598,7 +598,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
who found the bugs.
|
who found the bugs.
|
||||||
</p>
|
</p>
|
||||||
<p class="footer-meta">
|
<p class="footer-meta">
|
||||||
v0.9.3 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
v0.9.6 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ static const struct kernel_patched_from cgroup_ra_patched_branches[] = {
|
|||||||
{5, 4, 179},
|
{5, 4, 179},
|
||||||
{5, 10, 100},
|
{5, 10, 100},
|
||||||
{5, 15, 23},
|
{5, 15, 23},
|
||||||
{5, 16, 9},
|
{5, 16, 7}, /* Debian tracker: earlier than 5.16.9 in stable */
|
||||||
{5, 17, 0}, /* mainline */
|
{5, 17, 0}, /* mainline */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@
|
|||||||
|
|
||||||
static const struct kernel_patched_from cls_route4_patched_branches[] = {
|
static const struct kernel_patched_from cls_route4_patched_branches[] = {
|
||||||
{5, 4, 213},
|
{5, 4, 213},
|
||||||
{5, 10, 143},
|
{5, 10, 136}, /* Debian tracker: earlier than 5.10.143 */
|
||||||
{5, 15, 69},
|
{5, 15, 69},
|
||||||
{5, 18, 18},
|
{5, 18, 16}, /* Debian tracker: earlier than 5.18.18 */
|
||||||
{5, 19, 7},
|
{5, 19, 7},
|
||||||
{5, 20, 0}, /* mainline */
|
{5, 20, 0}, /* mainline */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ static const struct kernel_patched_from dirty_cow_patched_branches[] = {
|
|||||||
{3, 16, 38},
|
{3, 16, 38},
|
||||||
{3, 18, 43},
|
{3, 18, 43},
|
||||||
{4, 4, 26}, /* Ubuntu 16.04 baseline */
|
{4, 4, 26}, /* Ubuntu 16.04 baseline */
|
||||||
{4, 7, 10},
|
{4, 7, 8}, /* Debian tracker: earlier than 4.7.10 */
|
||||||
{4, 8, 3},
|
{4, 8, 3},
|
||||||
{4, 9, 0}, /* mainline fix */
|
{4, 9, 0}, /* mainline fix */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ static void revert_passwd_page_cache(void)
|
|||||||
* - mainline (≥ 5.17) is patched
|
* - mainline (≥ 5.17) is patched
|
||||||
*/
|
*/
|
||||||
static const struct kernel_patched_from dirty_pipe_patched_branches[] = {
|
static const struct kernel_patched_from dirty_pipe_patched_branches[] = {
|
||||||
{5, 10, 102}, /* 5.10.x backport */
|
{5, 10, 92}, /* 5.10.x backport (Debian tracker: earlier than 5.10.102) */
|
||||||
{5, 15, 25}, /* 5.15.x backport */
|
{5, 15, 25}, /* 5.15.x backport */
|
||||||
{5, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */
|
{5, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */
|
||||||
{5, 17, 0}, /* mainline fix lands; everything from here is fine */
|
{5, 17, 0}, /* mainline fix lands; everything from here is fine */
|
||||||
|
|||||||
@@ -903,11 +903,25 @@ static int fg_active_probe(void)
|
|||||||
* - --active → empirical override (catches distro silent
|
* - --active → empirical override (catches distro silent
|
||||||
* backports and unfixed 7.0.x ≤ 7.0.8)
|
* backports and unfixed 7.0.x ≤ 7.0.8)
|
||||||
*
|
*
|
||||||
* Stable-branch backports for 5.10 / 6.1 / 6.12 — when they ship —
|
* Per NVD CVE-2026-46300 (queried 2026-05-28): SKBFL_SHARED_FRAG was
|
||||||
* extend the table with the matching {major, minor, patch} entry.
|
* introduced at 5.11; the marker-propagation bug is present 5.11+. The
|
||||||
|
* fix was backported across every active stable branch:
|
||||||
|
*
|
||||||
|
* 5.15-LTS: vulnerable 5.15.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[] = {
|
static const struct kernel_patched_from fragnesia_patched_branches[] = {
|
||||||
{7, 0, 9}, /* mainline + 7.0.x stable: fix lands at 7.0.9 */
|
{5, 15, 208}, /* 5.15-LTS backport */
|
||||||
|
{6, 1, 174}, /* 6.1-LTS backport */
|
||||||
|
{6, 6, 141}, /* 6.6-LTS backport */
|
||||||
|
{6, 12, 90}, /* 6.12-LTS backport (Debian trixie ships .90 with fix) */
|
||||||
|
{6, 18, 33}, /* 6.18-LTS backport */
|
||||||
|
{7, 0, 9}, /* 7.0 stable (Debian forky/sid ship .9 with backported fix) */
|
||||||
};
|
};
|
||||||
static const struct kernel_range fragnesia_range = {
|
static const struct kernel_range fragnesia_range = {
|
||||||
.patched_from = fragnesia_patched_branches,
|
.patched_from = fragnesia_patched_branches,
|
||||||
@@ -930,6 +944,17 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Predates the bug: SKBFL_SHARED_FRAG marker only exists from 5.11
|
||||||
|
* onwards; older kernels don't have the buggy skb_try_coalesce()
|
||||||
|
* code path. */
|
||||||
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 11, 0)) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] fragnesia: kernel %s predates the "
|
||||||
|
"SKBFL_SHARED_FRAG marker added in 5.11 — not "
|
||||||
|
"applicable\n", v->release);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ctx->host->unprivileged_userns_allowed) {
|
if (!ctx->host->unprivileged_userns_allowed) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] fragnesia: unprivileged user "
|
fprintf(stderr, "[i] fragnesia: unprivileged user "
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
* VULNERABLE by version-only check; the RLIMIT_STACK active probe
|
* VULNERABLE by version-only check; the RLIMIT_STACK active probe
|
||||||
* (--active) is required to confirm exploitability on a real host. */
|
* (--active) is required to confirm exploitability on a real host. */
|
||||||
static const struct kernel_patched_from mutagen_patched_branches[] = {
|
static const struct kernel_patched_from mutagen_patched_branches[] = {
|
||||||
|
{4, 12, 6}, /* Debian-tracked backport on 4.12 branch */
|
||||||
{4, 14, 71}, /* 4.14 LTS stable backport */
|
{4, 14, 71}, /* 4.14 LTS stable backport */
|
||||||
{4, 18, 8}, /* mainline + everything above inherits */
|
{4, 18, 8}, /* mainline + everything above inherits */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ static const struct kernel_patched_from netfilter_xtcompat_patched_branches[] =
|
|||||||
{4, 14, 240},
|
{4, 14, 240},
|
||||||
{4, 19, 198},
|
{4, 19, 198},
|
||||||
{5, 4, 128},
|
{5, 4, 128},
|
||||||
{5, 10, 46},
|
{5, 10, 38}, /* Debian tracker: earlier than 5.10.46 */
|
||||||
{5, 11, 20},
|
{5, 11, 20},
|
||||||
{5, 12, 13},
|
{5, 12, 13},
|
||||||
{5, 13, 0}, /* mainline (5.13 carries b29c457a6511) */
|
{5, 13, 0}, /* mainline (5.13 carries b29c457a6511) */
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
static const struct kernel_patched_from overlayfs_setuid_patched_branches[] = {
|
static const struct kernel_patched_from overlayfs_setuid_patched_branches[] = {
|
||||||
{5, 10, 179}, /* 5.10.x stable backport (per Debian tracker — bullseye) */
|
{5, 10, 179}, /* 5.10.x stable backport (per Debian tracker — bullseye) */
|
||||||
{5, 15, 110},
|
{5, 15, 110},
|
||||||
{6, 1, 27},
|
{6, 1, 11}, /* Debian tracker: earlier than 6.1.27 */
|
||||||
{6, 2, 13},
|
{6, 2, 13},
|
||||||
{6, 3, 0}, /* mainline */
|
{6, 3, 0}, /* mainline */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
* patch (likely 6.16 once the post-rc release tags). Conservatively
|
* patch (likely 6.16 once the post-rc release tags). Conservatively
|
||||||
* placeholding at {7, 0, 0} until that lands. */
|
* placeholding at {7, 0, 0} until that lands. */
|
||||||
static const struct kernel_patched_from pintheft_patched_branches[] = {
|
static const struct kernel_patched_from pintheft_patched_branches[] = {
|
||||||
|
{6, 12, 90}, /* Debian trixie ships 6.12.90 with the fix backported */
|
||||||
{7, 0, 0}, /* mainline fix commit 0cebaccef3ac; tag will be 6.16 or 7.0
|
{7, 0, 0}, /* mainline fix commit 0cebaccef3ac; tag will be 6.16 or 7.0
|
||||||
depending on when 6.15 closes — refresh when known */
|
depending on when 6.15 closes — refresh when known */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ static const struct kernel_patched_from ptrace_traceme_patched_branches[] = {
|
|||||||
{4, 4, 182},
|
{4, 4, 182},
|
||||||
{4, 9, 182},
|
{4, 9, 182},
|
||||||
{4, 14, 131},
|
{4, 14, 131},
|
||||||
{4, 19, 58},
|
{4, 19, 37}, /* Debian tracker: earlier than 4.19.58 */
|
||||||
{5, 0, 20},
|
{5, 0, 20},
|
||||||
{5, 1, 17},
|
{5, 1, 17},
|
||||||
{5, 2, 0}, /* mainline (5.2-rc) */
|
{5, 2, 0}, /* mainline (5.2-rc) */
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
static const struct kernel_patched_from sequoia_patched_branches[] = {
|
static const struct kernel_patched_from sequoia_patched_branches[] = {
|
||||||
{5, 4, 134},
|
{5, 4, 134},
|
||||||
{5, 10, 52},
|
{5, 10, 46}, /* Debian tracker: earlier than 5.10.52 */
|
||||||
{5, 13, 4},
|
{5, 13, 4},
|
||||||
{5, 14, 0}, /* mainline */
|
{5, 14, 0}, /* mainline */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ static bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
|
|||||||
static bool find_runas_blacklist_grant(const char *sudo_path, char *cmd_out, size_t cap)
|
static bool find_runas_blacklist_grant(const char *sudo_path, char *cmd_out, size_t cap)
|
||||||
{
|
{
|
||||||
char cmd[512];
|
char cmd[512];
|
||||||
snprintf(cmd, sizeof cmd, "%s -ln 2>/dev/null", sudo_path);
|
/* -n -l separated + stdin closed: see sudoedit_editor for the same
|
||||||
|
* pattern + rationale. `--auto` must never block on a tty prompt. */
|
||||||
|
snprintf(cmd, sizeof cmd, "%s -n -l </dev/null 2>/dev/null", sudo_path);
|
||||||
FILE *p = popen(cmd, "r");
|
FILE *p = popen(cmd, "r");
|
||||||
if (!p) return false;
|
if (!p) return false;
|
||||||
char line[512];
|
char line[512];
|
||||||
|
|||||||
@@ -149,8 +149,13 @@ static bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
|
|||||||
static bool find_sudoedit_target(const char *sudo_path, char *out, size_t outsz)
|
static bool find_sudoedit_target(const char *sudo_path, char *out, size_t outsz)
|
||||||
{
|
{
|
||||||
char cmd[512];
|
char cmd[512];
|
||||||
/* -n: non-interactive (no password prompt); -l: list. */
|
/* -n: non-interactive (no password prompt); -l: list. The two flags
|
||||||
snprintf(cmd, sizeof cmd, "%s -ln 2>&1", sudo_path);
|
* are written separately and stdin is redirected from /dev/null so
|
||||||
|
* sudo cannot fall back to a tty prompt even if the local PAM stack
|
||||||
|
* tries to coerce one (some sudoers + pam_unix configurations have
|
||||||
|
* been observed prompting despite `-n` when the flags are bundled
|
||||||
|
* as `-ln`). Belt-and-suspenders so `--auto` never blocks on input. */
|
||||||
|
snprintf(cmd, sizeof cmd, "%s -n -l </dev/null 2>&1", sudo_path);
|
||||||
FILE *p = popen(cmd, "r");
|
FILE *p = popen(cmd, "r");
|
||||||
if (!p) return false;
|
if (!p) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ static const struct kernel_patched_from tioscpgrp_patched_branches[] = {
|
|||||||
{4, 14, 213}, /* 4.14 LTS */
|
{4, 14, 213}, /* 4.14 LTS */
|
||||||
{4, 19, 165}, /* 4.19 LTS */
|
{4, 19, 165}, /* 4.19 LTS */
|
||||||
{5, 4, 85}, /* 5.4 LTS */
|
{5, 4, 85}, /* 5.4 LTS */
|
||||||
|
{5, 9, 15}, /* Debian-tracked 5.9 backport */
|
||||||
{5, 10, 0}, /* mainline fix in 5.10 */
|
{5, 10, 0}, /* mainline fix in 5.10 */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define SKELETONKEY_VERSION "0.9.3"
|
#define SKELETONKEY_VERSION "0.9.6"
|
||||||
|
|
||||||
static const char BANNER[] =
|
static const char BANNER[] =
|
||||||
"\n"
|
"\n"
|
||||||
|
|||||||
@@ -332,6 +332,12 @@ static void run_all(void)
|
|||||||
&dirtydecrypt_module, &h_ubuntu_24_userns_ok,
|
&dirtydecrypt_module, &h_ubuntu_24_userns_ok,
|
||||||
SKELETONKEY_OK);
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* fragnesia: SKBFL_SHARED_FRAG marker added in 5.11; kernels before
|
||||||
|
* that predate the buggy skb_try_coalesce() code → OK */
|
||||||
|
run_one("fragnesia: kernel 4.4 predates 5.11 SKBFL_SHARED_FRAG → OK",
|
||||||
|
&fragnesia_module, &h_kernel_4_4,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
/* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */
|
/* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */
|
||||||
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
|
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
|
||||||
&fragnesia_module, &h_pre7_no_userns_no_dbus,
|
&fragnesia_module, &h_pre7_no_userns_no_dbus,
|
||||||
|
|||||||
@@ -118,18 +118,38 @@ def fetch_kev_catalog() -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]:
|
def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]:
|
||||||
"""Return (cwe_id, description) from NVD. Returns (None, None) on miss."""
|
"""Return (cwe_id, description) from NVD. Returns (None, None) on miss.
|
||||||
|
|
||||||
|
Same urlopen-hangs-silently pattern as the CISA fetch: NVD's HTTP/2
|
||||||
|
endpoint sometimes leaves Python sockets in CLOSE_WAIT forever even
|
||||||
|
though the 30s timeout should have fired (observed on macOS 2026-05-24,
|
||||||
|
process hung 55+ minutes). We try urlopen first, then fall back to
|
||||||
|
curl --max-time which honors the wall clock reliably."""
|
||||||
url = NVD_URL.format(cve=cve)
|
url = NVD_URL.format(cve=cve)
|
||||||
req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"})
|
req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"})
|
||||||
|
blob = None
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=30) as r:
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
blob = json.loads(r.read().decode("utf-8"))
|
blob = json.loads(r.read().decode("utf-8"))
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr)
|
print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr)
|
||||||
return None, None
|
return None, None
|
||||||
except (urllib.error.URLError, json.JSONDecodeError) as e:
|
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e:
|
||||||
print(f"[!] NVD parse error for {cve}: {e}", file=sys.stderr)
|
print(f"[!] NVD urlopen failed for {cve} ({e}); trying curl", file=sys.stderr)
|
||||||
return None, None
|
if blob is None:
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
raw = subprocess.check_output(
|
||||||
|
["curl", "-fsSL", "--max-time", "20",
|
||||||
|
"-H", "User-Agent: skeletonkey-cve-metadata/1",
|
||||||
|
url],
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
blob = json.loads(raw.decode("utf-8"))
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError,
|
||||||
|
json.JSONDecodeError) as e:
|
||||||
|
print(f"[!] NVD curl fallback failed for {cve}: {e}", file=sys.stderr)
|
||||||
|
return None, None
|
||||||
vulns = blob.get("vulnerabilities") or []
|
vulns = blob.get("vulnerabilities") or []
|
||||||
if not vulns:
|
if not vulns:
|
||||||
return None, None
|
return None, None
|
||||||
|
|||||||
+105
-46
@@ -27,18 +27,28 @@ To skip boxes you don't need (save disk):
|
|||||||
./tools/verify-vm/verify.sh nf_tables
|
./tools/verify-vm/verify.sh nf_tables
|
||||||
```
|
```
|
||||||
|
|
||||||
What that does:
|
What that does (two-phase model — install kernel, then verify):
|
||||||
|
|
||||||
1. Reads `tools/verify-vm/targets.yaml`: finds `nf_tables` → box
|
1. Reads `tools/verify-vm/targets.yaml`: finds `nf_tables` → box
|
||||||
`generic/ubuntu2204` + kernel pin `linux-image-5.15.0-43-generic`.
|
`generic/ubuntu2204` + `mainline_version: 5.15.5`.
|
||||||
2. `vagrant up skk-nf_tables` (provisions on first call, resumes on
|
2. `vagrant up skk-nf_tables` if not already running (each module gets
|
||||||
subsequent).
|
its own machine for isolation).
|
||||||
3. Installs the pinned vulnerable kernel via `apt`, reboots.
|
3. **Prep phase** — runs every prep provisioner that applies:
|
||||||
4. Mounts the local repo at `/vagrant`, runs `make`, then runs
|
- `pin-kernel-<pkg>` if `kernel_pkg` is set (apt install + GRUB_DEFAULT pin)
|
||||||
`skeletonkey --explain nf_tables --active`.
|
- `pin-mainline-<ver>` if `mainline_version` is set (download from
|
||||||
5. Parses the `VERDICT:` line, compares against `expect_detect` from
|
kernel.ubuntu.com/mainline, dpkg -i, GRUB_DEFAULT pin)
|
||||||
targets.yaml, emits a JSON verification record on stdout.
|
- `module-provision-<name>` if `provisioners/<name>.sh` exists
|
||||||
6. Suspends the VM (`vagrant suspend`) — instant resume next run.
|
(build vulnerable sudo from source, drop polkit allow rule,
|
||||||
|
install udisks2, etc.)
|
||||||
|
4. **Conditional reboot** — `vagrant reload` if `uname -r` doesn't
|
||||||
|
match the target kernel after the prep phase. Confirms post-reboot
|
||||||
|
kernel actually landed on the target; warns if it didn't.
|
||||||
|
5. **Verify phase** — `build-and-verify` provisioner: rsync the source,
|
||||||
|
`make`, run `skeletonkey --explain <module> --active`.
|
||||||
|
6. Parses the `VERDICT:` line, compares against `expect_detect` from
|
||||||
|
targets.yaml, appends a JSON verification record to
|
||||||
|
`docs/VERIFICATIONS.jsonl`.
|
||||||
|
7. Suspends the VM (`vagrant suspend`) — instant resume next run.
|
||||||
|
|
||||||
Lifecycle flags:
|
Lifecycle flags:
|
||||||
|
|
||||||
@@ -54,73 +64,122 @@ Lifecycle flags:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Shows the (module, box, target kernel, expected verdict, notes) matrix
|
Shows the (module, box, target kernel, expected verdict, notes) matrix
|
||||||
for all 26 modules. Three are flagged `manual: true` because no
|
for all targets. Modules with `manual: true` are blocked by their
|
||||||
public Vagrant box covers them:
|
target environment — see the notes field for the reason (VMware-only
|
||||||
|
guest, EOL kernel needed, t64-transition libs missing, etc.).
|
||||||
- `vmwgfx` — only reachable on VMware guests; needs a vSphere/Fusion VM
|
|
||||||
not Parallels.
|
|
||||||
- `dirtydecrypt`, `fragnesia` — only present in Linux 7.0+ which isn't
|
|
||||||
shipping as a distro kernel yet.
|
|
||||||
|
|
||||||
For those, verification needs a hand-built or special-distro VM.
|
|
||||||
|
|
||||||
## Verification records
|
## Verification records
|
||||||
|
|
||||||
`verify.sh` emits JSON on stdout after each run. Example:
|
`verify.sh` appends one JSON record per run to
|
||||||
|
`docs/VERIFICATIONS.jsonl`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"module": "nf_tables",
|
"module": "nf_tables",
|
||||||
"verified_at": "2026-05-23T17:42:11Z",
|
"verified_at": "2026-05-24T03:24:01Z",
|
||||||
"host_kernel": "5.15.0-43-generic",
|
"host_kernel": "5.15.5-051505-generic",
|
||||||
"host_distro": "Ubuntu 22.04.5 LTS",
|
"host_distro": "Ubuntu 22.04.3 LTS",
|
||||||
"vm_box": "generic/ubuntu2204",
|
"vm_box": "generic/ubuntu2204",
|
||||||
"expect_detect": "VULNERABLE",
|
"expect_detect": "VULNERABLE",
|
||||||
"actual_detect": "VULNERABLE",
|
"actual_detect": "VULNERABLE",
|
||||||
"status": "match",
|
"status": "match"
|
||||||
"log": "tools/verify-vm/logs/verify-nf_tables-20260523-174211.log"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`status: match` means detect() returned what we expected on a known-
|
`status: match` means detect() returned what we expected on a known-
|
||||||
vulnerable kernel. Anything else (`MISMATCH`, status code != 0) means
|
vulnerable kernel. Anything else (`MISMATCH`, exit code != 0) means
|
||||||
either:
|
either:
|
||||||
|
|
||||||
- The kernel pin didn't take (check `host_kernel` against
|
- The kernel pin didn't take — check `host_kernel` against
|
||||||
`kernel_version` in targets.yaml).
|
`kernel_version` in targets.yaml. The "post-reboot kernel" line in
|
||||||
|
the verify log will say if `vagrant reload` did or didn't land on
|
||||||
|
the target.
|
||||||
- The exploit's preconditions aren't met in the default Vagrant image
|
- The exploit's preconditions aren't met in the default Vagrant image
|
||||||
(e.g. apparmor blocks unprivileged userns; need to adjust the
|
(e.g. apparmor blocks unprivileged userns; provisioner needed).
|
||||||
Vagrantfile provisioner).
|
- The module's detect() logic is wrong for this kernel/distro combo
|
||||||
- The detect() logic is wrong for this kernel/distro combo (a real bug
|
(a real module bug — fix it, as we did for `dirtydecrypt` after
|
||||||
— fix it).
|
cross-checking against NVD).
|
||||||
|
|
||||||
Records are intended to feed a per-module `verified_on[]` table (next
|
Run `tools/refresh-verifications.py` after new records land to
|
||||||
project step) so `--list` can show a `✓ verified <date>` column.
|
regenerate `core/verifications.c` so the binary's `--explain` and
|
||||||
|
`--list` reflect the latest evidence.
|
||||||
|
|
||||||
## How it routes module → box
|
## How it routes module → box
|
||||||
|
|
||||||
Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has:
|
Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has:
|
||||||
|
|
||||||
- `box` — which `boxes/` template (e.g. `ubuntu2204`)
|
- `box` — generic/<distro> (e.g. `ubuntu2204`)
|
||||||
- `kernel_pkg` — apt package name to install if the stock kernel
|
- `kernel_pkg` — apt package for a vulnerable stock-archive kernel,
|
||||||
is patched (omit / empty if stock is already vulnerable)
|
if one still exists in the distro's repos
|
||||||
|
- `mainline_version` — alternative to `kernel_pkg`: pulls a vanilla
|
||||||
|
upstream kernel from `kernel.ubuntu.com/mainline/v<ver>/`. Use when
|
||||||
|
the apt-archive version has been garbage-collected (Ubuntu drops
|
||||||
|
old ABI versions) or when you need a specific point release that
|
||||||
|
the distro never packaged.
|
||||||
- `kernel_version` — what `uname -r` should report after install
|
- `kernel_version` — what `uname -r` should report after install
|
||||||
- `expect_detect` — `VULNERABLE` | `OK` | `PRECOND_FAIL`
|
- `expect_detect` — `VULNERABLE` | `OK` | `PRECOND_FAIL`
|
||||||
- `notes` — short rationale; comments in the file have the full context
|
- `manual: true` — skip auto verification; explain why in `notes`
|
||||||
|
- `notes` — full context for why this target was picked
|
||||||
|
|
||||||
Adding a new module is one block in targets.yaml. The verifier picks
|
Adding a new module is one block in targets.yaml. If the module needs
|
||||||
it up automatically.
|
per-target setup beyond installing a kernel — for example building
|
||||||
|
sudo from source, adding a sudoers grant, or dropping a polkit allow
|
||||||
|
rule — write a shell script at `tools/verify-vm/provisioners/<module>.sh`
|
||||||
|
and the Vagrantfile will pick it up automatically.
|
||||||
|
|
||||||
|
## Module-specific provisioners (`provisioners/<module>.sh`)
|
||||||
|
|
||||||
|
When the kernel pin alone doesn't make a host vulnerable — e.g.
|
||||||
|
the bug is sudo-version-gated, or a polkit "active session" check
|
||||||
|
blocks the SSH path — drop a shell script at
|
||||||
|
`tools/verify-vm/provisioners/<module_name>.sh`. The Vagrantfile
|
||||||
|
runs it as root in the prep phase, before the `vagrant reload`
|
||||||
|
check. Scripts should be idempotent (apt is no-op if installed,
|
||||||
|
file overwrites are safe) since they re-run on every verify.
|
||||||
|
|
||||||
|
Existing examples:
|
||||||
|
|
||||||
|
- `sudo_chwoot.sh` — builds sudo 1.9.16p1 from upstream into
|
||||||
|
`/usr/local/bin` so the vulnerable `--chroot` code path is reachable
|
||||||
|
on Ubuntu 22.04 (which ships pre-feature 1.9.9).
|
||||||
|
- `udisks_libblockdev.sh` — installs `udisks2` + drops a polkit rule
|
||||||
|
allowing the vagrant user to invoke `loop-setup` / `filesystem-mount`
|
||||||
|
(without this, the SSH session is not "active" per polkit and the
|
||||||
|
D-Bus call short-circuits).
|
||||||
|
- `sudo_runas_neg1.sh` — adds `vagrant ALL=(ALL,!root) NOPASSWD: /bin/vi`
|
||||||
|
to `/etc/sudoers.d/` so `find_runas_blacklist_grant()` has a grant
|
||||||
|
to abuse.
|
||||||
|
|
||||||
|
## Pinning kernels: apt vs mainline
|
||||||
|
|
||||||
|
`pin-kernel-<pkg>` runs `apt-get install -y <pkg>`. Best when the
|
||||||
|
target version still lives in the distro's archive (rare for old
|
||||||
|
point releases — Ubuntu eventually GCs them). Also pins `GRUB_DEFAULT`
|
||||||
|
to the just-installed kernel so the reboot lands on it instead of
|
||||||
|
the higher-version stock kernel.
|
||||||
|
|
||||||
|
`pin-mainline-<ver>` downloads vanilla mainline debs from
|
||||||
|
`kernel.ubuntu.com/mainline/v<ver>/`. Tries `/amd64/` first, falls
|
||||||
|
back to bare `/v<ver>/` for old kernels (≤ ~4.15) where amd64 wasn't
|
||||||
|
a separate subdir. Accepts both `linux-image-` (older naming) and
|
||||||
|
`linux-image-unsigned-` (current). Pins `GRUB_DEFAULT` to the
|
||||||
|
mainline kernel so grub doesn't keep booting the higher-versioned
|
||||||
|
stock kernel.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
tools/verify-vm/
|
tools/verify-vm/
|
||||||
├── README.md this file
|
├── README.md this file
|
||||||
├── setup.sh one-time bootstrap (Vagrant, plugin, box cache)
|
├── setup.sh one-time bootstrap (Vagrant, plugin, box cache)
|
||||||
├── verify.sh per-module verifier
|
├── verify.sh per-module verifier
|
||||||
├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars)
|
├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars)
|
||||||
├── targets.yaml module → box mapping with rationale
|
├── targets.yaml module → box mapping with rationale
|
||||||
└── logs/ per-verification stdout/stderr capture
|
├── provisioners/ optional per-module shell hooks
|
||||||
|
│ ├── sudo_chwoot.sh
|
||||||
|
│ ├── sudo_runas_neg1.sh
|
||||||
|
│ └── udisks_libblockdev.sh
|
||||||
|
└── logs/ per-verification stdout/stderr capture
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why Vagrant + Parallels
|
## Why Vagrant + Parallels
|
||||||
|
|||||||
Reference in New Issue
Block a user