
CTF-style Docker lab for CVE-2026-41651 (Pack2TheRoot): PackageKit permissive-polkit local privilege escalation
A Dockerised, CTF-style local-privilege-escalation lab built around the
Pack2TheRoot advisory (CVE-2026-41651), a 12-year-old flaw in the
PackageKit daemon disclosed
by Deutsche Telekom's Red Team on 22 April 2026.
The lab includes three containers: a permissive-polkit CTF challenge, a hardened comparison target, and a full TOCTOU exploit reproduction that builds the vulnerable PackageKit from source and runs the actual CVE-2026-41651 root-cause exploit.
| Container | Port | Role | Exploit outcome |
|---|---|---|---|
pack2theroot-vuln | tcp/2222 (ssh) | PackageKit ≤ 1.3.4 + permissive polkit rule | Root via crafted RPM |
pack2theroot-patched | tcp/2223 (ssh) | Same, but polkit hardened | Exploit fails |
pack2theroot-exploit | — | Vulnerable PackageKit 1.3.4 built from source | Root via TOCTOU D-Bus race |
Plus:
docs/CHALLENGE.md)docs/HINTS.md)docs/SOLUTION.md)docs/diagrams/)pk-transaction.cThe exploit sends two async D-Bus InstallFiles() calls on the same
transaction: first with SIMULATE (bypasses polkit, queues a GLib idle
callback), then immediately with NONE + the malicious package (overwrites
the cached flags/paths before the idle fires). GLib's priority ordering
guarantees both D-Bus messages dispatch before the idle callback — making
this deterministic, not a timing race.
Known IOC (from the advisory):
journalctl --no-pager -u packagekit | grep -iE 'emitted_finished|pk_transaction_finished_emit|pk-transaction\.c:514'
git clone https://github.com/dinosn/pack2theroot-lab.git
cd pack2theroot-lab
docker compose build
docker compose up -d
ssh labuser@localhost -p 2222 # password: labuser
# then work through docs/CHALLENGE.md
The flag is at /root/flag.txt (mode 0400 root:root). Format:
PACK2THEROOT{...}
# Build and run — auto-mode executes the exploit immediately:
docker compose run --rm exploit
# Or manual mode:
docker compose run --rm --entrypoint /entrypoint.sh exploit
# Then inside the container:
su - victim -c /exploit/cve-2026-41651
The exploit builds two .deb packages in C (no external tools needed),
sends the two-call D-Bus race, and creates a SUID bash at
/tmp/.suid_bash. See docs/SOLUTION.md for the
full technical walkthrough.
Tear down:
docker compose down -v
pack2theroot-lab/
├── README.md
├── docker-compose.yml
├── docs/
│ ├── CHALLENGE.md # player brief
│ ├── HINTS.md # progressive hints (both paths)
│ ├── SOLUTION.md # full walkthrough + flag
│ ├── references.md # primary and secondary sources
│ ├── threat-model.md # attacker/defender assumptions
│ └── diagrams/ # SVG visual diagrams of the exploit flow
│ ├── 01-docker-build.svg
│ ├── 02-container-startup.svg
│ ├── 03-exploit-execution.svg
│ ├── 04-dbus-race-sequence.svg
│ ├── 05-attack-overview.svg
│ └── 06-auto-mode-full-run.svg
├── exploit/
│ ├── Dockerfile # builds vulnerable PackageKit 1.3.4 from source
│ ├── entrypoint.sh # starts dbus + polkitd + packagekitd
│ ├── Makefile
│ └── src/
│ └── cve-2026-41651.c # the TOCTOU exploit (public PoC)
├── vulnerable/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ └── policy/
│ └── 10-pack2theroot-lab-misconfig.rules # CTF challenge
├── patched/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ ├── local/ # drop a 1.3.5 backport RPM here (optional)
│ └── policy/
│ └── 10-pack2theroot-lab-hardened.rules # the fix
└── scripts/
├── exploit-check.sh # checks version, polkit, D-Bus, IOCs
├── check-version.sh # VULNERABLE vs PATCHED verdict
├── check-preconditions.sh # pkcon / polkit / D-Bus sanity
├── monitor-ioc.sh # tail journal for the advisory IOC
├── compare-env.sh # diff both containers
└── apply-polkit-mitigation.sh # interim hardening for unpatched hosts
The docs/diagrams/ directory contains SVG diagrams
showing the exploit flow. Open them in any browser:
See docs/CHALLENGE.md. TL;DR:
labuser (password labuser).rpm-build, rpmdevtools are installed).pkcon install-local --allow-untrusted.%post scriptlet runs as root. Read /root/flag.txt.Stuck? docs/HINTS.md has progressively more
explicit hints. Only open
docs/SOLUTION.md after you've either solved it or
genuinely need the write-up.
Inside either container, as any user:
/opt/lab/check-version.sh # VULNERABLE or PATCHED verdict
/opt/lab/check-preconditions.sh # exposure self-check
/opt/lab/exploit-check.sh # version + polkit + D-Bus + IOC check
/opt/lab/monitor-ioc.sh # tail the journal for the IOC
From the host, side-by-side:
./scripts/compare-env.sh
Apply the interim polkit mitigation to a vulnerable container:
docker exec pack2theroot-vuln /opt/lab/apply-polkit-mitigation.sh
# Then retry the exploit — should now fail.
The patched image handles PackageKit version status honestly:
/etc/pack2theroot-banner) stating
whether the installed version is the real patch or a distro placeholder.To build a version-complete patched image once your distro publishes
the backport, drop the RPM(s) into patched/local/ and rebuild:
docker compose build --build-arg USE_LOCAL_RPM=1 patched
--privileged to run the full
dbus/polkitd/packagekitd stack. Only run it in isolated environments.labuser) is intentionally weak. Never reuse it.Primary sources:
CVE-2026-41651:
https://nvd.nist.gov/vuln/detail/CVE-2026-4165176cfb675:
https://github.com/PackageKit/PackageKit/commit/76cfb675fb31acc3ad5595d4380bfff56d2a8697See docs/references.md for secondary coverage
and distribution trackers.
MIT — see LICENSE.
| Field | Value |
|---|
| CVE | CVE-2026-41651 |
| Name | Pack2TheRoot |
| Class | Local Privilege Escalation (TOCTOU / CWE-367) |
| Component | PackageKit daemon (pk-transaction.c) |
| Vulnerable range | >= 1.0.2 and <= 1.3.4 (12 years of releases) |
| Fixed in | PackageKit 1.3.5 (commit 76cfb675, released 2026-04-22) |
| CVSS 3.1 | 8.8 (HIGH) — AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Pre-conditions | Local unprivileged shell on a host running PackageKit |
| Discoverer | Deutsche Telekom Security GmbH — Red Team |
| Bug | Location | Effect |
|---|
| BUG 1 | InstallFiles() (~line 4036) | Overwrites cached_transaction_flags and cached_full_paths unconditionally — no state guard |
| BUG 2 | pk_transaction_set_state() (~lines 876–881) | Silently drops backward state transitions instead of erroring — flags already poisoned |
| BUG 3 | pk_transaction_run() (~lines 2273–2277) | Reads cached_transaction_flags at dispatch time, not at authorization time |
| Bypass | SIMULATE flag (~lines 2893–2900) | PK_TRANSACTION_FLAG_SIMULATE (0x4) bypasses polkit entirely |
| Diagram | Description |
|---|
01-docker-build.svg | Two-stage Docker build — compiling vulnerable PackageKit |
02-container-startup.svg | Container launch — dbus, polkitd, packagekitd startup |
03-exploit-execution.svg | Step-by-step exploit output with annotated phases |
04-dbus-race-sequence.svg | D-Bus sequence diagram — how BUG 1/2/3 chain with GLib priority |
05-attack-overview.svg | High-level attack flow: attacker vs system with timeline |
06-auto-mode-full-run.svg | Complete --auto mode terminal — container start to root shell |