9a4cc91619
Adds the third ported module — Pack2TheRoot, a userspace PackageKit
D-Bus TOCTOU LPE — and spends real effort hardening --auto so its
detect step gives an accurate, robust verdict before deploying.
pack2theroot (CVE-2026-41651):
- Ported from the public Vozec PoC
(github.com/Vozec/CVE-2026-41651). Original disclosure by the
Deutsche Telekom security team.
- Two back-to-back InstallFiles D-Bus calls (SIMULATE then NONE)
overwrite the cached transaction flags between polkit auth and
dispatch. GLib priority ordering makes the overwrite deterministic,
not a timing race; postinst of the malicious .deb drops a SUID bash
in /tmp.
- detect() reads PackageKit's VersionMajor/Minor/Micro directly over
D-Bus and compares against the pinned fix release 1.3.5 (commit
76cfb675). This is a high-confidence verdict, not precondition-only.
- Debian-family only (PoC builds its own .deb in pure C; ar/ustar/
gzip-stored inline). Cleanup removes /tmp .debs + best-effort
unlinks /tmp/.suid_bash + sudo -n dpkg -r the staging packages.
- Adds an optional GLib/GIO build dependency. The top-level Makefile
autodetects via `pkg-config gio-2.0`; when absent the module
compiles as a stub returning PRECOND_FAIL.
- Embedded auditd + sigma rules cover the file-side footprint
(/tmp/.suid_bash, /tmp/.pk-*.deb, non-root dpkg/apt execve).
--auto accuracy improvements:
- Auto-enables --active before the scan. Per-module sentinel probes
(page-cache /tmp files, fork-isolated namespace mounts) turn
version-only checks into definitive verdicts, so silent distro
backports don't fool the scan and --auto won't pick blind on
TEST_ERROR.
- Per-module verdict printing — every module's result is shown
(VULNERABLE / patched / precondition / indeterminate), not just
VULNERABLE rows. Operator sees the full picture.
- Scan-end summary line: "N vulnerable, M patched/n.a., K
precondition-fail, L indeterminate" with a separate callout when
modules crashed.
- Distro fingerprint added to the auto banner (ID + VERSION_ID from
/etc/os-release alongside kernel/arch).
- Fork-isolated detect() — each detector runs in a child process so
a SIGILL/SIGSEGV in one module's probe is contained and the scan
continues. Surfaced live while testing: entrybleed's prefetchnta
KASLR sweep SIGILLs on emulated CPUs (linuxkit on darwin); without
isolation the whole --auto died at module 7 of 31. With isolation
the scan reports "detect() crashed (signal 4) — continuing" and
finishes cleanly.
module_safety_rank additions:
- pack2theroot: 95 (userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint
— clean but heavier than pwnkit's gconv-modules-only path).
- dirtydecrypt / fragnesia: 86 (page-cache writes; one step below the
verified copy_fail/dirty_frag family at 88 to prefer verified
modules when both apply).
Docs:
- README badge / tagline / tier table / ⚪ block / example output /
v0.5.0 status — all updated to "28 verified + 3 ported".
- CVES.md counts line, the ported-modules note (now calling out
pack2theroot's high-confidence detect vs. precondition-only for
the page-cache pair), inventory row, operations table row.
- ROADMAP Phase 7+: pack2theroot moved out of carry-overs into the
"landed (ported, pending VM verification)" group; added a new
"--auto accuracy work" subsection documenting the dispatcher
hardening landed in this commit.
- docs/index.html: scanning-count example bumped to 31, status line
updated to mention 3 ported modules.
Build verification: full `make clean && make` in `docker gcc:latest`
with libglib2.0-dev installed: links into a 31-module skeletonkey
ELF (413KB), `--list` shows all modules including pack2theroot,
`--detect-rules --format=auditd` emits the new pack2theroot section,
`--auto --i-know --no-shell` exercises the new banner + active
probes + verdict table + fork isolation + scan summary end-to-end.
Only build warning is the pre-existing
`-Wunterminated-string-initialization` in dirty_pipe (not introduced
here).
73 lines
3.6 KiB
Markdown
73 lines
3.6 KiB
Markdown
# 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).
|