Hello there, ('ω')ノ
全体像(何がどう繋がる?)
/login のリダイレクトが不備
- クエリの utm_content をキャッシュキーから除外する正規表現が甘い。
- これにより
lang=en?utm_content=...の後ろに未キー化のパラメータ列を実質追記できる(=unkeyed append)。
/login/ が読み込む
/js/localize.jsが CSPPlangの値をURLエンコードせずに import へ渡すため、&cors=1&x=1...といった追加クエリをそのまま混入できる(= client-side parameter pollution)。
/js/localize.jsはOriginでレスポンスヘッダー注入cors=1を付けるとサーバは Origin を見て CORS 用のヘッダーを返す。- Origin 値に CRLF(%0d%0a) を入れると ヘッダー分割→ボディ差し込みが発生(= response header injection)。
cache key injection が可能
Pragma: x-get-cache-keyを付けると実際に使われるキャッシュキーが分かる。- 観察すると、URL上の特殊トークンで“ヘッダーを合成”してキーを作る挙動($origin=...$ など)がある=ヘッダー注入が URL 経由で再現できる。
これらを 「①直接キャッシュ汚染 → ②被害者を /login から汚染済みJSへ誘導」 の2手で決めます。
事前設定(Burp Repeater)
- Protocol: HTTP/2 に切替(Inspector → Request attributes)。
- ヘッダー名は小文字に統一(
originなど)。 - 自動 URL エンコードに注意。本文中の
%0d%0aや%250d%250a(二重エンコード)はそのまま送る。
ステップ1:cache key の“見える化”(なぜ必要?)
- Repeater で次を送る(任意のパスでOK):
GET /js/localize.js?lang=en HTTP/2 pragma: x-get-cache-key
- 応答に キャッシュキーが出ます。
- ここから、URL内に
$$origin=value$$を入れるとキーに “h:origin=value” のような形で反映されることが分かる=cache key injection のサイン。 - 後続の「URLだけでヘッダーを合成」する作戦の確信を得ます。
ステップ2:まず“JSオブジェクト”を汚染(直接キャッシュに毒を入れる)
ねらい:/js/localize.js の特定キーで
alert(1)をボディとしてキャッシュさせる。 ポイント:HTTP/2 ではヘッダー値に生 CRLF は送れないため、%0d%0aを使う。originは小文字。
リクエスト:
GET /js/localize.js?lang=en?utm_content=z&cors=1&x=1 HTTP/2 origin: x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
cors=1で CORS 分岐へ入る。origin:に%0d%0aを入れ、Content-Length: 8と空行を注入 → 以降のalert(1)がレスポンスボディになる。- 末尾の
$$$$は“お守り”のパディング(CLを越える分は無視される想定)。 - これで 「パス=
/js/localize.js、クエリ=lang=en?utm_content=z&cors=1&x=1、ヘッダー=origin: x...」 のキャッシュエントリにalert(1)が保存されます。
なぜ utm_content=z を入れる? 次のステップで /login の不備を利用して、被害者の /login から同じキーの
localize.jsを引かせるための“型合わせ”です。
ステップ3:/login の unkeyed append から同じキーの JS を読ませる
ねらい:被害者が /login にアクセス → ページ内の
localize.jsの import URL が CSPP で汚染され、ステップ2と同じキーの JS を取りに行くようにする(=キャッシュヒットでalert(1)実行)。
リクエスト(キャッシュ汚染用の /login への“仕込み”):
GET /login?lang=en?utm_content=x%26cors=1%26x=1$$origin=x%250d%250aContent-Length:%208%250d%250a%250d%250aalert(1)$$%23 HTTP/2
ここが肝(細かい文字の意味)
lang=en?utm_content=.../loginのリダイレクトで utm_content はキャッシュキーから除外されるため、“後続の文字列が unkeyed で付く”。- さらに
localize.js?lang=...の**lang** はURLエンコードされずに import に渡るため、&cors=1&x=1...がそのままクエリへ混入(=CSPP)。
%26は&のエンコード(URLの中でクエリ区切りとして解釈させたい)$$origin=...$$- cache key injection の“書式”。URLの中で
originヘッダーを合成させます。 - HTTP/2準拠で小文字
originにしている点が重要。
- cache key injection の“書式”。URLの中で
%250d%250aは%0d%0aの二重エンコード- まずプロキシが
%25xx→%xxと1段階デコード。 - 次にアプリ(または中間層)が
%0d%0a→ CRLF と2段階目で解釈し、ヘッダー分割が成立。
- まずプロキシが
末尾
%23は#のエンコード(以降をフラグメント扱いにさせ、不要な文字列を無視させるための切り捨て)。
これで /login?lang=en へ来る被害者は、redirect→loginページの中で
/js/localize.js?lang=en?utm_content=z&cors=1&x=1(相当)を読み込み、ステップ2で汚染済みのキャッシュからalert(1)を受け取ります。
動作確認とコツ
- 被害者はChrome想定:あなたが上記 2 リクエストを送った後、/login を別タブで普通に開く(または何度か開く)。
- 期待挙動:localize.js の読み込みで
alert(1)が発火。 うまくいかない場合:
- HTTP/2か? Repeater の Protocol を再確認。
- ヘッダー名は小文字?
originが大文字だと弾かれる。 - 二重エンコードは維持?
%250d%250aが%0d%0aに勝手に変換されていないか。 - まず Pragma でキーを見たか?
$$origin=...$$でcache key がどう変わるかを見てから本攻撃へ。 - 順序:ステップ2(JS毒入れ)→ステップ3(/login誘導)。逆だとヒットしない。
まとめ(最小実行セット)
- JSを毒化(直接キャッシュ汚染)
GET /js/localize.js?lang=en?utm_content=z&cors=1&x=1 HTTP/2 origin: x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
- /login から同じキーを読ませる(unkeyed append + key injection)
GET /login?lang=en?utm_content=x%26cors=1%26x=1$$origin=x%250d%250aContent-Length:%208%250d%250a%250d%250aalert(1)$$%23 HTTP/2
これで /login?lang=en のリダイレクト先ページが 毒化された localize.js を参照し、被害者ブラウザで
alert(1)が実行されます。
よくある質問(Why系)
- なぜ
originは小文字? HTTP/2 ではヘッダー名は小文字。大文字混じりはプロトコル違反で落ちる。 - なぜ二重エンコード? URL → 1段階デコード(
%25→%)→ その後 CRLF(%0d%0a)として解釈、という二段階を通すため。 - なぜ
Content-Length: 8?alert(1)は8文字。ぴったりにしてボディを確定させる。 - なぜ
utm_content? キャッシュキーから除外されるため、キーを変えずに後続パラメータ(&cors=1...)を実質的に追加できる。
Best regards, (^^ゞ