Shikata Ga Nai

Private? There is no such things.

Lab: Password reset broken logic — 解説と手順(APPRENTICE)

Hello there, ('ω')ノ

1. 概要(このラボで何を学ぶか)

このラボでは、
パスワードリセット機能のロジックが壊れている(不完全)ことで、

  • リセットトークンを持っていない他人のアカウントのパスワードを変更できてしまう

という脆弱性を学びます。

具体的には:

  1. 自分のユーザで普通に「パスワードリセット」を体験し、
  2. Burp で リセット時のリクエスト構造 を観察して、
  3. トークンが実際には検証されていないことを確認し、
  4. username だけ carlos に書き換えてパスワードを上書きする

という流れで、Carlos のアカウント乗っ取り → My account へアクセスしてラボをクリアします。


2. 前提知識

  • パスワードリセットの正しい流れ

    1. ユーザ名/メールを入力
    2. メールに一意のトークン付き URL が届く
    3. その URL を踏んだ人だけがパスワードを変更できる(トークン検証必須)
  • トークン検証が抜けていると何が起こるか
    → 「誰でも POST で username を差し替えれば、その人のパスワードを変えられる」

  • Burp での作業

    • Proxy > HTTP history でリクエスト調査
    • Repeater でリクエストをコピー&改変して再送

3. なぜこの手順で進めるのか(考え方)

  1. まずは 自分のユーザで正常なパスワードリセットの流れを把握 する
    → どんな URL・パラメータ・トークンが使われるかを理解。

  2. 次に、Burp で パスワード変更の POST リクエスト を観察
    → クエリストリング・ボディの両方にある temp-forgot-password-token の挙動を確認。

  3. トークンを削除してもリセットできてしまうか? を実験
    → ここで「実はトークンが全く検証されていない」という壊れたロジックを確信。

  4. 最後に、usernamecarlos に書き換えて同じ構造のリクエストを送信
    → トークンなしでもターゲットユーザのパスワードが変えられてしまう。


4. 実際の手順(1アクションごとに「なぜ」を説明)

ここから先は、ラボ環境のみで実行してください。


ステップ 1 — 自分のユーザでパスワードリセットの流れを把握

  1. 画面から 「Forgot your password?」 をクリック
  2. 自分のユーザ名 wiener を入力し、送信
  3. 「Email client」ボタンからメールボックスを開き、
    パスワードリセットメール内のリンクをクリック
  4. 表示されたページで 新しいパスワード を設定し、送信(どんな値でもよい)

なぜ?
- ここで「正常なフロー」を体験しておくことで、後で Burp でそのリクエストを見た時に
「どれがトークンか」「どこに何が入っているか」が理解しやすくなります。


ステップ 2 — HTTP history でパスワードリセット関連リクエストを確認

  1. Burp → Proxy > HTTP history を開く
  2. POST /forgot-password?temp-forgot-password-token=... のようなリクエストを探す
    • URL クエリ:temp-forgot-password-token=xxxxx
    • ボディ:username=wiener&new-password-1=...&new-password-2=...
  3. このリクエストを Send to Repeater

ポイント
- メール内のリンクに含まれていた「一時トークン」が、
URL のクエリパラメータとして渡されている。
- 同時に、フォームから送られる hidden input 等 にも同じトークンが含まれていることが多い。


ステップ 3 — Repeater でトークンを削除しても動くかをテスト(ロジック崩壊の確認)

Repeater に送った自分のパスワードリセットリクエストを編集します。

  1. URL 部分から temp-forgot-password-token の値を 空にする
    • 例:/forgot-password?temp-forgot-password-token=
  2. リクエストボディ中のトークン値も 空に変更
    • もしくは該当パラメータを削除(ラボによるが、まずは値を空にする)
  3. username=wiener、新パスワードは適当な値のままにして送信

期待される結果
- それでもパスワードリセットが成功する(エラーにならない)

ここが重要な「壊れたロジック」
- 本来ならサーバ側は
- URLに含まれたトークン
- そのトークンがDBに保存されているか
- 有効期限内か
を検証するべきです。
- しかし、このラボでは トークン値を無視して username だけで処理している ため、
トークンが空でもパスワードが変わってしまいます。


ステップ 4 — 再度パスワードリセットを行い、攻撃用のリクエストを準備

ラボ手順どおり、もう一度自分のパスワードリセットを行います。

  1. ブラウザから再度 wiener で「Forgot your password?」を実行
  2. 新しいリセットリンクを踏んで、新パスワードを適当に設定
  3. 再び HTTP history から POST /forgot-password?... を見つけ、
    今回のものを Send to Repeater

なぜ再度やる?
- 攻撃時に混乱しないように「最新のフロー」で採取したリクエストをベースにするためです。
- 1回目のトークンを使い回すと、アプリ側が後で無効化しているケースもあり得るため、
ラボ手順に合わせて新鮮なリクエストを利用します。


ステップ 5 — Repeater で username を carlos に変更して送信(攻撃実行)

  1. Repeater で、再度
    • URLの temp-forgot-password-token の値を空にする
    • ボディのトークン値も空にする
  2. username= の値を carlos に変更
  3. new-password-1 / new-password-2 を、自分がログインに使いたい任意のパスワード に変更
  4. リクエストを送信

期待される結果
- 問題なく「パスワードリセット成功」扱いになる
- つまり、トークン検証なく、username だけでパスワードが書き換えられてしまった ことになる


ステップ 6 — Carlos の新パスワードでログインし、My account へ

  1. ブラウザに戻る
  2. ログアウト状態にしてから
  3. carlos と、さきほど自分が設定した新パスワードでログイン
  4. My account ページへ遷移
  5. ラボが Solved と表示される

5. どこが問題だったのか(設計の破綻ポイント)

このラボの致命的なポイントは:

  • パスワードリセットの POST 時に、トークンが一切検証されていない
  • サーバが信じているのは username のみ
  • つまり「そのユーザとしてパスワードを変える権利があるか?」をチェックしていない

本来必要なチェックは:

  1. temp-forgot-password-token がリクエストに含まれているか
  2. それが DB に記録されたトークンと一致しているか
  3. 対象 username とトークンが紐づいているか
  4. 有効期限を過ぎていないか
  5. 使用済みでないか

これらが 全てスキップ されているのがこのラボの「broken logic」です。


6. 実務での防御策

  • トークン中心設計
    • パスワード変更時は username をフォームに含めず、
      トークンから username を解決する
  • トークン検証の徹底
    • 一意性・有効期限・1回限りの使用・IP / UA 検証など
  • エラーハンドリングの慎重設計
    • どの条件でエラーを返したかを外部から推測されないようにする
  • 監査ログ
    • パスワードリセット開始・完了・失敗のログを残し、不審な試行を検出する

7. まとめ(ワンフレーズ)

「パスワードリセットの最終 POST でトークンを見ていない」= username さえ変えれば誰のパスワードでも勝手にリセットできる、という典型的な “broken logic” の事例を体験するラボ。

Best regards, (^^ゞ