2016年12月15日木曜日

PHPの全バージョンの挙動をCGIモードで試す

PHPの挙動を調べていると、マニュアルにも、ChangeLogにも載っていない変更にしばしば遭遇します。たとえば、PCRE系関数(preg_xxxx)の正規表現指定(第1引数)において、過去のPHPではNULLバイトを許容していましたが、最近のPHPでは、正規表現中のNULLバイトをエラーにしています。この変更は、マニュアルには載っておらず、ChangeLogには記載されているもののNULLバイトとは書いていないので、ちょっと気がつきにくいですね。
Fixed bug #55856 (preg_replace should fail on trailing garbage)
このような場合、ソースコードの該当箇所を調べるか、適当にあたりをつけたバージョンのPHPをビルドして試すなどの手法がとられているかと思いますが、@hnwさんが 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);
phpallを用いると、以下の実行結果を得ます。
$ 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 ...
これにより、PCRE関数で正規表現のNULLバイトチェックが入ったのは PHP 5.4.7であることが一目瞭然分かるわけです。
しかし、phpallでも調査できないような課題があります。それはHTTP固有の問題、たとえばHTTPヘッダやCookie、セッション等の問題です。また、$_GETや$_POSTの挙動もCLI版のPHPでは把握ができません。

ウェブアプリケーションのセキュリティを専門とする立場からは、前述の課題は、下記の脆弱性に関連するものであり、決して軽視はできません。
  • HTTPヘッダインジェクション
  • セッション固定
  • セッションアダプション
  • セッション汚染
  • その他セッション系脆弱性
どうしたらこれを調べられるかと考えているうちに、全てのPHPをCGIモードで動かせばいいというアイデアに至りました。名付けて phpcgiall の誕生です。

phpcgiallの動作原理

動作原理といっても、単に全てのPHPをCGIモードで動かすだけですが、テストのしやすさなども考慮して、以下のような設定を用いています。

まず、各バージョンのPHPに関する設定ですが、以下のような Apache設定ファイルをバージョン毎にジェネレートしています。以下は PHP 5.6.29の例です。これら設定ファイルは、~/phpcgi.confディレクトリに置かれます。CGI版のPHPバイナリは、~/phpcgi/ ディレクトリに置かれます。また、PHPスクリプト(コンテンツ)は、私のDropboxフォルダ上の共通のディレクトリ上に配置しています。
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>
これに対して、共通の設定として、下記を httpd.confの末尾に入れています。
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-5.6.29/phpinfo.php にアクセスすると、~/Dropbox/www/phpinfo.phpをPHP 5.6.29が実行した結果を返すことになります。



実際には、各バージョンのPHPの呼び出しは、curlコマンドを呼び出す簡単なPythonスクリプトを用いています。

継続行とヘッダインジェクション

それでは、phpcgiallを用いて、PHPのHTTPヘッダインジェクション対策の変遷について調べてみましょう。
PHPのheader関数はPHP-5.1.2で改行のチェックが入り、HTTPヘッダインジェクションができなくなっているはずでしたが、実際には以下の抜けがありました。
  • キャリッジリターン(\x0D)のみで攻撃可能なブラウザがあった
  • IE限定だが、継続行(LWS)を用いた攻撃ができた
いずれも最新版のPHPでは解消されています。このうち、継続行の問題の方を phpcgiall で確かめてみましょう。
継続行を用いた HTTPヘッダインジェクションとは、以下のように改行の後に空白を用いる継続行(Linear White Space)を用いるものです。
header("Location: http://exapmle.jp/\r\n Set-Cookie: a=b;");
すると、古いPHPでは、以下のように「継続行」形式として改行チェックをスルーしてしまいます。
Location: http://example.jp/
 Set-Cookie: a=b;
ブラウザ側で「継続行」として一つのヘッダとして認識してくれれば問題はないのですが、IEは上記を2つのヘッダとして認識するために、Set-Cookieヘッダが追加された形になります。詳しくは、ブログ記事PHPにおけるHTTPヘッダインジェクションはまだしぶとく生き残るを参照下さい。

それでは、phpcgiallにてこの問題を検証してみましょう。PoCは下記です。
<?php
  header("Location: http://example.jp/\r\n Set-Cookie: a=b;");
実行結果は下記の通りです。ログファイルからWarningの表示を確認しています。
$ 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 ...
上記から、header関数で「継続行」が禁止されたのは、PHP 5.4.38、5.5.22、5.6.6、7.0.0であることがわかります。

キャリッジリターンのみを用いたHTTPヘッダインジェクションはどうか?

次に、キャリッジリターン(\x0D)のみを用いたHTTPヘッダインジェクションについて調べてみましょう。PoCは下記となります。
<?php
  header("Location: http://example.jp/\rSet-Cookie: a=b;");
これに対して、問題があることが確実なバージョンとして、PHP 5.3.0での結果を見てみましょう。キャリッジリターンをわかりやすく表示するために、sedによりキャリッジリターンを [CR] と変換して表示しています。
$ 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]
あれあれ? Set-Cookieヘッダ(赤字)の前にキャリッジリターンがありません。これでは、Set-Cookieが独立したヘッダとして、ブラウザに認識されません。
試みに、CGI版のPHPをコマンドラインから起動して、PHPの出力を見てみましょう。
$ ~/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]
今度は、Set-Cookieヘッダの前にキャリッジリターンがありますね。
どうやら、ApacheがCGIインターフェースを処理する際に、ラインフィード(0x0A)を伴わない単独のキャリッジリターン(0x0D)を削除してしまうようです。
ということで、キャリッジリターンのみを用いたHTTPヘッダインジェクションのテストは、CGIプログラムの形ではうまくいかないことがわかりました。Warningから該当バージョンは追えますが、脆弱な挙動として現れないのはちょっと残念ですね。

まとめ

@hnwさんのphpallを拡張する形で、PHPの前バージョンをCGIモードで動かす環境 phpcgiall を作成しました。
概ね期待通りの結果を得られましたが、HTTPヘッダの微妙な挙動については、Apacheモジュール版とCGI版では微妙な差があり、検証に注意を要することが課題と言えます。
(続く)

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ