2014/05/30

CVE-2014-0509: 上位サロゲートを使ったFlashのXSS

Flashの文字列処理の方法が適切でないために、 適切にXSS対策が施されたFlashファイル上でもXSSを引き起こせる場合があった問題について書きます。
この問題は以下に掲載されているように、 2014年4月のFlash Playerのアップデートで修正されました。

http://helpx.adobe.com/security/products/flash-player/apsb14-09.html
These updates resolve a cross-site-scripting vulnerability (CVE-2014-0509).

本問題は、 このブログでも何度か取り上げた ExternalInterface.call() の問題に関係するものです。取り上げたのはこの辺の記事です:
ExternalInterface.call()の問題を知らない人は、先にこれらの記事をみておくと理解しやすいかもしれません。

問題の詳細

次のようなコードが、この問題の影響を受けます。

http://vulnerabledoma.in/surrogate_xss.as
...
var q:String=loaderInfo.parameters["q"].split("\\").join("\\\\"); ExternalInterface.call("console.log",q);
...
前半でqパラメータ中に含まれる「\」を split("\\").join("\\\\") で置換して変数に代入後、その変数をExternalInterface.call()の第2引数に渡しています。
この方法はExternalInterface.call() のバグの回避法として全く間違っていません。

ところが、次のような文字列を与えると、XSSが起きていました。

http://vulnerabledoma.in/surrogate_xss.swf?q=%ED%A0%80\"))}catch(e){alert(1)}//
 
注目すべきは先頭で与えた%ED%A0%80 です。%ED%A0%80 は上位サロゲート( サロゲートペアの前半バイト )にあたるコードポイント、 U+D800 を直接UTF-8エンコードした場合にできるバイト値です。Flashは、この上位サロゲートの扱いが適切でなく、任意の次の文字を潰してしまう( 正確には、後続の文字とくっついてU+FFFDを作ってしまう )挙動がありました。これが今回のバグです。

先ほどのSWFで、XSSが起こる流れは次のようなかんじです。

http://vulnerabledoma.in/surrogate_xss.swf?q=%ED%A0%80\"))}catch(e){alert(1)}//
// \が\\に置換されて U+D800\\"))}catch(e){alert(1)}// がqに代入される
// この場面では置換前から U+FFFD になっているとは考えないらしい
var q:String=loaderInfo.parameters["q"].split("\\").join("\\\\");

// console.logを引数qで呼び出す
ExternalInterface.call("console.log",q);

// FlashがJavaScript呼び出し時に生成するもの
// このとき、「"」はFlash側で「\"」にエスケープされる
// \自体はエスケープしてくれないので直前で\\にした訳だけど…あれ?
try{__flash__toXML(console.log("�\\"))}catch(e){alert(1)}//"));}catch (e){"<undefined/>"}

split("\\").join("\\\\") を使って置換するとき、置換前の段階では \はまだ喰われていないのか、ちゃんと\が置換されているところがキモです。 このせいで、のちのち喰われる場面にきたとき、不整合が起きます。

面白いことに、他の置換できる関数、例えばreplace(/\\/g,"\\\\") を使って置換する場合は、 置換前から後続の文字とくっついてU+FFFDだと評価されているのか、\\への置換は起きませんでした。
また、ExternalInterface.call()に 「U+D800"」を与えた場合でも、( 「"」の文字はJavaScript実行時にFlash側で「\"」とエスケープされますが、このエスケープよりも先に ) 「U+D800"」から「U+FFFD」が作られるようなので、 問題は起きません。

いろいろ試してみたものの、結局 split().join() と ExternalInterface.call() の組み合わせしかXSSに繋がるケースは思いつきませんでした。マイナーすぎて、修正してくれるか微妙だなーと思いながらそのケースだけAdobeに報告しましたが、割と短い期間で修正してくれました。ナイスです。
修正後は上位サロゲートにあたるバイト値が後続の文字を一切潰さなくなりました。


実はこのバグ、GitHubがバグ報酬制度を始めたときにみつけたものです。
GitHubで使われていた、ZeroClipboardというSWFが、まさにパラメータ文字列をsplit("\\").join("\\\\")で置換してExternalInterface.call()の第2引数に渡す処理をしていました。

このバグの報酬として、GitHubの報酬制度を通じて$1,800を頂きました。
GitHub Bug Bounty · Masato Kinugawa

GitHubはContent Security Policyを導入していますが、FlashがCSPに対応していないので、CSPもすり抜けるとして、ちょっと普通のXSSより評価ポイントが高くなっています。ちなみに、ZeroClipboardの古いバージョンには今回のバグとは関係ないExternalInterface.call()絡みのXSSがある( http://seclists.org/fulldisclosure/2013/Feb/103 )ので、使っている人は1度バージョンを確認した方がいいかもしれません。

加えて、HackerOneのFlashの報酬制度を通して、$2,000を頂きました。
 #7803 Security bypass could lead to information disclosure - HackerOne

Flashの報酬制度で報酬を頂くのは、これで3度目になります。毎度ありがたいです。


今回、split().join()とExternalInterface.call()が組み合わさった時に問題が起きることに気が付いたのは、ZeroClipboardのSWFに問題が起きそうな文字を適当に入れていたらたまたま、というかんじでした。つくづく、何も考えずにひとまず適当に試してみるのもバグの発見には重要な行為だと感じます。