Files
SKELETONKEY/modules/pack2theroot_cve_2026_41651/MODULE.md
T
leviathan 9a4cc91619 pack2theroot (CVE-2026-41651) + --auto accuracy work
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).
2026-05-22 22:42:07 -04:00

3.6 KiB

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 .debs in /tmp, fires the two InstallFiles D-Bus calls back-to-back, polls up to 120s for /tmp/.suid_bash to appear, then execvs 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).