この投稿は PHP Advent Calendar 2016の16日目の記事です。
それに、やはりPHPの本流はCGI版ではなく、Apacheモジュール版であろという思いから、なんとかphpallのApacheモジュール版はできないだろうかと考えるようになりました。
基本的にアイデアは3つありました。
PHP 3.0.18(PHP 3の最終バージョン)から最新のPHP 7.1.0 まで、229バージョンのPHPバイナリができあがりました。
以下のような設定フアイルのテンプレートをPHPのバージョン毎に用意しています。
他のバージョンも含めると、問題が発生するバージョンは下記となります(見やすくなるように表示の順序を入れ替えています)。
エグゼクティブサマリ
PHPのバージョン間の挙動の違いを調査するツールとして、@hnwによるphpallや、それを改造したphpcgiallがあったが、現実のPHPの利用環境とは違いがあり、検証の妨げになる場合があった。このため、PHPのバージョン毎にApacheを異なるポートで動かすことにより、全てのバージョン(229種)のPHPをApacheモジュールとして動作させることに成功し、modphpallと命名した。modphpallはPHPの検証に有効であることを、キャリッジリターンのみで起こるPHPヘッダインジェクションを用いて確認した。はじめに
昨日の日記では、PHPの全バージョンをCGIモードで試す phpcgiall について紹介しました。HTTPヘッダインジェクションやセッションの挙動について確認するためには、CLI版のPHPでは限界があり、ウェブスクリプトとして全バージョンのPHPを試すことができる phpcgiall は有効であるものの、ヘッダの微妙な挙動については、Apacheモジュール版と差異があることがわかりました。
それに、やはりPHPの本流はCGI版ではなく、Apacheモジュール版であろという思いから、なんとかphpallのApacheモジュール版はできないだろうかと考えるようになりました。
基本的にアイデアは3つありました。
- Apacheに同時に複数バージョンのPHPを組み込めるようにする
- Apacheに組み込むPHPを次々に切り替える
- Apacheのインスタンスを複数起動して、それぞれ異なるバージョンのPHPを動かす
PHPのビルド
既にCLI版とCGI版のPHP版があるので、Apacheモジュール版のPHPをビルドすることには、さほど困難はありませんでした。ただ、時間はそれなりにかかりますね。PHP 3.0.18(PHP 3の最終バージョン)から最新のPHP 7.1.0 まで、229バージョンのPHPバイナリができあがりました。
Apacheのバージョン
使用するApacheバージョンは、あまり良く考えずに、PHP 3とPHP 4はApache1.3.42(1.3系の最終バージョン)、PHP 5以降はApache2.2.31(2.2系の最新)を用いましたが、これで特に問題は出ていません。ディレクトリ構成
ディレクトリ構成は下記のとおりです。~/apache1.3 | PHP 3, 4向け。Apache 1.3.42のバイナリ、コンフィグレーション |
~/apache2.2 | PHP 5, 7向け。Apache 2.2.31のバイナリ、コンフィグレーション |
~/phplib | PHP 3, 4, 5, 7のApacheモジュール版バイナリ |
~/Dropbox/www | ドキュメントルート。PHPスクリプトを配置 |
設定ファイル
PHPのバージョンの数だけ、ポート番号を変えてApacheを起動することになるため、設定ファイル(httpd.conに相当するもの)は、簡単なスクリプトでジェネレートしています。以下のような設定フアイルのテンプレートをPHPのバージョン毎に用意しています。
そして、スクリプトで%PORT%と%PHPVER%を書き換えて、以下のような設定ファイルを生成します。Include "conf/httpd_header.conf"
Listen %PORT%
LoadModule php5_module /home/ockeghem/phplib/%PHPVER%.so
ServerName phpcgiall:%PORT%
PidFile "logs/httpd_%PHPVER%.pid"
Include "conf/httpd_tail.conf"
そして、同じスクリプトで、以下のような形で起動します。Include "conf/httpd_header.conf"
Listen 49364
LoadModule php5_module /home/ockeghem/phplib/php-5.6.29.so
ServerName phpcgiall:49364
PidFile "logs/httpd_php-5.6.29.pid"
Include "conf/httpd_tail.conf"
起動後、phpinfo()の表示例を示します。apache2.2/bin/httpd -f conf/httpd_php-5.6.29.conf
使用メモリ量
同一サーバー内で229ものApacheインスタンスを起動するので、メモリ使用量が心配になります。手元の環境は、Ubuntu 12.04LTS (32ビット)ですが、VMに割り当てるメモリを当初21Gバイトとしていました。現実には2Gバイト程度でも一応動くようです。現在、暫定的に8Gバイトのメモリを割り当てていますが、起動当初の状態は下記となります。$ free
total used free shared buffers cached
Mem: 8266464 3794592 4471872 0 48444 1938060
-/+ buffers/cache: 1808088 6458376
Swap: 2093052 0 2093052
$
使ってみる
それでは、modphpallを試してみましょう。PoCは、昨日の日記で示したキャリッジリターンのみを用いたHTTPヘッダインジェクションです。PHP 5.3.0による実行結果は下記のとおりです。昨日同様、キャリッジリターン(0x0D)をsedにより[CR]と変換して表示しています。<?php
header("Location: http://example.jp/\rSet-Cookie: a=b;");
めでたく(?)Set-Cookiヘッダの前にキャリッジリターンが出力されており、一部のブラウザ(IE、Google Chrome、Safari等)ではHTTPヘッダインジェクションが起こる結果が出ました。$ curl --dump-header - http://localhost:49220/headerinj-cr.php | sed 's/\r/[CR]/'
HTTP/1.1 302 Found[CR]
Date: Thu, 15 Dec 2016 10:20:07 GMT[CR]
Server: Apache/2.2.31 (Unix) PHP/5.3.0[CR]
X-Powered-By: PHP/5.3.0[CR]
Location: http://example.jp/[CR]Set-Cookie: a=b;
Content-Length: 0[CR]
Content-Type: text/html[CR]
[CR]
他のバージョンも含めると、問題が発生するバージョンは下記となります(見やすくなるように表示の順序を入れ替えています)。
キャリッジリターンのみでHTTPヘッダインジェクションが起こるのは、PHP 5.3.10までであることがわかります。$ grep Set-Cookie *.log | sed 's/\r/[CR]/'
php-3.0.18.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-4.4.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-5.3.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.3.10.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
$