CVE-2026-8933:一次安全加固是如何制造出提权漏洞的
漏洞分析
CVE-2026-8933:Canonical 将 snap-confine 从 setuid-root 改为 set-capabilities 最小权限模型,却引入了 FUSE 竞争 + 符号链接竞争两条攻击路径。攻击者可从普通用户提权到 root,影响 Ubuntu 24.04/25.10/26.04 默认安装。老的 setuid-root 版本反而不受影响
一、从一次"安全加固"说起 ------------- 说实话,刚看到这个漏洞的时候我愣了一下。 CVE-2026-8933 是一个 Ubuntu snap-confine 的本地权限提升漏洞。发现它的是 Qualys 的 Threat Research Unit。CVSS 7.8,攻击复杂度低,权限要求低,影响范围是 Ubuntu Desktop 24.04、25.10 和 26.04 的默认安装。 这些表面信息都没什么特别的。 真正有意思的是这个漏洞的成因——它不是出在没人维护的老代码里,不是出在某个边缘功能模块里。它出在一次**安全加固**上。Canonical 想把 snap-confine 从 set-uid-root 改成 set-capabilities,目的是遵循最小权限原则,减少不必要的 root 权限。结果呢? 加固之后反而能从普通用户直接提权到 root。 而且更有意思的是,**老的 set-uid-root 版本完全不受影响。** 你加固了,反而出事了。不加固,反而没事。 **一句话:最小权限原则的实践引入了一组竞态条件,最终抵消了安全收益。** - - - - - - ### 背景知识 snap-confine 是 snapd 内部的一个核心组件。它的职责是在 snap 应用启动前构建一个隔离的执行环境——创建 mount namespace、准备 rootfs、设置必要的绑定挂载。只有 snapd 安装了的系统上才有它。 传统上 snap-confine 是一个 **set-uid-root** 二进制文件。普通用户执行它时,它的 effective UID 会被提升为 root,所有操作都以 root 的身份执行。 Canonical 觉得这样不太对——一个做沙箱初始化的程序,没必要全程以 root 身份运行。于是在某个版本中,他们把 snap-confine 从 set-uid-root 迁移到了 **set-capabilities** 模式。迁移之后,snap-confine 运行时的 effective UID 是调用用户的非特权 UID,但它保留了接近 root 的 capabilities。 Ubuntu 26.04 加载的 capabilities 清单: ```php cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_setgid,cap_setuid, cap_sys_chroot,cap_sys_ptrace,cap_sys_admin,cap_sys_resource=p ``` 注意 `cap_dac_override` 这项——后面会看到它的重要性。 24.04 的 capabilities 少三个:cap\_setgid、cap\_setuid、cap\_sys\_resource。不过少这几个并不影响漏洞利用。  - - - - - - 二、两个 race condition ------------------- 漏洞的核心位于 `sc_bootstrap_mount_namespace` 函数中。这个函数负责创建临时 scratch 目录,在里面构建 snap 应用的 rootfs。 代码路径上有两个关键的时间窗口: **窗口1 — 目录级别。** 第 511 行调用 `mkdtemp()` 在 `/tmp` 下创建了 `snap.rootfs_XXXXXX` 临时目录,然后到第 362 行调用 `chown(scratch_dir, 0, 0)` 把目录所有权改给 root 之前,**这个目录属于调用它的非特权用户**。 ```php 511: if (mkdtemp(scratch_dir) == NULL) { ... 362: if (chown(scratch_dir, 0, 0) < 0) { ``` **窗口2 — 文件级别。** 第 450 行调用 `open(full_path, O_CREAT | O_TRUNC, 0644)` 在 scratch 目录中创建一个文件,到第 454 行 `fchown(fd, 0, 0)` 将文件所有权改为 root 之前,**这个文件也属于非特权用户**。 ```php 450: int fd = open(full_path, O_CREAT | O_TRUNC, 0644); ... 454: if (fchown(fd, 0, 0) < 0) { ``` 看起来好像没什么大不了的——临时归你,然后改给 root,中间只有一瞬间。但这里藏了两个致命细节。 第一,第 450 行的 `open()` 没有指定 `O_NOFOLLOW`。这意味着如果 `full_path` 是一个符号链接,open() 会跟着它走到目标文件,而不是报错。第二,snap-confine 有 `cap_dac_override`,所以即使目标文件是 root 所有的系统路径,它也能写入。 这两个细节组合起来,让攻击者有了一个"趁目录和文件暂时归自己所有的时候塞入符号链接,让 snap-confine 帮自己在任意路径创建文件"的机会。 不过事情没那么简单——snap-confine 对 scratch 目录做了几层 mount 保护。  - - - - - - 三、第一关:FUSE 绕 mount ------------------ snap-confine 在第 511 行创建 scratch 目录后,紧接着在第 523 到 535 行对这个目录做了三件事: ```php 523: sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL); 530: sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL); 535: sc_do_mount("none", scratch_dir, "tmpfs", 0, "uid=0,gid=0"); ``` bind mount → unbindable mount → tmpfs mount。三层操作之后,这个目录从外部已经完全不可见了——它被一个 root 所有的 tmpfs 覆盖了。攻击者想趁文件还归自己的时候放符号链接?连目录都访问不了。 这套设计显然是有意识的安全防护:scratch 目录的内容不应该被外部进程干扰。如果是在 set-uid-root 模式下,这个防护是够的。但 set-capabilities 模式下,effective UID 是用户自己,情况就不同了。 攻击者的解法是用 FUSE 文件系统抢先挂载。 具体操作是这样的:在 `mkdtemp()` 刚创建好 scratch 目录、chown 还没执行的窗口里,攻击者用 `fusermount` 把一个 FUSE 文件系统挂载到这个目录上。然后当 snap-confine 执行第 523-535 行的 mount 操作时,这些 mount 都会落在 FUSE 文件系统之上——攻击者随后用 `fusermount -u -z`(`-z` 是延迟卸载)把 FUSE 文件系统卸载掉,连带上面 snap-confine 挂载的所有文件系统一起"脱落"。 到第 450 行的 `open()` 执行时,`/tmp/snap.rootfs_XXXXXX` 已经恢复为第 511 行 mkdtemp 创建时的原始状态,并且仍然属于非特权用户。 攻击者可以在里面放符号链接了。 这里有一个值得注意的点:攻击者需要和一个具有接近 root capabilities 的程序赛跑。race condition 的窗口确实很短,但 Qualys 的 PoC 证明它是实际可利用的——两个 race 都跑了足够多的轮次来捕捉窗口。 **说白了:mount 隔离在 set-capabilities 模式下失效了,因为用户可以通过 FUSE 把 mount 操作"弹走"。** - - - - - - 四、第二关:缺 O\_NOFOLLOW 的 open() ---------------------------- 解决了目录访问问题,第二步是在 snap-confine 创建具体文件的时候做手脚。 攻击者在 scratch 目录中预先放置了一个符号链接。符号链接的名字需要匹配 snap-confine 后续要创建的文件名——Qualys 的 PoC 针对的是 firefox snap 会创建的文件 `default256.png`。链接的目标指向攻击者想要写入的任意路径,比如 `/run/udev/rules.d/` 下的某个文件。 当 snap-confine 执行到第 450 行时: ```c int fd = open(full_path, O_CREAT | O_TRUNC, 0644); ``` `full_path` 是 `scratch_dir + "/" + "default256.png"`。因为 `open()` 没有用 `O_NOFOLLOW`,它跟着符号链接走到了攻击者指定的目标路径。 这时 snap-confine 恰好有 `cap_dac_override`——所以即使目标路径是像 `/run/udev/rules.d/` 这样的受保护目录,open() 也能通过 DAC 检查,成功创建或覆写文件。 这还没完。第 454 行的 `fchown(fd, 0, 0)` 会把文件的所有权改给 root,理论上攻击者就写不了了。但攻击者在第二个 race condition 窗口里做了另一件事:在第 450 行的 `open()` 刚执行完、第 454 行的 `fchown()` 还没执行的间隙,**把刚创建的文件的权限改成了 0666**。 这样即使文件被 chown 给了 root,由于权限是全局可写的,攻击者后续仍然可以继续修改文件内容。 一个"不必以 root 运行"的加固改动,保留了 `cap_dac_override`。而这个 capability 的保留,加上 `open()` 没加 `O_NOFOLLOW`,刚好凑齐了任意文件创建的条件。 不能说 `cap_dac_override` 本身是错的——它是 snap-confine 执行某些特权操作所必需的。但问题在于 set-capabilities 模式的"部分特权"模型创造了一个模糊地带:你有能力穿过 DAC 写文件,但你的临时文件又不是直接归 root 所有的。这个"半特权"状态正是 race condition 的温床。 **一句话:cap\_dac\_override + 缺失的 O\_NOFOLLOW + 临时文件初始归用户所有,三个条件缺一个都凑不出任意文件创建。** - - - - - - 五、第三关:AppArmor 里藏着一条漏洞 ---------------------- 文件是创建了,但 snap-confine 本身运行在一个高度限制的 AppArmor 配置文件下。 AppArmor 会限制 snap-confine 能写哪些路径。即使有 `cap_dac_override`,AppArmor 的限制是独立于 DAC 的——你不在这个白名单路径列表里,就是写不了。 到这里为止,攻击者还需要回答一个问题:**写到哪里?** 一个直觉的目标是 `/etc/` 下的文件——如果能改写 cron 配置或者 SSH 配置,就能直接拿到持久的 root 访问。但 AppArmor 把 `/etc/` 写死了。 Qualys 的研究人员仔细翻了一遍 snap-confine 的 AppArmor 配置,发现了这么一条规则: ```php /run/udev/** rw, ``` **rw**。读写权限。在 `/run/udev/` 下,snap-confine 可以写任何文件。 为什么这条规则会存在?我猜测是因为 snap-confine 在构建 sandbox 的过程中需要与 systemd-udevd 交互——比如设备节点的创建和管理。不管是出于什么原因,这条规则成了整条攻击链中绕不过去的一环。 **对比 set-uid-root 模式:** 如果在 set-uid-root 模式下,即使 AppArmor 允许写入 /run/udev/,由于进程以 root UID 运行,所有操作"本来就是 root"。而在 set-capabilities 模式下,effective UID 是非特权用户但 AppArmor 允许写 → 攻击者可以利用这个间隙写入恶意内容。AppArmor 和 UID 之间的"权限差"才是问题。 - - - - - - 六、第四关:udev 规则执行 --------------- 现在攻击者可以在 `/run/udev/rules.d/` 下创建文件了。下一步利用的是 udev 的规则执行机制。 Linux 的 udev(`systemd-udevd`)会在设备事件发生时扫描 `/run/udev/rules.d/` 下的规则文件,根据规则内容执行对应的操作。规则文件的内容大致是这种格式: ```php PROGRAM="/bin/sh -c id>>/tmp/pwned" ``` 攻击者把这样的内容写入 `.rules` 文件,然后需要触发 udev 去执行它。 触发方式出乎意料地简单:**挂载或卸载一个 FUSE 文件系统。** 当 FUSE 文件系统被挂载/卸载时,内核会产生设备事件(uevent),`systemd-udevd` 收到事件后会扫描规则目录并执行匹配的规则。 而且 `systemd-udevd` 是以完整的 root 权限运行的。这意味着规则中的 `PROGRAM` 指定的命令也会以 root 身份执行。 整条攻击链到这一步就完成了。 - - - - - - 七、PoC 验证 -------- Ubuntu 26.04 上从普通用户提权到 root效果:  可以看到两件事:第一,`/run/udev/rules.d/` 下出现了一个 `default256.png.rules` 文件,权限是 `-rw-rw-rw-`(0666,第二轮 race condition 放宽权限的结果)。第二,`/tmp/pwned` 文件的内容证明 shell 命令已以 root 身份执行(不过这是漏洞公布者展示的漏洞效果,目前exp还未公布,主要是学漏洞思路)  - - - - - - 八、时间线与修复 -------- | 时间 | 事件 | |---|---| | 2026-04-22 | Qualys TRU 向 Ubuntu 安全团队发送漏洞公告和完整利用代码 | | 2026-07-13 | Ubuntu 安全团队将补丁发送至 linux-distros 邮件列表 | | 2026-07-14 | Qualys 将漏洞公告发送至 linux-distros 邮件列表 | | 2026-07-21 14:00 UTC | 协调公开披露 | 从报告到公开,刚好三个月。时间线算是相当标准的 90 天协调披露周期。 受影响的版本范围是 snapd `>= 2.75.0` 且 `< 2.76.1`。修复版本 snapd 2.76.1 已在公开日发布。Ubuntu 安全团队也同步推送了对应发行版的更新包: - Ubuntu 26.04 LTS: snapd 2.76+ubuntu26.04.3 - Ubuntu 24.04 LTS: snapd 2.76+ubuntu24.04.1 - Ubuntu 22.04 LTS: snapd 2.76+ubuntu22.04.1 如果你在运行 Ubuntu Desktop 并且启用了 snap,建议立即升级: ```bash sudo snap refresh snapd # 或者 sudo apt update && sudo apt upgrade snapd ``` 需要注意的是这个漏洞影响的是 **Ubuntu Desktop** 版本。Ubuntu Server 默认不安装 snapd 桌面组件,理论上不在受影响范围内,但如果你在 Server 上手动启用了 snap,也需要更新。 - - - - - - 九、检测规则 ------ 如果暂时无法修复,可以通过以下方式检测和缓解。 ### 系统级检测 检查 snap-confine 的模式和版本: ```bash # 检查 snap-confine 是否为 set-capabilities 模式 getcap /usr/lib/snapd/snap-confine # 预期输出无 set-capabilities(已修复),或有 cap_xxx=p(受影响) # 检查 snapd 版本 snap version | grep snapd # 受影响:>= 2.75.0 且 < 2.76.1 ``` ### 日志检测 在 `/var/log/syslog` 或 `journalctl` 中搜索可疑的 udev 规则写入和触发事件: ```bash # 搜索非预期的 rules 文件写入 journalctl -u systemd-udevd --since "2026-07-01" | grep -i "rules" # 检查 /run/udev/rules.d/ 下的异常规则文件 ls -la /run/udev/rules.d/ # 正常的规则文件由 netplan 等系统组件管理 # 非预期的规则文件通常以随机数字开头,如 2064396357-default256.png.rules ``` ### Auditd 检测规则 如果启用了 auditd,可以添加以下规则来监控 `/run/udev/rules.d/` 的写入: ```text -w /run/udev/rules.d/ -p wa -k snap-confine-udev-rule-creation ``` 触发告警的条件:非 root 进程在 `/run/udev/rules.d/` 下创建或修改文件。 ### 临时缓解 如果无法立即更新 snapd,可以考虑: 1. 限制对 `fusermount` 的访问。不过这个影响面较大,因为 FUSE 是很多桌面应用的依赖。 2. 监控 `/tmp` 下 `snap.rootfs_*` 目录的行为。当 `snap.rootfs_` 目录被创建后立即被 FUSE 挂载——这是攻击的核心前兆。 - - - - - - 十、本质问题 ------ 回头看这个漏洞,我认为值得拿出来说的不是那几步 race condition 的利用技巧,而是更深层的问题:**安全加固本身是需要被审计的。** Canonical 做 set-uid-root → set-capabilities 迁移的动机是好的——减少不必要的 root 权限,符合最小权限原则。但这次迁移只考虑了"权限减少"这一个维度,没有重新审视代码路径中的隐式假设。 在 set-uid-root 模式下,snap-confine 以 root 运行,mkdtemp 创建的文件和目录直接归 root 所有——非特权用户无法介入。这个隐式假设在 set-capabilities 模式下被打破了:effective UID 不再是 root,但 capabilities 保留了原来需要高权限才能执行的特权操作。 这个"半特权"状态在安全领域很常见也很有用——但它要求代码路径上的每一步都要重新考虑"现在谁拥有这个资源"的问题。 具体到这个漏洞,迁移之后应该重新审视什么? - `open()` 是否应该加上 `O_NOFOLLOW`? - 临时目录和文件是否应该在创建时就通过 `mk[s]temp` 的变体指定所有者? - AppArmor 配置中允许 `rw` 的路径是否需要缩小? 这些都不是复杂的问题,但在"只关注权限减少、不关注语义变化"的迁移中,它们被漏掉了。 说实话,安全加固引入漏洞的案例并不少。从 Android 的 SEAndroid 策略误配置到 Kubernetes 的 PodSecurityPolicy 迁移,再到各类 Namespace 隔离的边界收缩——"加固漏了"几乎是所有安全工程团队都会遇到的问题。 **本质不是"不该加固",而是"加固时你以为只改了一点,实际上整个执行模型的假设都变了。"** 这个案例最有价值的不是那四步攻击技术,而是一个反思:下次你做安全加固的时候,除了问"这个代码现在以什么权限运行",还可以多问一句——"这个代码之前以什么权限运行,那些权限代入了什么隐式假设?"
发表于 2026-08-26 09:00:00
阅读 ( 11014 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
zee
10 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!