
Proof-of-concept exploit for CVE-2026-41651, a PackageKit TOCTOU local privilege escalation, with technical analysis, detection logic, and remediation guidance.
Classification: Purple Team Assessment Artifact
Authorized use only. This document and the accompanying test script are intended solely for internal security validation on systems where written authorization has been obtained.

| Field | Value |
|---|---|
| CVE ID | CVE-2026-41651 |
| CWE | CWE-367 — Time-of-check Time-of-use (TOCTOU) Race Condition |
| Component | PackageKit (packagekitd, D-Bus service) |
| Affected | PackageKit ≤ 1.3.4 |
| Fixed in | PackageKit 1.3.5 |
| Impact | Local Privilege Escalation → root |
| Attack Vector | Local / D-Bus (unprivileged user session) |
| Disclosed | 2026-04-22 (Deutsche Telekom Red Team) |
| Advisory | GHSA-f55j-vvr9-69xv |
| Fix commit | 76cfb675fb31acc3ad5595d4380bfff56d2a8697 |
PackageKit is a D-Bus abstraction layer for system package management, present by default on GNOME-based desktops across Debian, Ubuntu, Fedora, RHEL, SUSE, and Arch Linux. Because it mediates privileged package operations on behalf of unprivileged users, a flaw in its authorization flow has system-wide root impact.
pk-transaction.c (pre-1.3.5) did not enforce a state guard on action method re-invocation. A D-Bus client could call InstallFiles (or other action methods) multiple times on the same transaction object after it had already transitioned out of PK_TRANSACTION_STATE_NEW.
Attacker (unprivileged)
│
├─① CreateTransaction() → PackageKit returns transaction object path (tid)
│
├─② InstallFiles(tid, FLAG_SIMULATE=4, [dummy.pkg])
│ PackageKit queues a polkit authorization check for dummy.pkg.
│ No installation occurs yet — SIMULATE means dry-run only.
│
├─③ InstallFiles(tid, FLAG_NONE=0, [payload.pkg]) ← TOCTOU window
│ Re-invokes on the same tid before auth resolves.
│ Vulnerable versions overwrite the queued parameters with payload.pkg.
│
└─④ polkit grants authorization (user approved or auto-authorized)
packagekitd installs payload.pkg as root.
payload postinst/post script: install -m 4755 /bin/bash /tmp/.suid_bash
Attacker executes /tmp/.suid_bash -p → root shell.
Steps ② and ③ are sent as non-blocking async D-Bus calls on the same connection and flushed in a single write. The two messages arrive at packagekitd before it can process ② and advance the state machine, leaving the TOCTOU window open. The fix in 1.3.5 adds an explicit state check that returns PK_TRANSACTION_ERROR_INVALID_STATE on any re-invocation after PK_TRANSACTION_STATE_NEW.
File: cve-2026-41651-purpleteam.py
Language: Python 3
Dependencies: python3-gi (GObject introspection / GLib/Gio bindings)
Demonstrates exploitability of CVE-2026-41651 on a prepared test system for the purposes of:
| Phase | Action |
|---|---|
| Setup | Probes /proc/mounts to select a SUID/exec-capable drop directory, sets umask(022), applies explicit chmod on all build artifacts, then builds a dummy and a payload package in /tmp |
| Exploit | Opens a system D-Bus connection, creates a PackageKit transaction, fires the two-call race |
| Payload | Package post-install script copies /bin/bash to <drop_dir>/.suid_bash with mode 04755, owner root. Drop directory is resolved at runtime (see Hardened Environment Support) |
| Escalation | Polls for the SUID binary (90 s timeout), then execls into it with -p for a root shell |
| Cleanup | Removes the temporary .deb/.rpm files on exit (success or failure) |
Default-hardened systems can block the exploit at two independent points. The script handles both automatically.
027 / 077)A process-inherited umask of 027 or 077 causes mkdir() to produce 750 or 700 directories. Both dpkg-deb and rpmbuild must traverse the full build tree — if any directory is unreadable the build fails silently.
Fix applied: os.umask(0o022) is called at startup before any file or directory is created. Additionally, every directory and file in the build tree receives an explicit chmod immediately after creation (0o755 for directories and scripts, 0o644 for data files), so the correct permissions are guaranteed regardless of the inherited umask.
nosuid / noexec mount flags on /tmp| Flag | Effect on exploit |
|---|---|
nosuid | Kernel silently strips the SUID bit from any file stored on that filesystem — the copied bash never becomes root |
noexec | The SUID binary cannot be executed at all |
Fix applied: At startup, _find_suid_dir() reads /proc/mounts and tests candidate directories in preference order until it finds one whose filesystem has neither flag set. The resolved path is then baked into the payload's post-install script and used for polling and execution.
| Priority | Candidate | Typical hardening |
|---|---|---|
| 1 | /var/tmp | Rarely carries nosuid/noexec; survives reboots |
| 2 | /dev/shm | tmpfs, usually permissive; cleared on reboot |
| 3 | /tmp | Often hardened on CIS/STIG systems |
| 4 | $HOME | Last resort; always writable by the user |
If all candidates are blocked the script exits with a clear error rather than silently failing.
| Distribution Family | Package Tool | Tested |
|---|---|---|
| Debian / Ubuntu | dpkg-deb | ✓ |
| RHEL / Fedora | rpmbuild | ✓ |
| SUSE / openSUSE | rpmbuild | ✓ |