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

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

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

今回もMOPS関連の話題です。MOPSではPHP関連のセキュリティ製品やセキュリティ知識の論文を募集し、11の論文が公開されました。今回は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

なぜPHPコード実行が起きるのか?

PHPコード実行が起きる原因は不適切なプログラムが原因であることがほとんどです。不適切なプログラムが作成されてしまう原因のほとんどはプログラマの知識不足です。⁠Our Dynamic PHP - Obvious and not so obvious PHP code injection and evaluation」⁠我々のダイナミックなPHP ─ 明白および不明確なPHPコードの挿入と実行)はタイトルのとおり「明白なコード実行」「不明確なコード実行」がどのように起きるのか解説しています。

今回の記事は、この文書に書かれている内容の要約に筆者の所見を追加した体裁となっています。

PHPにおけるコード実行

PHPは動的な言語であるため、eval関数へのパラメータに不正なコードを挿入し、不正なコードを実行することが可能です。PHPにおけるコード実行はeval関数へのコード挿入だけではありません。

明白なケース ─ eval関数

最も明白なケースはeval関数のパラメータへのコード挿入です。

<?php
eval("echo $foobar;");
?>

このコードの場合、ダブルクオートで囲まれたパラメータを渡しているので変数$foobarの内容はPHPが置換し、eval関数は$foobarの中身の文字列がPHPのコードとして評価されます。例えば、$foobarが

''; system('ls')

であれば、

echo '';  system('ls');

が実行されます。

<?php
eval('echo $foobar;');
?>

この場合、シングルクォートで囲まれた文字列をパラメータを渡しているので、eval関数は'echo $foobar'を評価します。結果は

<?php
echo $foobar;
?>

を実行した場合と変わりありません。

eval関数を実行した場合に気を付けなければならないことは、evalで評価したPHPコードで生成された変数は、evalの実行が終了した後にも残ってしまうことです。この仕様はセキュリティ上の問題となる可能性があります。Gerkis氏はeval関数のパラメータを適切にエスケープするのは非常に難しいので、できる限りevalの使用は避け、もしevalの使用が不可避な場合は文字リテラルを利用(つまり変数は利用しない)することを勧めています。変数が必要な場合は変数を初期化し、何か不具合があった場合に備えてユーザ定義エラーハンドラを使用すべきだ、としています。筆者もGerkis氏の意見に異論ありません。

明白なケース ─ スクリプトのインクルード

PHPはinlcude/include_once/require/require_once文でスクリプトを読み込むことができます。実は外部スクリプトの読み込みの問題はPHPに限ったことではないのですが、PHPが埋め込み型言語であるため、外部スクリプトの読み込みに対して簡単に脆弱になってしまいます。この問題についてはこの連載でも解説していますが、今回も解説します。

外部スクリプトの読み込みの問題は2つの種類に分類できます。

  • ローカルファイルの読み込み(LFI)
  • リモードファイルの読み込み(RFI)

この種類の攻撃を防ぐ最良の対策は「動的なパス」を利用しないことです。

Gerkis氏はファイルの読み込みに定数を利用することはよいアプローチだとしています。

<?php
define('APP_PATH', '/var/www/htdocs/');
require_once(APP_PATH . 'lib.php');
?>

そして、動的なファイル読み込みはすべきではないとしています。

<?php
$to_include = $_GET['file'];
require_once($to_include . '.html');
?>

これは典型的なファイルインクルード脆弱性です。文書の中ではDATA URIを利用した攻撃が紹介されています。上記のような脆弱なコードに対して

http://www.example.com/index.php?file=data:text/plain,<?php phpinfo();?>%00

等とするとPHPのコードが実行できてしまいます。DATA URIを利用した攻撃は脆弱なHTTPリダイレクトを利用したJavaScriptインジェクションの例が有名です。

Location: data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5jb29raWUpPC9zY3JpcHQ+

などとしてJavaScriptインジェクションが可能でした。この攻撃に脆弱なWebアプリケーションがあまりに多いため、現在ではブラウザがこのようなリダイレクトでJavaScriptを実行できないように対策を行っています(PHPに限らずRailsアプリなど、スクリプト系アプリの多くのリダイレクトが脆弱です⁠⁠。

これと同様の攻撃がPHPのinclude文に対して行えます。include文に対するDATA URIを利用した攻撃は非常に危険です。従来、ローカルファイル実行の問題とされていた脆弱性の問題の一部はリモートファイル実行と同様に任意のリモートからのスクリプトを実行可能です。

Perl互換正規表現

正規表現のコールバックを利用したコード実行は時折、脆弱性としてレポートされていました。時々アプリケーションの脆弱性としてレポートされているので、日本のブログでもこの正規表現のコールバックの問題を取り扱ってるブログを見かけたこともあります。本連載ではまだ取り扱っていなかった脆弱性なので、ほかの脆弱性より詳しく解説することにします。

PCRE(PERL互換正規表現)関数にはeモディファイアがあります。eモディファイアを利用すると正規表現にマッチした文字列にPHPの関数を適用することができます。

脆弱なPCREの利用
<?php
$var = '<tag>phpinfo()</tag>';
preg_replace("/<tag>(.*?)<\/tag>/e", 'addslashes(\\1)', $var);
?>

これを実行するとphpinfo関数が実行されてしまいます。これは<tag>(.*?)<\/tag>にマッチする文字列がPHPスクリプトとして評価されてしまうために起きる現象です。

正規表現

<tag>(.*)</tag>

が評価する文字列として

<tag>phpinfo()</tag>

を渡すと、

phpinfo()

がバックリファレンスの1番目にマッチした文字列になります。その結果、PHPがコードとして評価する文字列として

addslasshes(phpinfo())

が生成され、eval関数で実行した場合と同等に評価されます。この結果、意図しないphpinfo関数が実行されてしまいます。実はこのサンプルコードには間違いがあります。意図はどうあれ、addslashes関数のパラメータは文字列なので本来は

addslasshes('phpinfo()')

と処理されるよう

<?php
$var = '<tag>phpinfo()</tag>';
preg_replace("/<tag>(.*?)<\/tag>/e", "addslashes('\\1')", $var);
?>

と書くべきです。このコードを実行した場合はphpinfo()は文字列として扱われ関数呼び出しは発生しません。

ここで勘の良い読者は「ダブルクオートやシングルクォートがあるとどうなるのだろう?」と思ったと思います。実際に実行してみましょう。結果から分かるようにaddslashes関数をprintに書き換えます。

<?php
$var = "<tag>');phpinfo() //</tag>'";
preg_replace("/<tag>(.*?)<\/tag>/e", "print('\\1')", $var);
?>

これを実行すると

print('');phpinfo() //'

の文字列が生成されevalで評価されればphpinfo関数が実行されるはずですが、phpinfo()は実行されず。文字列が出力されます。

実行結果
');phpinfo() //

何故、コードとして評価されなかったかはPCREモジュールの中味を見れば分かります。

ext/pcre/php_pcre.c
                if (backref &lt; count) {
                    /* Find the corresponding string match and substitute it
                       in instead of the backref */
                    match = subject + offsets[backref<<1];
                    match_len = offsets[(backref<<1)+1] - offsets[backref<<1];
                    if (match_len) {
                        esc_match = php_addslashes_ex(match, match_len, &esc_match_len, 0, 1 TSRMLS_CC);
                    } else {
                        esc_match = match;
                        esc_match_len = 0;
                    }
                } else {

マッチした文字列に対してaddslashesの内部関数であるphp_addslashes_exを用いてエスケープ処理しています。このため、

print('');phpinfo() //')

とはならず、

print('\');phpinfo() //')

が評価されるのでphpinfo関数は実行されません。このコード実行は古くから知られているので知っている方も多いと思います。そもそも文字列を渡して処理した後の文字列を返すコールバック関数を定義する仕様であるため、この例のようなコードでは本来処理すべき文字列で構文エラーが発生するため、プログラムの開発途中でバグに気付くことが多いと思います。

もう少し現実的なコードは以下のようなコードでしょう。マッチした文字列をhtmlentities関数でHTMLエスケープしています。

<tag></tag>で囲まれた文字列をHTMLエスケープする
<?php
$var = '<tag><script>alert(1)</tag><script>alert(2)</script></tag>';
echo preg_replace("/<tag>(.*?)<\/tag>/e", "htmlentities('\\1', ENT_QUOTES, 'utf-8')", $var);
?>
出力結果
&lt;script&gt;alert(1)<script>alert(2)</script></tag>

正規表現に "?" がついており、(.*?) が最短一致になっているので上記のような結果になります。コード実行だけでなく、安易に正規表現を利用したセキュリティ処理を行うとセキュリティ問題の原因となるので注意が必要です。

eモディファイアが記載されていない場合でもコードが実行が可能な場合もあります。次のコードでは正規表現中の文字列に変数が利用されています。この変数に細工するとコード実行が可能になります。

<?php
$regexp = "<\/tag>/e\0";
$var = '<tag>phpinfo()</tag>';
preg_replace("/<tag>(.*?)$regexp<\/tag>/", '\\1', $var);
?>

preg_replaceの正規表現部分の文字列はバイナリセーフではありません。つまり、ヌル文字を挿入することにより文字列の終端となります。

$regexpを

$regexp = "<\/tag>/e\0";

と設定できれば、

preg_replace("/<tag>(.*?)$regexp<\/tag>/", '\\1', $var);

の正規表現

"/<tag>(.*?)$regexp<\/tag>/"

は$regexpの値で置き換えられ、

"/<tag>(.*?) <\/tag>/e\0<\/tag>/"

となり、ヌル文字で終了してしまい、

"/<tag>(.*?) <\/tag>/e"

と同じになります。このため、このコードを実行するphpinfo関数が実行されてしまいます。

元の文書では次のコードがPoCとして書かれています。

<?php
$regexp = $_GET['re'];
$var = '<tag>phpinfo()</tag>';
preg_replace("/<tag>(.*?)$regexp<\/tag>/", '\\1', $var);
?>

攻撃URLとして

http://www.example.com/index.php?re=<\/tag>/e%00

が紹介されています。magic_quotes_gpcが有効な場合、ヌル文字がエスケープされるためこの攻撃は成功しません。しかし、今時のアプリケーションでmagic_quotes_gpc=onを前提にアプリケーションを作ることは考えられません。

Gerkis氏は対策としてできる限り、ダブルクオートではなくシングルクォートを使うこと(特にバックリファレンスをシングルクォートで囲む⁠⁠、コールバック関数が必要な場合はpreg_replace関数よりpreg_replace_callback関数を使うこと、正規表現をエスケープするpreg_quote関数の利用を勧めています。

まとめ

Gerkis氏の論文をすべては紹介しきれていませんが、今回はここまでとします。

次回もGerkis氏の論文の続きを紹介します。

おすすめ記事

記事・ニュース一覧