認証は、アプリのセキュリティの最前線です。ここが破れれば、その先の認可も暗号も意味をなしません。PortSwigger が言う通り「認証の脆弱性は、攻撃者に機密データへのアクセスを許し」、特権アカウントを奪われればアプリ全体が乗っ取られます。本記事は、その攻撃手法を公式に忠実に解説します。
絶対の前提: 総当たり・列挙・リセット汚染は、いずれも強い侵襲性を持ちます。実行は 合法ラボ または書面で許可されたスコープでのみ。無許可のログイン試行は、それ自体が不正アクセス行為です(→ 法律ガイド)。地図は ピラー。
1. ユーザー名列挙 — 攻撃の第一段
総当たりの前に、「存在する有効なユーザー名」を確定させると効率が跳ね上がります。アプリは、しばしばわずかな差でユーザーの存在を漏らします。
| 漏洩の信号 | 例 |
|---|---|
| メッセージの差 | 「ユーザー名が不正」 vs 「パスワードが不正」 |
| 応答時間の差 | 存在するユーザーだけハッシュ照合が走り、応答が遅い |
| HTTPステータス/微妙な文言差 | 末尾の句点の有無、リダイレクト先の違い |
# Burp Intruder で username を辞書にして送り、レスポンスの差分(長さ・文言・時間)を観察
POST /login HTTP/1.1
Host: lab.example
Content-Type: application/x-www-form-urlencoded
username=§candidate§&password=wrongpass
Burp Intruder の Sniper 攻撃で username 位置だけを回し、レスポンス長や応答時間の外れ値から有効ユーザーを割り出します。
2. 総当たり(ブルートフォース)と保護の回避
有効ユーザー名が判明したら、パスワードを総当たりします。問題は保護機構の不備です。
2.1 IPベースのレート制限の回避
「同一IPから一定回数失敗したらブロック」は、ヘッダ偽装やIPローテーションで回避され得ます。
# X-Forwarded-For を毎回変えて、IPベースのカウンタを欺く(脆弱な実装で有効)
X-Forwarded-For: 1.2.3.4
2.2 アカウントロックの抜け穴 — パスワードスプレー
「1ユーザーがN回失敗でロック」は、多数のユーザーに同じ弱いパスワードを1回ずつ試すパスワードスプレーで回避されます。各ユーザーの失敗回数はロック閾値に届きません。
# 縦に弱いパスワードを固定し、横に大量ユーザーを試す(Pitchfork/Cluster bomb的)
user001 : Password1!
user002 : Password1!
... ← どのユーザーもロックされない
さらに、ロックされたという応答自体が「このユーザーは存在する」という列挙の信号になります。
3. 多要素認証(2FA/MFA)のバイパス
MFAは強力ですが、正しく実装されて初めて効きます。PortSwigger が挙げる典型的な穴:
3.1 検証ロジックの欠落(2段目を飛ばす)
1段目(パスワード)成功後に発行されるセッションが、2段目未完了でも保護リソースへ到達できる場合。2FA入力画面をスキップして、直接遷移先URLを叩くと通ってしまう。
# 2FAページを経由せず、ログイン後のページへ直接アクセスして通るか確認
GET /my-account HTTP/1.1
Host: lab.example
Cookie: session=<1段目だけ通したセッション>
3.2 OTPの総当たり
検証コード(OTP)に試行回数制限・レート制限がないと、4〜6桁を総当たりできます。Burp Intruder で 0000〜9999 を回すだけで突破され得ます。
3.3 ユーザーの取り違え
2段階目の検証が、1段目のユーザーと別のユーザーのコードを受け付けてしまう実装ミス。アカウント識別をクライアント任せにすると起きます。
4. その他の機構 — リセット・Remember Me・変更
4.1 パスワードリセットの汚染(裏口)
パスワードリセットは「本人確認を一時的に緩める」ため、認証の裏口になりやすい。
- 推測可能なトークン:連番・タイムスタンプ・短いトークンは予測される。
- Hostヘッダ汚染:リセットリンクの生成に
Hostヘッダを使っていると、攻撃者がHost: evil.exampleを送り、被害者宛のメールに攻撃者ドメインのリンクを仕込める。被害者がクリックするとトークンが攻撃者に渡る。 - 識別子の差し替え:リセット確定リクエストの
username/userIdを他人に変えられると、他人のパスワードを設定できる。
# Hostヘッダ汚染:被害者宛リセットメールのリンクが攻撃者ドメインを指すよう仕込む
POST /forgot-password HTTP/1.1
Host: evil-attacker.example ← サーバーがこれを信頼してリンク生成すると致命的
Content-Type: application/x-www-form-urlencoded
username=victim
4.2 Remember Me / パスワード変更
「ログイン状態を保持」のトークンが推測可能(ユーザー名のハッシュ等)だと偽造される。パスワード変更が現行パスワードを確認しないと、セッション奪取後の永続化に使われます。
5. 【守る側】根本対策
攻撃面を理解したら、設計で塞ぎます。PortSwigger と OWASP Authentication Cheat Sheet の要諦:
// ✅ ユーザー名列挙を防ぐ:成否で「同一の」メッセージ・同等の処理時間を返す
async function login(username: string, password: string) {
const user = await findUser(username);
// ユーザーが存在しなくても必ずハッシュ照合を走らせ、応答時間を一定化(タイミング差を消す)
const hash = user?.passwordHash ?? DUMMY_HASH;
const ok = await verifyPassword(password, hash); // Argon2id 等
if (!user || !ok) {
// 列挙を許さない一定の文言(どちらが違うかを明かさない)
throw new AuthError("ユーザー名またはパスワードが正しくありません");
}
// ...
}
設計原則:
- 列挙を塞ぐ:成否で同一メッセージ・同等の応答時間・同一ステータス。
- 堅牢なレート制限:IPだけでなくアカウント単位 + グローバルで。
X-Forwarded-Forを信頼しない。サーバーレスでの正しいレート制限は 専用ガイド。 - MFAを正しく実装:2段目完了まで保護リソースへ到達させない。OTPに試行回数制限。コードは短命・単回・1段目ユーザーに束縛。
- パスワードリセットの安全化:推測不能(CSPRNG)で長く・短命・単回・ユーザー束縛のトークン。リンク生成に
Hostヘッダを使わず、サーバー側の正規URLを信頼する(HTTP Host header攻撃対策)。 - パスワード強度とハッシュ:Argon2id等で保存し、弱いパスワードを拒否。詳細は パスワードハッシュ・暗号ガイド。
- セッション:認証後にセッションIDを再生成、
HttpOnly/Secure/SameSiteを付与。
認証基盤そのものをマネージドに寄せる選択(Cognito/Auth0/Clerk/Supabase Auth)は 認証プラットフォーム選定 を参照。**「認証は自前実装しない」**のが、多くのプロジェクトで最も安全な判断です。
6. まとめ
- 列挙→総当たりの2段構え:まず有効ユーザー名を、メッセージ差・時間差から特定。
- 保護機構は回避される:IP制限はヘッダ偽装、ロックはパスワードスプレーで。
- MFAは実装次第:2段目スキップ・OTP総当たり・ユーザー取り違えに注意。
- リセットは裏口:推測可能トークン・Hostヘッダ汚染・識別子差し替え。
- 根本対策:一定応答で列挙封じ + 堅牢なレート制限 + 正しいMFA + 安全なリセット + 強ハッシュ。
次は、テンプレートエンジンを乗っ取ってRCEに至る SSTIの完全攻略 へ。