なぜPHPアプリにセキュリティホールが多いのか?

第43回 PHP 5.3のcrypt関数の問題

この記事を読むのに必要な時間:およそ 8 分

Linuxの場合

筆者が普段利用しているMomonga Linux 7で./configureスクリプトを実行した場合,crypt関係がどのように設定されているのか紹介します。

configureスクリプトの実行結果から抜粋

checking for crypt in -lcrypt... (cached) yes
checking for standard DES crypt... (cached) yes
checking for extended DES crypt... (cached) no
checking for MD5 crypt... (cached) yes
checking for Blowfish crypt... (cached) no
checking for SHA512 crypt... (cached) no
checking for SHA256 crypt... (cached) no

DESとMD5がサポートされていることが分かります。実際にどのような設定になったかC言語のマクロで確認します。

main/php_config.hより抜粋

#define HAVE_CRYPT 1

/* Define to 1 if you have the <crypt.h> header file. */
#define HAVE_CRYPT_H 1

/* Define to 1 if you have the `crypt_r' function. */
/* #undef HAVE_CRYPT_R */

/* Whether the system supports BlowFish salt */
#define PHP_BLOWFISH_CRYPT 1

/* Whether the system supports extended DES salt */
#define PHP_EXT_DES_CRYPT 1

/* Whether the system supports extended DES salt */
#define PHP_EXT_DES_CRYPT 1

/* Whether the system supports SHA256 salt */
#define PHP_SHA256_CRYPT 1

/* Whether the system supports SHA512 salt */
#define PHP_SHA512_CRYPT 1

/* Whether the system supports standard DES salt */
#define PHP_STD_DES_CRYPT 1

/* Whether PHP has to use its own crypt_r for blowfish, des and ext des */
#define PHP_USE_PHP_CRYPT_R 1

ソースコードだけ見ると,システムがcrypt_rを提供している場合,システムが提供しているcrypt_rを利用しそうに見えますが違います。色々定義されていますが最後の

/* Whether PHP has to use its own crypt_r for blowfish, des and ext des */
#define PHP_USE_PHP_CRYPT_R 1

が決定的です。この定義があると先に紹介したext/standard/crypt.cのコードから分かるように,PHPが実装したcryptライブラリが「自動的」に利用されます※1⁠。

※1

configureスクリプトのオプションには,PHPのcryptライブラリを使うかシステムのcryptライブラリを使うか設定する項目はありません。変更したい場合はphp_config.hを直接編集する必要があります。

この例はMomonga Linuxの例ですが,現在利用されているほぼすべてのLinuxで同じ結果になると思われます。configureスクリプトの中で

if test "$ac_cv_crypt_blowfish" = "no" || test "$ac_cv_crypt_des" = "no" || test "$ac_cv_crypt_ext_des" = "no" || test "x$php_crypt_r" = "x0"; then

と定義されているため,Blowfish, DES, Extended DESがない場合,リエントラントなcrypt_rがない場合やcrypt_rで利用する構造体がサポートされていない場合,構造体のアライメントがサポートされていない場合はPHPのcrypt実装が利用されます。

PHPプロジェクトのCRYPT_BLOWFISHの解説ページ

PHPプロジェクトはcrypt関数のバグについて解説ページを用意しています。

筆者による意訳を載せておきます。

PHP 5.3.7+で実装された変更は,後方互換性よりセキュリティと正確性を重視しています。しかし,ユーザ(PHPアプリをインストールする管理者)が既存のハッシュに$2x$プレフィックスを利用することにより,セキュリティ上問題があるパスワード($2a$,$2y$を利用するもの)が変更されるまでの後方互換性を維持できるようになっています。

5.3.7より古いPHPでは,$2a$プレフィックスを持つパスワードは非ASCII文字を含むパスワードで不適切にシステム依存する動作をしていました。あるシステム環境(ほとんどの場合はPowerPCやARMであり,CPUアーキテクチャに関わらずBSD系UNIXやSolarisの一部)では正しく処理されていました。大部分のシステム環境(ほとんどのLinuxやその他 ─ 訳注:Windowsなど)では,常にではありませんがほとんど正しく処理されていない(バグのため,動作していなかった)うえ,セキュリティが良くない状態で動作していました。

PHP 5.3.7では,$2a$プレフィックスを持つ場合でもほとんど正しく動作するようになりました。先に紹介したセキュリティ的に弱い古いハッシュを持つ対策(訳注:$2x$プレフィックスのこと)も追加されました。$2x$プレフィックスを持つ場合は,古いハッシュが持つセキュリティリスクを許容しつつ動作するよう,バグを含んだ動作をするようになっています。

$2y$プレフィックスを持つ場合,管理者が望むなら新しいハッシュが設定された場合,互換性問題に対する対策なしに正しい動作をします。実用的には$2a$,$2y$どちらのプレフィックスを利用しても変わりありません。後方互換性対策のコードが(正しいUTF-8でさえない)外部からの意図的な攻撃だと分かる,おかしなパスワードがバグを含む5.3.7以前のハッシュにマッチさせようとするからです。

まとめ:8bit目が設定されていない文字エンコーディングを利用しているパスワードの場合,何の問題もありません。すべてのプレフィックスはまったく同様に動作します。パスワードで8bit目を利用する文字エンコーディングを利用している場合で,後方互換性よりセキュリティと正しさを優先した場合は何もする必要はありません。新しいPHPにアップグレードして新しい動作を$2a$プレフィクスを使えばよいです。

しかし,管理者がセキュリティより後方互換性を重視する場合で,一部の環境で見られている問題に対処したい場合は既存のパスワードデータベースの$2a$プレプレフィクスを$2x$に変更することができます。ほかの方法としては,PHPアプリケーションのコードで$2a$プレフィックスを持つパスワードに対して,新しくパスワードを設定する際に$2y$プレフィックスを付与すれば似たような動作を実現できます(この場合$2x$への自動変換は実行されません⁠⁠。

これまでの解説で何故アライメント(CPU)やプラットフォーム(OS)が脆弱性に影響するのか理解できると思います。PHPのconfigureスクリプトでPHPかシステムのどちらのcryptライブラリを利用するか決定するコードが,アライメントやシステムが提供するライブラリに依存しているからです。

Linux系OSでは随分前から認証にはPAM(Pluggable Authentication Module)が利用されていたため,歴史的にcrypt関数にはあまり注意が払われていません。このため,Linux環境のcryptは古いDESとMD5しかサポートしておらず,自動的にPHPのcrypt実装が使われるようになっています。

BSD系のOSやSoralisではcryptのサポートが充実しています。このため,configureスクリプトを実行するとBSD系のOSや,Solarisの前はBSD系のOSを利用していたSUN(現在のOracle)ではシステムのcryptが利用される場合が多いようです。PHPの実装を使用しないのでこれらの環境では5.3.7のバグに影響されないのは当然です。ちなみにBSD系のOSであるMac OS Xですが,筆者のOS X 10.5で試したところPHPの実装が使われるようでした。

crypt関数には色々問題があったのですべては書ききれませんが,PHPプロジェクトの文書と筆者が書いた記事では分りづらいのでポイントをまとめると

  • PHP 5.3.0以前はcrypt関数はシステム依存な関数であり,スレッドセーフではない場合も多くあった

  • PHP 5.3.0から5.3.6までは多くのプラットフォームでcrypt関数はまともに動作していなかった

  • PHP 5.3.7以前のPHPでもASCII文字(8ビット目が0)だけを利用していれば問題は発生しない

  • 正常なUTF-8エンコーディングでは脆弱にならない(未検証)

つまり「crypt関数なんて使えない関数だったから使ってないだろうし,使っていたとしても文字エンコーディングをバリデーションするかASCII文字だけに限定していれば問題ない」ということになります。

著者プロフィール

大垣靖男(おおがきやすお)

University of Denver卒。同校にてコンピュータサイエンスとビジネスを学ぶ。株式会社シーエーシーを経て,エレクトロニック・サービス・イニシアチブ有限会社を設立。
オープンソース製品は比較的古くから利用し,Linuxは0.9xのころから利用している。オープンソースシステム開発への参加はエレクトロニック・サービス・イニシアチブ設立後から。PHPプロジェクトでは,PostgreSQLモジュールのメンテナンスを担当している。

URLhttp://blog.ohgaki.net/

著書