分类: 紫队评估工件
仅限授权使用。 本文档及随附的测试脚本仅用于在已获得书面授权的系统上进行内部安全验证。

| 字段 | 值 |
|---|---|
| CVE ID | CVE-2026-41651 |
| CWE | CWE-367 — 检查时间与使用时间(TOCTOU)竞态条件 |
| 组件 | PackageKit(packagekitd,D-Bus 服务) |
| 受影响版本 | PackageKit ≤ 1.3.4 |
| 修复版本 | PackageKit 1.3.5 |
| 影响 | 本地权限提升 → root |
| 攻击向量 | 本地 / D-Bus(非特权用户会话) |
| 披露日期 | 2026-04-22(德国电信红队) |
| 安全公告 | GHSA-f55j-vvr9-69xv |
| 修复提交 | 76cfb675fb31acc3ad5595d4380bfff56d2a8697 |
PackageKit 是系统包管理的 D-Bus 抽象层,在基于 GNOME 的桌面环境中默认存在,覆盖 Debian、Ubuntu、Fedora、RHEL、SUSE 和 Arch Linux。由于它代表非特权用户执行特权包操作,其授权流程中的缺陷会对整个系统产生 root 级影响。
pk-transaction.c(1.3.5 之前版本)未对操作方法重复调用实施状态保护。D-Bus 客户端可以在事务对象已从 PK_TRANSACTION_STATE_NEW 状态转换后,多次调用同一事务对象上的 InstallFiles(或其他操作方法)。
攻击者(非特权)
│
├─① CreateTransaction() → PackageKit 返回事务对象路径 (tid)
│
├─② InstallFiles(tid, FLAG_SIMULATE=4, [dummy.pkg])
│ PackageKit 为 dummy.pkg 排队 polkit 授权检查。
│ 此时不会发生安装 — SIMULATE 仅表示试运行。
│
├─③ InstallFiles(tid, FLAG_NONE=0, [payload.pkg]) ← TOCTOU 窗口
│ 在授权解析之前对同一 tid 重新调用。
│ 存在漏洞的版本会用 payload.pkg 覆盖排队的参数。
│
└─④ polkit 授予授权(用户批准或自动授权)
packagekitd 以 root 身份安装 payload.pkg。
payload 的 postinst/post 脚本:install -m 4755 /bin/bash /tmp/.suid_bash
攻击者执行 /tmp/.suid_bash -p → root shell。
步骤②和③作为非阻塞异步 D-Bus 调用在同一连接上发送,并在一次写入中刷新。两条消息在 packagekitd 处理②并推进状态机之前到达,从而留下 TOCTOU 窗口。1.3.5 中的修复添加了显式状态检查,在 PK_TRANSACTION_STATE_NEW 之后的任何重复调用都会返回 PK_TRANSACTION_ERROR_INVALID_STATE。
文件: cve-2026-41651-purpleteam.py
语言: Python 3
依赖: python3-gi(GObject 内省 / GLib/Gio 绑定)
在准备好的测试系统上演示 CVE-2026-41651 的可利用性,目的如下:
| 阶段 | 操作 |
|---|---|
| 准备 | 探测 /proc/mounts 以选择 SUID/可执行权限的投放目录,设置 umask(022),对所有构建产物应用显式 chmod,然后在 /tmp 中构建 dummy 和 payload 包 |
| 利用 | 打开系统 D-Bus 连接,创建 PackageKit 事务,触发两次调用的竞态 |
| Payload | 包安装后脚本将 /bin/bash 复制到 <drop_dir>/.suid_bash,权限为 04755,属主为 root。投放目录在运行时解析(参见加固环境支持) |
| 提权 | 轮询 SUID 二进制文件(90 秒超时),然后以 -p 参数 execl 进入以获得 root shell |
| 清理 | 退出时(无论成功或失败)删除临时 .deb/.rpm 文件 |
默认加固的系统可以在两个独立点阻止该利用。脚本会自动处理这两种情况。
027 / 077)进程继承的 umask 为 027 或 077 会导致 mkdir() 生成 750 或 700 权限的目录。dpkg-deb 和 rpmbuild 都必须遍历完整的构建树 — 如果任何目录不可读,构建将静默失败。
已应用的修复: 在创建任何文件或目录之前,启动时调用 os.umask(0o022)。此外,构建树中的每个目录和文件在创建后立即接收显式 chmod(目录和脚本为 0o755,数据文件为 0o644),因此无论继承的 umask 如何,都能保证正确的权限。
/tmp 上的 nosuid / noexec 挂载标志| 标志 | 对利用的影响 |
|---|---|
nosuid | 内核静默剥离存储在该文件系统上任何文件的 SUID 位 — 复制的 bash 永远不会成为 root |
noexec | SUID 二进制文件根本无法执行 |
已应用的修复: 启动时,_find_suid_dir() 读取 /proc/mounts 并按优先级顺序测试候选目录,直到找到文件系统上两个标志均未设置的目录。解析后的路径随后被写入 payload 的安装后脚本,并用于轮询和执行。
| 优先级 | 候选目录 | 典型加固情况 |
|---|---|---|
| 1 | /var/tmp | 很少带有 nosuid/noexec;重启后保留 |
| 2 | /dev/shm | tmpfs,通常权限宽松;重启后清除 |
| 3 | /tmp | 在 CIS/STIG 系统上通常已加固 |
| 4 | $HOME | 最后手段;用户始终可写 |
如果所有候选目录都被阻止,脚本会以明确的错误退出,而不是静默失败。
| 发行版系列 | 包工具 | 已测试 |
|---|---|---|
| Debian / Ubuntu | dpkg-deb | ✓ |
| RHEL / Fedora | rpmbuild | ✓ |
| SUSE / openSUSE | rpmbuild | ✓ |
IOC 按从通用/基础设施级别到脚本特定痕迹的顺序列出。基于通用指标构建的检测将捕获此 CVE,无论使用哪种 PoC 变体。
| IOC | 路径 / 模式 | 备注 |
|---|---|---|
| 世界可写目录中的 SUID 二进制文件 | <drop_dir>/.suid_bash — 投放目录为 /var/tmp、/dev/shm、/tmp 或 $HOME,取决于挂载标志 | 脚本选择第一个没有 nosuid/noexec 的目录;扩大检测范围以覆盖全部四个候选目录 |
/tmp 中的临时包文件 | /tmp/*.deb、/tmp/*.rpm | 由非特权用户构建;合法的包操作使用 /var/cache 或下载路径 |
| dpkg 构建树 | /tmp/pkbuild_*、/tmp/build_* | dpkg-deb -b 的暂存目录 |
/tmp 中的 rpmbuild 树 | /tmp/rpmbuild_* | 在 ~/rpmbuild 之外调用 rpmbuild 的暂存目录 |
/tmp 中的 postinst / %post 脚本 | /tmp/*/DEBIAN/postinst、/tmp/*/SPECS/*.spec | 复制或 chmod 系统二进制的脚本是高置信度指标 |
| IOC | 详情 |
|---|---|
由非 root、非 apt 进程生成的 dpkg-deb | 父进程是用户 Python/shell 进程,而非 apt、dpkg 或 unattended-upgrades |
由非特权用户生成的 rpmbuild | 没有打包相关父进程(mock、koji、CI 运行器)时属于异常 |
packagekitd 从 /tmp 路径生成 /bin/sh 或 /bin/bash | packagekitd 运行 postinst 脚本;源路径在 /tmp 中即为异常 |
UID ≠ EUID 的 bash 进程 | SUID 执行;EUID=0、UID=<攻击者> |
bash -p 调用 | -p 标志在设置 SUID 时启用特权模式 — 很少被合法使用 |
短生命周期的 .suid_bash 进程祖先链 | 祖父进程为 packagekitd,父进程为攻击者的 shell |
| IOC | 详情 |
|---|---|
在同一对象路径上两次调用 org.freedesktop.PackageKit.Transaction.InstallFiles | 两次调用在毫秒内到达;合法客户端每个事务只调用一次 |
在同一 tid 上 FLAG_SIMULATE (4) 后紧跟 FLAG_NONE (0) | 此序列没有合法用例 |
CreateTransaction → 快速双重 InstallFiles 模式 | 可通过 D-Bus 监视器(dbus-monitor --system)或审计 dbus 规则检测 |
使用 /tmp 下文件路径的 InstallFiles | 合法的 GUI 包安装程序(GNOME Software、Discover)使用 $HOME 或 /var/cache 中的路径 |
启用方式:
auditctl -a always,exit -F arch=b64 -S all -F path=/usr/bin/packagekitd
或在/etc/audit/rules.d/中使用以下规则:
# 检测所有候选投放目录中的 SUID 文件创建
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F a2&04000 -F dir=/tmp -k suid_drop
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F a2&04000 -F dir=/var/tmp -k suid_drop
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F a2&04000 -F dir=/dev/shm -k suid_drop
# 检测非 root 用户执行 dpkg-deb / rpmbuild
-w /usr/bin/dpkg-deb -p x -k pkg_build_nonroot
-w /usr/bin/rpmbuild -p x -k pkg_build_nonroot
# 检测从任何候选投放目录执行 SUID bash
-a always,exit -F arch=b64 -S execve -F dir=/tmp -F uid!=0 -F euid=0 -k priv_esc_drop
-a always,exit -F arch=b64 -S execve -F dir=/var/tmp -F uid!=0 -F euid=0 -k priv_esc_drop
-a always,exit -F arch=b64 -S execve -F dir=/dev/shm -F uid!=0 -F euid=0 -k priv_esc_drop
| IOC | 详情 |
|---|---|
dpkg/rpm 历史中名为 pk-dummy-* 或 pk-payload-* 的包 | 脚本特定,但变体会使用不同名称 |
从本地文件(/tmp/*.deb 或 /tmp/*.rpm)安装的包 | dpkg -l 或 rpm -qa --last — 查找没有仓库来源的包 |
| 没有变更日志 / 空描述的已安装包 | 精心构造的包使用最小元数据;dpkg -s <pkg> 或 rpm -qi <pkg> |
| 安装后立即删除或从未出现在包数据库中的包 | 失败的利用尝试可能留下部分状态 |
本地文件安装的 packagekit 事务日志条目 | /var/log/PackageKit/transactions.db 或 journald packagekitd 条目 |