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

第38回MOPS:PHPにおけるコード実行(2)

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

前回もArthur Gerkis氏が投稿したPHPにおけるコード実行を解説した文書を紹介しました。今回はその続きです。

MOPS Submission 07: Our Dynamic PHP - Obvious and not so obvious PHP code injection and evaluation
http://www.php-security.org/2010/05/20/mops-submission-07-our-dynamic-php/index.html

動的コード

動的コードでのコード実行には「任意コードの実行」のみでなく「不正なコード実行パス」も含めて議論しています。

動的変数

ブラウザからの入力をグローバル変数として初期化するregister_globals設定がデフォルトでoffになってから久しいですが、レガシーコードの中にはregister_globals=onをエミュレートするプログラムも存在します。register_globals=on時にセキュリティ上問題となるコードは十分に議論されてきました。次のコードはregister_globals=onの場合に発生する典型的な脆弱性の例です。

register_globals=onをエミュレートするコード
<?php
foreach ($_GET as $key => $value) {
   $$key = $value;
}
// ... some code
if (logged_in() || $authenticated) {
   // ... administration area
}
?>

このコードに対して

http://www.example.com/index.php?authenticated=true

とアクセスすると、logged_in()がfalseを返しても$authenticatedがtureであるため、if文のコードが不正に実行されます。

PHP 4.2でregister_globals=offがデフォルトとなってから久しいので、現在ではあまり見かけないPHPアプリケーションの脆弱性ですが、古いコードでは時折上記のようなコードを見かけます。PHP 5.4ではregister_globals設定自体が削除されます。上記のようなコードを持つアプリケーションやregister_globals=onが必要なアプリケーションは早めに改修すべきです。

動的な関数

PHPには変数の中に保存された文字列を関数名として呼び出す、可変関数と呼ばれる機能があります。この機能を利用すると任意コード実行が可能となる場合があります。

可変関数の利用
<?php
$dyn_func = $_GET['dyn_func'];
$argument = $_GET['argument'];
$dyn_func($argument);
?>

このスクリプトに次のURLでアクセスすると、system関数でさまざまなコマンドを実行されてしまいます。

http://www.example.com/index.php?dyn_func=system&argument=uname

次はcreate_funciton関数でのコード実行の例です。

<?php
$foobar = "system('ls')";
$dyn_func = create_function('$foobar', "echo $foobar;");
$dyn_func('');
?>

このコードを実行するとsystem関数でlsコマンドが実行されます。create_function関数がeval関数のラッパーとして実装されているのでこのような動作になります。evel関数で記述した場合、次のような動作になります。

<?php
eval("function lambda_n() { echo system('ls'); }");
lambda_n();
?>

このように動作することはPHPマニュアルのcreate_function関数のページにも記載されています。

例1 create_function() による匿名関数の作成
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "新しい匿名関数: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// 出力
// 新しい匿名関数: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>

{}構文

PHPは変数とテキストを分けるための{$var_name}構文があります。通常は

<?php
$var_name = '変数';
echo "これは日本語のテキストに{$var_name}を埋め込んでいます";
?>

日本語のように単語の区切りが無いテキストに変数を埋め込むためには必須の構文です。この構文を利用すると変わったコード実行が可能になります。

<?php
$var = "I was innocent until ${`ls`} appeared here";
?>

このコードを実行すると`ls`をPHPスクリプト中で実行したようにlsコマンドを実行します。PHPがこのような動作するのは${}の中に入っている文字列をPHPのコードとして評価しているからです。${`ls`}はlsを実行した結果を変数名として利用しようとします。

これを利用すると、次のようなphpinfo関数を実行するコードが書けます。

<?php
$foobar = 'phpinfo';
${'foobar'}();
?>

この脆弱性だけではセキュリティ上の脅威となりませんが、フィルタを潜り抜けるpreg_replace関数の攻撃やコードと文字列が混ざっている場所での攻撃利用されます。多くの正規表現に対する攻撃はこのテクニックを利用しています。

コード実行可能だが稀なケース

ob_start関数はフラッシュするためのコールバック関数を登録可能です。次のコードはsystem('uname')を実行します。

出力バッファを利用するコード実行
<?php
$foobar = 'system';
ob_start($foobar);
echo 'uname';
ob_end_flush();
?>

$foobarと出力バッファの内容を制御できる場合はコード実行攻撃を行えます。

assert関数はeval関数と同様の動作をするので簡単に攻撃可能ですが、脆弱性となることは稀です。

assert関数を利用するコード実行
<?php
$foobar = 'system("uname")';
assert($foobar);
?>

配列関数の多くがコールバック関数をサポートしています。脆弱性となるケースは稀ですがコールバック関数を利用してコード実行が可能です。

<?php
$evil_callback = 'phpinfo()';
$some_array = array(0, 1, 2, 3);
$new_array = array_map($evil_callback, $some_array);
?>

$evil_callbackが制御可能な場合、phpinfo関数など攻撃に利用可能な関数を呼び出せます。

XML関数やSTREAM関数もコールバック関数を利用可能であるため配列関数と同様の手法で攻撃される可能性があります。

コールバックをサポートする関数
ob_start()

preg_replace()
preg_replace_callback()

array_map()
usort(), uasort(), uksort()
array_filter()
array_reduce()
array_diff_uassoc(), array_diff_ukey()
array_udiff(), array_udiff_assoc(), array_udiff_uassoc()
array_intersect_assoc(), array_intersect_uassoc()
array_uintersect(), array_uintersect_assoc(), array_uintersect_uassoc()
array_walk(), array_walk_recursive()

xml_set_character_data_handler()
xml_set_default_handler()
xml_set_element_handler()
xml_set_end_namespace_decl_handler()
xml_set_external_entity_ref_handler()
xml_set_notation_decl_handler()
xml_set_processing_instruction_handler()
xml_set_start_namespace_decl_handler()
xml_set_unparsed_entity_decl_handler()

stream_filter_register()
set_error_handler()
set_exception_handler()
register_shutdown_function()
register_tick_function()

その他の手法

次のPoCは実際には動作しませんが、オブジェクトのデストラクタを利用した攻撃方法を解説するコードです。$varに攻撃用のコードを埋め込み、unserialize関数の引数とすれば任意コード実行が可能となります。

<?php
class Example {
   var $var = '';
   function __destruct() {
      eval($this->var);
   }
}
unserialize($_GET['saved_code']);
?>

この攻撃を成功させるには攻撃用のクラス定義を読み込ませるか、既存のクラス定義を利用する必要があります。この攻撃を成功させることは非常に難しいですが、実際にこの脆弱性を利用した攻撃が知られています。

MOPBでStefan Esser氏はunserialize関数のメモリエラーを利用した攻撃方法を紹介すると同時に、シリアライズした如何なるデータもユーザから受け取るべきではない、と指摘していました。当時はまだこのような攻撃手法は知られていませんでしたが、ハンドラをサポートしたデータ型のデータを外部から受け取るリスクを考慮しての意見だと思います。

複数のデータやオブジェクトや配列を文字列として保存できるシリアライズ機能は便利ですがリスクが存在することも知っておくとよいでしょう。

Gerkis氏が参考文献としたURLの一覧

  1. http://www.hardened-php.net/suhosin/
    Suhosin, advanced protection system for PHP
  2. http://projects.webappsec.org/Remote-File-Inclusion
    explanation of RFI
  3. http://tools.ietf.org/html/rfc2397
    The ⁠data⁠⁠ URL scheme
  4. http://www.php.net/manual/en/wrappers.data.php
    Data (RFC 2397), PHP manual
  5. http://www.ush.it/2008/08/18/lfi2rce-local-file-inclusion-to-remote-code-execution-advanced-exploitation-proc-shortcuts/
    how LFI can lead to RCE
  6. http://www.exploit-db.com/papers/260
    how LFI can lead to RCE (2)
  7. http://www.sektioneins.com/en/advisories/index.html
  8. まとめ

    Gerkis氏は、ソースコード中にコードを埋め込む場合はかならず悪意があるコードが実行されることを考え、決してユーザを、たとえ管理者であっても信用してはならないとしています。ユーザ入力を正しくエスケープすることは非常に難しく、見落とす可能性は非常に大きいので、ブラックリスト型の対策は常に悪い方法であると認識すべきだとしています。筆者もGerkis氏の意見とまったく同じです。

    安全なソースコードを書くためにはホワイトリスト型の対策でユーザ入力を検査すべきで、ホワイトリスト型のほうがよほど簡単に十分なセキュリティを確保できます。

    開発者にとって便利な機能はセキュリティリスクとなることがよくあります。セキュリティとは利便性とのトレードオフの関係にあるので当然です。動的言語は非常に便利な反面、リスクがあることを理解して使う必要があります。

    次回はMOPSで紹介された静的にソースコードを分析する脆弱性スキャナ(RIPS)を紹介します。

おすすめ記事

記事・ニュース一覧