【PortSwigger Lab#4】CSPとAngularJSサンドボックスの二重防御をすり抜けたXSS
はじめに
今回はPortSwiggerのラボ「Reflected XSS with AngularJS sandbox escape and CSP(訳:CSPを使ったAngularJSサンドボックスエスケープによる反射型XSS)」に取り組んだので、その内容を備忘録としてまとめます。
前回の記事ではAngularJSのサンドボックスをcharAt()の上書きによって迂回しました。
今回はそこにCSP(Content Security Policy)という新たな制約が加わります。最初は「前回と同じようにサンドボックスを迂回すれば解けるのでは」と考えていましたが、CSPという壁が立ちはだかり、そう単純にはいきませんでした。
複数の防御機構が重なったとき、どこに突破口を見つけるかを考えるのが今回の学びでした。
ラボの概要
-
目的:CSPを迂回し、AngularJSのサンドボックスを迂回して
document.cookieをalertで表示する - 難易度:Expert
このラボではAngularJSとCSPが組み合わさった環境で、サンドボックスの迂回に加えてCSPのバイパスも求められます。前回とは異なり、攻撃者自身がエクスプロイトサーバーを使って被害者にペイロードを届ける形式となっています。
前提知識
反射型XSS・AngularJSのサンドボックス・orderByフィルターについて
反射型XSS・AngularJSのサンドボックス・orderByフィルターについては前回の記事で詳しく解説しています。未読の方はあわせてご参照ください。
CSP(Content Security Policy)とは
CSPはXSSなどのコードインジェクション攻撃を緩和するためのブラウザのセキュリティ機能です。HTTPレスポンスヘッダーとして送信され、Webブラウザに対して「このページではどのリソースを読み込んでよいか」を指示することで、インラインスクリプトの実行や外部スクリプトの読み込みを制限します。
たとえばscript-src 'self'というCSPが設定されていれば、同一オリジン以外からのスクリプトの読み込みが禁止され、インラインスクリプトも実行できなくなります。一見すると強固な防御に見えますが、今回のラボのようにAngularJSのテンプレート評価の仕組みを悪用することで、CSPの制限下でもスクリプトを実行できる場合があります。
ng-cspとは
ng-cspはAngularJSをCSPに対応した動作モードに切り替えるための属性です。
通常AngularJSは式の評価にeval()を使用しますが、CSP環境ではeval()の使用が禁止されています。ng-cspを付与することでAngularJSがeval()を使わない評価方式に切り替わり、CSP環境下でも正常に動作できるようになります。
ng-focusと$eventとは
ng-focusはAngularJSの属性で、要素がフォーカスを受け取ったときに実行する処理を記述できます。フォーカスとは、たとえばテキストボックスをクリックしてカーソルが入力待ちの状態になることを指します。URLの末尾に#xを付けることで、ページ読み込み時にid=xを持つinput要素へ自動的にフォーカスが当たるようにしています。通常<script>タグを使ったスクリプトの実行はCSPによってブロックされますが、ng-focusはAngularJSのテンプレート評価として処理されるためCSPの制限をすり抜けることができます。
$eventはAngularJSが用意している変数で、発生したイベントの情報を丸ごと持っているオブジェクトです。今回の攻撃ではng-focusに記述した処理の中で$eventを参照し、フォーカスイベントの詳細情報を取り出すことでwindowオブジェクトへ到達する手がかりを得ています。
$event.composedPath()とは
composedPath()は$eventに用意されている関数で、イベントが発生した要素からページの最上位までの経路を配列として返します。たとえばinput要素でフォーカスイベントが発生した場合、input→body→html→document→windowという順番で要素が並んだ配列が返されます。
この配列の末尾には必ずwindowオブジェクトが含まれます。AngularJSのサンドボックスはwindowオブジェクトへの直接アクセスをブロックしますが、composedPath()が返す配列を経由することで間接的にwindowオブジェクトへ到達できます。
なおこの挙動はChromeに特有のものです。
攻撃の流れ
1. 環境の確認
ラボにアクセスし、検索ボックスにcatと入力して検索します。入力値がURLのパラメータとしてそのままページに反映されていることが確認できます。

入力値がURLのパラメータとしてそのままページに反映されていることが確認できる。
ページのソースを確認すると、<body>タグにng-appとng-cspが付与されていることが確認できます。
<body ng-app ng-csp>
開発者ツールのNetworkタブからページのレスポンスヘッダーを確認すると、以下のCSPが設定されていることが確認できます。
default-src 'self'; script-src 'self'; style-src 'unsafe-inline' 'self'
<body>タグにng-appが付与されていることからページ全体がAngularJSのスコープ内にあること、ng-cspが付与されていることからAngularJSがCSP対応モードで動作していることが確認できます。またレスポンスヘッダーのscript-src 'self'という設定から、同一オリジン以外のスクリプトの読み込みとインラインスクリプトの実行が制限されていることがわかります。

ページソースでng-appとng-cspを確認。レスポンスヘッダーにCSPの設定も確認できる。
2. 制約の整理
環境の確認を通じて、このラボにはCSPという制約があることが明らかになりました。script-src 'self'により、インラインスクリプトの実行とeval()の使用が禁止されています。
試しに{{1+1}}を検索ボックスに入力してみると、'2'と評価されて表示されました。これによりCSPが設定されていてもAngularJSのテンプレート評価は機能していることがわかりました。

{{1+1}}が'2'と評価されて表示され、AngularJSのテンプレート評価が有効であることが確認できる。
つまりCSPはインラインスクリプトをブロックしますが、AngularJSのテンプレート評価はブロックしません。この性質を利用することでCSPを迂回できる可能性があります。
3. 直接注入を試みる
CSPの制限下でもAngularJSのテンプレート評価が機能していることがわかったので、次に検索ボックスに直接ペイロードを入力して試みます。しかし検索ボックスには80文字の文字数制限があり、今回必要なペイロードはその制限を超えてしまいました。

文字数制限により直接注入が阻まれた。
そこで今回のラボ環境に用意されているエクスプロイトサーバーを活用します。locationを使って被害者のブラウザをペイロード付きURLにリダイレクトさせることで、文字数制限を回避できます。
4. ペイロードの構築
CSPを迂回するために、ng-focus属性に着目します。
<script>タグを使ったスクリプトはCSPによってブロックされますが、ng-focusに記述した処理はAngularJSが評価するためCSPの制限をすり抜けることができます。
次にalertの実行方法を考えます。alertはwindowオブジェクトのメソッドであるためwindowのスコープから呼び出す必要がありますが、AngularJSのサンドボックスはwindowへの直接アクセスをブロックします。そこで$event.composedPath()を利用します。この関数が返す配列の末尾には必ずwindowオブジェクトが含まれており、orderByフィルターで配列を処理する過程でwindowのスコープからalertを呼び出せます。
またalertは直接呼び出さず、z=alertという形で変数に代入してから呼び出す方法をとっています。AngularJSがwindow.alertという直接呼び出しを検出してブロックするためです。orderByフィルターがwindowオブジェクトに到達したタイミングでzを通じてalertが実行され、windowチェックを回避できます。なおzは仮の変数名で、他の変数名でも同様に機能します。
ここまでの内容を1つのペイロードにまとめます。
<input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(document.cookie)'>#x
URLの末尾に#xを付けているのは、ブラウザはURLに#要素IDが含まれている場合、ページ読み込み時にその要素へ自動的にスクロールしてフォーカスを当てる仕様があるためです。これを利用することで、ユーザーが何も操作しなくてもng-focusに記述した処理が自動的に実行されます。
5. エクスプロイトサーバーへの配置と実行
構築したペイロードをエクスプロイトサーバーに配置します。「Body」欄に以下のコードを入力して「Store」で保存します。URLのラボIDの部分は自身のものに置き換えてください。
<script>
location='https://ラボID.web-security-academy.net/?search=%3Cinput%20id=x%20ng-focus=$event.composedPath()|orderBy:%27(z=alert)(document.cookie)%27%3E#x';
</script>

エクスプロイトサーバーにペイロードを配置した様子。
「View exploit」をクリックして自分のブラウザでペイロードの動作を確認します。自分のブラウザには被害者のセッションクッキーが存在しないため、alertのダイアログは空で表示されますが、ダイアログが表示された時点でペイロードが正しく機能していることが確認できます。

ペイロードが発火しダイアログが表示された。セッションクッキーが存在しないため内容は空になっている。
続いて「Deliver exploit to victim」をクリックすると、被害者のブラウザでペイロードが実行されてラボ完了となります。

ラボ完了。画面上部に「Congratulations, you solved the lab!」と表示されている。
まとめ
今回のラボでは、AngularJSのサンドボックスとCSPという二重の制約がある中で、ng-focusと$event.composedPath()を組み合わせることでXSSを成立させました。
CSPはXSSに対する有効な防御手段ですが、今回のようにAngularJSのテンプレート評価を経由することでその制限をすり抜けられる場合があります。複数の防御機構が重なっていても、フレームワークの仕様やブラウザの挙動を組み合わせることで突破口が生まれるというのが今回の学びでした。
防御側の視点で考えると、CSPを設定するだけでなく、使用しているフレームワークの特性まで考慮した設計が重要だと感じました。「CSPがあるから安全」という過信が、今回のような迂回を許してしまう原因になりえます。
今後もPortSwiggerやHackTheBoxで学んだ内容を記事としてまとめていく予定ですので、ぜひまた読んでいただけると嬉しいです。
Discussion