▼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引数のクラス(クラス名で指定)またはそのサブクラスであるか否かを返します。trueis_subclass_of関数は、第1引数のクラス(インスタンスかクラス名で指定)が、第2引数のクラス(クラス名で指定)のサブクラスか否かを返します。falsePHPの__autoload関数PHPには、__autoload関数というものがあります。この関数は、未定義のクラスが参照された際に、そのクラスの定義を自動的に読み込めるようにするものです。結果的に、X.phpがインクルードされる前述のis_subclass_of関数の題意引数に指定したクラス名が、未定義クラスを指す場合も、__autoload関数が呼び出されます。PHP5.3.6以前では、インターフェースBを実装しているクラスImplBは、is_subclass_of関数はfalseを返し、ImplBのサブクラスについてはtrueを返していました。trueこれに対して、PHP5.3.7では、インターフェースについてもis_subclass_of関数はtrueを返すようになりました。上記の表示はいずれもがtrueを表示するように変わりました。PHP5.3.7において副作用的に発生したis_a関数の仕様変更インターフェースの扱いはCVE-2011-3379とは直接関係ないのですが、上記変更に伴って、is_a関数の仕様が変わりました。第1引数としてクラス名(文字列)が指定できるようになった第1引数に指定したクラス名が未定義クラスの場合、__autoload関数が呼び出されるようになったすなわち、以下の例では、__autoload('X')が暗黙に呼び出されます。__autoload('X')が暗黙に呼び出されるなぜ、このような変更が起こってしまったかという原因ですが、is_a関数とis_subclass_of関数は中味がよく似ているため、実装は共通であり、スイッチで処理内容が切り替わるようになっているためと思われます。具体的には、zend_builtin_functions.c内のis_a_impl関数の第2引数only_subclassの値でこの2つの関数を切り替えています。このため、is_subclass_of関数の変更の影響が、is_a関数にも及んだものと推測されます。#55475のスレッドでは、以下のような議論が行われました。これはこれで一貫した処理なのでは?でも、安定版の中で振る舞いが変わるのはやはりまずいよPEARだと、この影響でまずいことがあるこれはセキュリティバグだ。第1引数にURLが指定されると、リモートファイルインクルード(RFI)になるここでは、PEARの件と、セキュリティバグの件を紹介します。PEARライブラリFileオブジェクトの誤動作例PEARライブラリ中のFileオブジェクトには、readAllというメソッドがあります。以下に示すように、ファイル名を指定して呼び出すと、ファイルの中味が文字列として帰ります。エラーの場合はPEAR_Errorというクラスのインスタンスが返るので、PEAR::isErrorメソッドでエラー判定します。(PEAR::isError($content)){問題は、このエラー判定です。isErrorの実装をいかに示します。$code;}is_a関数で、PEAR_Errorクラスのインスタンスであるかどうかを比較していますが、$dataの中身が、「PEAR_Error」という文字列だったらどうでしょうか。'PEAR_Error')という呼び出しになりPHP5.3.7ではtrueが帰ります。結果として、isErrorはtrueを返してしまいます。ファイルの中身が「PEAR_Error」だったら、正常な読み込みができた場合でもエラーとして判定されてしまうことになります。そんなにしょっちゅうあるとは思えませんが、不完全なプログラムであることには間違いありません。これが脆弱性として発現する場合もあるのです。これがCVE-2011-3379です。Groenendal氏のブログ記事から、脆弱性のあるスクリプトを紹介します。$uploaded_file\n";}このスクリプトは、__autoload関数が定義されていて、未定義のクラスが参照された場合に、クラス名に.php拡張子をつけたファイルをインクルードするようになっています。処理の中身は、利用者がアップロードしたファイルを読み込んで、画面に表示するものです。PoCなので、XSS対策していないのは目をつぶることにしましょうwここで問題は、PEAR::isErrorメソッドが内部でis_a関数を読んでいるので、ファイルから読み込んだ文字列に対して、is_a関数が呼び出されることです。スクリプトが読み込まれて実行されてしまうことになります。これはリモートファイルインクルード攻撃(RFI)の成功です。Offの場合でも、利用者がアップロード機能を利用して、拡張子がphpのファイルにPHPスクリプトを書き込むことができ、そのファイル名を外部から知ることができれば、ローカルファイルインクルード攻撃(LFI)が可能です。利用者がPHPスクリプトをアップロードする。インクルードされ、スクリプトが実行される拡張子がphpで、中味がPHPスクリプトというファイルをアップロードできること自体がとても恐ろしいアプリケーションであるわけですが、外部からこのスクリプトを起動できなければ、LFIは成立しません。is_a関数の仕様変更により、「元々は外部から起動できないはずのスクリプトが起動できてしまう」ことが問題です。上記シナリオ以外にもis_a関数が__autoload関数を呼び出すシナリオがあるかもしれませんが、それを考え出すときりがないので、上記シナリオの場合に、攻撃が成立する条件を考えます。それは以下をすべて満たす場合です。対策は以下の通りです。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側で対策すべきと考えます。