プライバシーポリシー

2011年8月27日土曜日

Apache killerは危険~Apache killerを評価する上での注意~

Apacheの脆弱性(CVE-2011-3192)いわゆるApache killerが話題になっていますが、その脅威については一部誤解があるようです。

以下は、非常に脅威とする報告の例です。
一方今回のはプロセスの肥大化を伴うので、実メモリ消費して更にスワップも使い尽くしてOS毎激重になったあげくLinuxとかの場合はOOM Killer発動と、他のプロセスや場合によってはOSを巻き込んで逝ってしまいます。
CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.xより引用
以下は、それほど脅威でなかったとする報告の例です。
pooh.gr.jp は結構頑丈だったので 60 並列でやっと CPU idle 30% まで減らせた。
Apache Killer (CVE-2011-3192) 対策 for CentOS 5.6より引用
この差はなんでしょうか。サーバーが「結構頑丈だった」せいでしょうか。

それだけではありません。Apache killerを評価する上で重要なパラメータが2つあります。以下に示します。
  • ダウンロード指定するコンテンツのバイト数
  • ApacheのMaxClients設定

なぜコンテンツのバイト数が重要か

既に報告されているように、Apache killerはRangeヘッダに多くのパラメータを指定することにより、Apacheの使用メモリを増大させます。現在出回っているPoCの生成するRangeヘッダを以下に示します。
Range: bytes=0-,5-0,5-1,5-2,5-3,5-4,5-5,5-6,5-7,5-8,5-9,5-10,5-11,5-12,5-13,5-14,5-15,
5-16,5-17,5-18,5-19,5-20,5-21,5-22,5-23,5-24,5-25,5-26,5-27,5-28,5-29,5-30,5-31,5-32,

--CUT--

5-1281,5-1282,5-1283,5-1284,5-1285,5-1286,5-1287,5-1288,5-1289,5-1290,5-1291,5-1292,
5-1293,5-1294,5-1295,5-1296,5-1297,5-1298,5-1299
ご覧のように、5-1,5-2,5-3,…,5-1299 までのRange指定が含まれます。このため、URLで指定するコンテンツのデータサイズが1299バイト以上ないと、このPoCの「真価」が発揮されないと予想されます。
これを実験で試してみました。killapache.plをN=1で走らせ、URLで指定されるコンテンツ(/index.html)を0バイトから2000バイトまで変化させて、apacheのプロセスのメモリサイズを測定しました(追記:MaxClientsは3としました。理由は後述)。まずは生データです。

以下は、グラフにしたものです。

予想通り、コンテンツサイズが1300バイトまではプロセスサイズが大きくなりますが、それ以上ではプロセスサイズは変わりません。すなわち、Apache killerを評価する上では、コンテンツのサイズを1300バイト以上にすることが重要なポイントとなります。

pooh.gr.jpの評価では、robots.txt(約30バイト)を指定していたため、Apache killerはまったく真価を発揮しておらず、単なるF5攻撃と変わらない状況だったと予想されます。


MaxClients設定が重要な理由

ここまで説明すると、Apache killerの評価をする上でMax Clientsが重要である理由を説明することも容易になります。Apache killerのリクエストを受け付けると、元々24メガバイトだったApacheプロセスの消費メモリが96メガバイトと4倍になります。しかし、MaxClientsが十分余裕を持って設定されていれば、プロセスメモリが増大しても処理を継続することができます。

徳丸の評価結果

私も最初上記をよく理解しておらず、「あれ、Apache killer大したことない?」と誤解しかかったり、「評価環境が非力なので差が出ないのかな」と思ったりしました。上記に気がついた後、以下の条件で評価してみました。

VMのメモリ割りあて: 2ギガバイト
swap:512メガバイト
コンテンツのサイズ:約50Kバイト(一般的なコンテンツのサイズを想定)
MaxClients: 50
OS:CentOS5.6(32ビット)


上記条件でApache killerを走らせた場合、必要なメモリサイズは96 * 50 = 4800メガバイトとなり、メモリ+swapがまったく不足する状況となります。
結果は、ping以外はまったく応答しなくなりまりした。sshからコマンドを打つことも、VMwareのコンソールからログインすることもできなくなり、リセットするしかない状態でした。

このエントリを読んだ皆様、Apache killerはやはり凶悪です。くれぐれも油断なきよう、至急の対策をお勧めします。
私は、Apache2.2を使っていましたので、アドバイザリに従い、httpd.confに以下を追加しました。

# Drop the Range header when more than 5 ranges.
# CVE-2011-3192
SetEnvIf Range (?:,.*?){5,5} bad-range=1
RequestHeader unset Range env=bad-range
# We always drop Request-Range; as this is a legacy
# dating back to MSIE3 and Netscape 2 and 3.
RequestHeader unset Request-Range

2011年8月24日水曜日

PHP5.3.7のcrypt関数のバグはこうして生まれた

昨日のブログエントリ「PHP5.3.7のcrypt関数に致命的な脆弱性(Bug #55439)」にて、crypt関数の重大な脆弱性について報告しました。脆弱性の出方が近年まれに見るほどのものだったので、twitterやブクマなどを見ても、「どうしてこうなった」という疑問を多数目にしました。
そこで、このエントリでは、この脆弱性がどのように混入したのかを追ってみたいと思います。

PHPのレポジトリのログや公開されているソースの状況から、PHP5.3.7RC4までこのバグはなく、PHP5.3.7RC5でこのバグが混入した模様です。RC5はPHP5.3.7最後のRelease Candidateですから、まさに正式リリースの直前でバグが入ったことになります。
バグの入る直前のソースは、ここの関数php_md5_crypt_rから参照することができます。以下に、おおまかな流れを図示します。まずはバグの入る前です。


上図の各処理の概要は以下の通りです。
  1. passwdはstatic配列なので初期値は全て0('\0')
  2. MD5を示すマジック「$1$」をpasswdの先頭にmemcpyでコピー
  3. ソルトを定位置にstrlcpyでコピー
  4. 現時点の文字列末尾に$をstrcatで追加
  5. ハッシュ値を定位置に書き込み
  6. 文字列の終端を示す'\0'を書き込み
このような状態で、ソースコードの静的解析が実施され、その結果、(4)のstrcatが指摘されたようです。strcatはバッファ長を指定できないので、データの内容によってはバッファオーバーフローの原因になります。それを指摘されたのでしょう。もっとも、この処理の場合はデータが固定長なので脆弱性の心配はないのですが、コミットログのコメントには以下のように書いてあります。
Make static analyzers happy
静的解析ツールの警告表示を消したかったのでしょうね。このため、以下の変更が行われました(差分)。

strcat(passwd, "$");
  ↓
strncat(passwd, "$", 1);

strncatは書き込む文字列の最大長を指定するstrcatの改良版です。この場合は1が指定されているので、最大1文字書き込み、その後に終端の'\0'を書き込みます。

この時点ではバグは入っていません。問題は、この後です。いったんstrncatに変更した箇所が、さらにstrlcatに変更されています。

strlcat関数はstrncatの改良版です。strncatは、追加する文字列の最大長を指定しますが、元の文字列長と追加文字列、さらに終端の'\0'のトータルの長さを意識しなければならないという点で使いにくいという問題があります。これに対して、strlcatはバッファ長を指定するので、文字列長の計算を呼び出し側で意識しなくてもよいという利点があります。バッファオーバーフロー対策としてはstrncatよりも確実です。
この変更点を以下に示します(差分)。

strncat(passwd, "$", 1);
  ↓
strlcat(passwd, "$", 1);

ここで不幸にもバグが入りました。strncatとstrlcatでは、第3パラメータの意味が異なります。strncatは、追加する文字列の最大長なので、1で問題ありません。一方、strlcatはバッファ長を指定するので、既に"$1$"とソルト(8文字)の都合11文字が入っているバッファに対して、バッファ長1を指定したことになり、「バッファは既にいっぱいなので文字を追加する余地はない」と解釈されます。その結果、"$"の書き込みは行われません。バッファはそのままの状態になります。
ここで、strlcatへの変更後の処理を以下に示します。(4)の部分が変更点です。


処理(5)と(6)でハッシュ値と文字列終端'\0'が書き込まれますが、その直前の箇所が'\0'のままです。このため、C言語の文字列としては、"$1$"とソルトだけでちぎれた状態になります。このため、肝心のハッシュが出力されないという結果になりました。

以上がBug #55439の混入した経緯です。

ところで、strlcpy/strlcatの仕様をWikipediaで確認していたところ、興味深い記述を見つけました。
一方で、GNU Cライブラリ (glibc) の開発者たちは、GNU Coding Standardsで禁じられている「長い行を黙って切り詰める」関数である、このような仕様の関数はバグである、いい加減なプログラムを助長してしまう、新たなセキュリティ問題を生む、など否定的な見解を示しており、標準規格に含まれない限りはglibcには実装しない意向である。
上記の意見には賛否両論あるところでしょうが、今回のケースでは、glibcの開発者達の予想が不幸にも的中してしまったことになります。

プロジェクトマネジメント上の問題としては、テスト不足というのは明らかですが、その前段階の問題として、RC5というリリース直前の状態で不急の修正をしたことが大きな要因だと考えます。

追記

今回の原因を作ったRasmus Lerdorf(PHP/FIのオリジナル開発者)がGoogle+で今回の経緯を説明しています。英語ですが、興味のある方はご覧下さい。私のエントリの修正は必要なさそうです。

追記2

今さらですが、PHP5.3.7のmake testを走らせたところ、ちゃんとテストでFAILしているのですね。
TEST 8080/8990 [ext/standard/tests/strings/crypt.phpt]FAIL crypt() function [ext/standard/tests/strings/crypt.phpt]
テスト(crypt.phpt)の内容を見ると非常に単純なテストであり単体レベルではもっと色々なパターンで試験すべきだと思います。しかし、せめてこのテストを単体テストで実施することと、テストしたことをコミット時に確認していれば、バグ流出は防げたと思います。

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メソッドの出番はないので、プレースホルダの使用を強く推奨します。