Fixed bug #55856 (preg_replace should fail on trailing garbage)このような場合、ソースコードの該当箇所を調べるか、適当にあたりをつけたバージョンのPHPをビルドして試すなどの手法がとられているかと思いますが、@hnwさんが phpall を発表されたことで、この種の調査が一挙に楽になりました。
例えば、以下のようなサンプルスクリプトを用意して、
phpallを用いると、以下の実行結果を得ます。<?php ini_set('display_errors', 'On'); $vul = 0; function vul($x) { echo "vulnerable\n"; exit; } echo 'version=' . phpversion() . "\n"; $x = preg_replace("/^test/e\0/", "vul('\\0');", "test"); echo "not vulnerable\n"; var_dump($x);
これにより、PCRE関数で正規表現のNULLバイトチェックが入ったのは PHP 5.4.7であることが一目瞭然分かるわけです。$ phpall regexpinj.php ... 略 php-5.4.4: version=5.4.4 vulnerable php-5.4.5: version=5.4.5 vulnerable php-5.4.6: version=5.4.6 vulnerable php-5.4.7: version=5.4.7 Warning: preg_replace(): Null byte in regex in ... php-5.4.8: version=5.4.8 Warning: preg_replace(): Null byte in regex in ... php-5.4.9: version=5.4.9 Warning: preg_replace(): Null byte in regex in ...
しかし、phpallでも調査できないような課題があります。それはHTTP固有の問題、たとえばHTTPヘッダやCookie、セッション等の問題です。また、$_GETや$_POSTの挙動もCLI版のPHPでは把握ができません。
ウェブアプリケーションのセキュリティを専門とする立場からは、前述の課題は、下記の脆弱性に関連するものであり、決して軽視はできません。
- HTTPヘッダインジェクション
- セッション固定
- セッションアダプション
- セッション汚染
- その他セッション系脆弱性
phpcgiallの動作原理
動作原理といっても、単に全てのPHPをCGIモードで動かすだけですが、テストのしやすさなども考慮して、以下のような設定を用いています。まず、各バージョンのPHPに関する設定ですが、以下のような Apache設定ファイルをバージョン毎にジェネレートしています。以下は PHP 5.6.29の例です。これら設定ファイルは、~/phpcgi.confディレクトリに置かれます。CGI版のPHPバイナリは、~/phpcgi/ ディレクトリに置かれます。また、PHPスクリプト(コンテンツ)は、私のDropboxフォルダ上の共通のディレクトリ上に配置しています。
これに対して、共通の設定として、下記を httpd.confの末尾に入れています。Alias /php-5.6.29/ "/home/ockeghem/Dropbox/www/" <Location /php-5.6.29> AddHandler application/x-httpd-php-5.6.29 .php Action application/x-httpd-php-5.6.29 /php-cgi/php-5.6.29 Options Indexes FollowSymLinks Order allow,deny allow from all </Location>
この設定により、/php-5.6.29/phpinfo.php にアクセスすると、~/Dropbox/www/phpinfo.phpをPHP 5.6.29が実行した結果を返すことになります。Alias /d/ "/home/ockeghem/Dropbox/www/" <Directory "/home/ockeghem/Dropbox/www"> Options Indexes FollowSymLinks AllowOverride None Order allow,deny Allow from all </Directory> ScriptAlias /php-cgi/ /home/ockeghem/phpcgi/ <Directory "/home/ockeghem/phpcgi"> AllowOverride None Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch Order allow,deny Allow from all </Directory> Include /home/ockeghem/phpcgi.conf/
実際には、各バージョンのPHPの呼び出しは、curlコマンドを呼び出す簡単なPythonスクリプトを用いています。
継続行とヘッダインジェクション
それでは、phpcgiallを用いて、PHPのHTTPヘッダインジェクション対策の変遷について調べてみましょう。PHPのheader関数はPHP-5.1.2で改行のチェックが入り、HTTPヘッダインジェクションができなくなっているはずでしたが、実際には以下の抜けがありました。
- キャリッジリターン(\x0D)のみで攻撃可能なブラウザがあった
- IE限定だが、継続行(LWS)を用いた攻撃ができた
継続行を用いた HTTPヘッダインジェクションとは、以下のように改行の後に空白を用いる継続行(Linear White Space)を用いるものです。
すると、古いPHPでは、以下のように「継続行」形式として改行チェックをスルーしてしまいます。header("Location: http://exapmle.jp/\r\n Set-Cookie: a=b;");
ブラウザ側で「継続行」として一つのヘッダとして認識してくれれば問題はないのですが、IEは上記を2つのヘッダとして認識するために、Set-Cookieヘッダが追加された形になります。詳しくは、ブログ記事PHPにおけるHTTPヘッダインジェクションはまだしぶとく生き残るを参照下さい。Location: http://example.jp/ Set-Cookie: a=b;
それでは、phpcgiallにてこの問題を検証してみましょう。PoCは下記です。
実行結果は下記の通りです。ログファイルからWarningの表示を確認しています。<?php header("Location: http://example.jp/\r\n Set-Cookie: a=b;");
上記から、header関数で「継続行」が禁止されたのは、PHP 5.4.38、5.5.22、5.6.6、7.0.0であることがわかります。$ grep Warning * php-5.4.38.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... php-5.4.39.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... ... php-5.5.22.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... php-5.5.23.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... ... php-5.6.6.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... php-5.6.7.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... ... php-7.0.0.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ... php-7.0.1.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
キャリッジリターンのみを用いたHTTPヘッダインジェクションはどうか?
次に、キャリッジリターン(\x0D)のみを用いたHTTPヘッダインジェクションについて調べてみましょう。PoCは下記となります。これに対して、問題があることが確実なバージョンとして、PHP 5.3.0での結果を見てみましょう。キャリッジリターンをわかりやすく表示するために、sedによりキャリッジリターンを [CR] と変換して表示しています。<?php header("Location: http://example.jp/\rSet-Cookie: a=b;");
あれあれ? Set-Cookieヘッダ(赤字)の前にキャリッジリターンがありません。これでは、Set-Cookieが独立したヘッダとして、ブラウザに認識されません。$ curl --dump-header - http://localhost/php-5.3.0/headerinj-cr.php | sed 's/\r/[CR]/' HTTP/1.1 302 Moved Temporarily[CR] Date: Wed, 14 Dec 2016 12:55:39 GMT[CR] Server: Apache/2.2.22 (Ubuntu)[CR] X-Powered-By: PHP/5.3.0[CR] Location: http://example.jp/Set-Cookie: a=b;[CR] Vary: Accept-Encoding[CR] Content-Length: 0[CR] Content-Type: text/html[CR] [CR]
試みに、CGI版のPHPをコマンドラインから起動して、PHPの出力を見てみましょう。
今度は、Set-Cookieヘッダの前にキャリッジリターンがありますね。$ ~/phpcgi/php-5.3.0 headerinj-cr.php | sed 's/\r/[CR]/' Status: 302 Moved Temporarily[CR] X-Powered-By: PHP/5.3.0[CR] Location: http://example.jp/[CR]Set-Cookie: a=b; Content-type: text/html[CR] [CR]
どうやら、ApacheがCGIインターフェースを処理する際に、ラインフィード(0x0A)を伴わない単独のキャリッジリターン(0x0D)を削除してしまうようです。
ということで、キャリッジリターンのみを用いたHTTPヘッダインジェクションのテストは、CGIプログラムの形ではうまくいかないことがわかりました。Warningから該当バージョンは追えますが、脆弱な挙動として現れないのはちょっと残念ですね。
まとめ
@hnwさんのphpallを拡張する形で、PHPの前バージョンをCGIモードで動かす環境 phpcgiall を作成しました。概ね期待通りの結果を得られましたが、HTTPヘッダの微妙な挙動については、Apacheモジュール版とCGI版では微妙な差があり、検証に注意を要することが課題と言えます。
(続く)
0 件のコメント:
コメントを投稿