PHPがサポートするURLスキームを調査する

この記事はm1z0r3 Advent Calendar 2018の13日目です。

突然ですが皆さんphp://から始まるURLをご存知ですか?
見て分かる通り PHP固有のプロトコルスキームです。

http://php.net/manual/ja/wrappers.php
上のリンクに示されているようにPHPではphp://以外にも複数のURLスキームをサポートするため、本記事ではphpスキームをメインに何ができるのかを検証しました。

この記事のモチベーション

CTFでfile_get_contents($_GET['page']);と記述されたPHPファイルに自分自身を食べさせるという問題がありました。
この関数はファイルだけでなくURLも認識するため、特にオプションを設定せずともURLだけでどこまでPHPが実行可能なのかということを調べます。

php://スキーム

php://で提供されるのは入出力ストリームです。

php://stdin, php://stdout, php://stderr

それぞれ標準入力、標準出力、標準エラー出力の単方向ストリームを示します。ドキュメントによると識別子のコピーを参照するだけなのでfcloseしても本当に閉じるわけではなさそうですね。

php://input, php://output

php://inputはHTTPリクエストのBodyにアクセスする読み込みストリームです。
php://outputは出力バッファにアクセスする書き込みストリームです。これはWebサーバならレスポンスボディを示し、CLIなら標準出力を示します。

上記のストリームを雑に組み合わせてみました。
例えばこのthru.phpは入力をそのまま標準出力に流します。

$ cat thru.php
<?php
file_put_contents("php://output", file_get_contents("php://stdin"));

$ echo abcde | php thru.php
abcde

php://fd

ファイル識別子番号nphp://fd/nと与えることでその識別子へのストリームを構成します。
標準入力は0、標準出力は1、標準エラー出力は2ですから、thru.phpは以下でも同じ動作をします。

<?php
file_put_contents("php://fd/1", file_get_contents("php://fd/0"));

php://memory, php://temp

どちらもメモリを一時ファイルのように扱える読み書きストリームです。
ただし、php://tempは2MBを超えると実際に一時ファイルを作成します。
php://tempのメモリ上限をnnバイトに変更したい場合はphp://temp//maxmemory:nnと記述することができます。

php://filter

お待たせしました。本日のメインディッシュです。
これはファイルなどにアクセスする際にフィルタを通すことができるように設計されたラッパーです。
雑な使用例だと「ファイルには常にbase64で書き込まれてほしいから、書き込む時は変換して読み込む時は復元する!」といった具合でしょうか。
どんなフィルタがあるかは後述しますが、まずは使い方を抑えます。

1. 何もしない
php://filter/resource=ファイル
2. 読み込む時にフィルタを通す
php://filter/read=読み込みフィルタ/resource=ファイル
3. 書き込む時にフィルタを通す
php://filter/write=書き込みフィルタ/resource=ファイル
4. 読み込みと書き込みでフィルタを分ける
php://filter/read=読み込みフィルタ/write=書き込みフィルタ/resource=ファイル
5. 読み書きどちらにも同じフィルタを通す
php://filter/フィルタ/resource=ファイル
6. フィルタ1を通した後にフィルタ2を通す
php://filter/read=フィルタ1|フィルタ2/resource=ファイル

それでは使えるフィルタを見て行きます。まずは(環境依存かもしれないのですが)stream_get_filter関数を叩いて何ができるのか見てみましょう。

$ php -a
Interactive shell

php > print_r(stream_get_filters());
Array
(
    [0] => zlib.*
    [1] => bzip2.*
    [2] => convert.iconv.*
    [3] => string.rot13
    [4] => string.toupper
    [5] => string.tolower
    [6] => string.strip_tags
    [7] => convert.*
    [8] => consumed
    [9] => dechunk
)

なんかいっぱいあるけど*で省略されててわからん!
わかりやすいものから順番に説明します!

string.*

文字列フィルタです。
ファイルの読み書きの際に勝手に文字列を変換してくれるようになります。
ほぼ見て分かるのですが、rot13はROT13変換、toupperは大文字変換、tolowerは小文字変換、strip_tagsはHTMLタグやコメントの除去です。

URLの組み方は先述のフォーマットに従います。
具体的で簡単な使用例を以下に示します。

$ php -a
Interactive shell

php > $fp = fopen("php://filter/read=string.tolower/write=string.toupper/resource=upper.txt", "r+");
php > fwrite($fp, "ABCDefgh\n");
php > echo file_get_contents("upper.txt");
ABCDEFGH
php > rewind($fp);
php > echo fread($fp, filesize("upper.txt"));
abcdefgh

このように読み込み時にはstring.tolower、書き込み時にはstring.toupperを適用すると、"ABCDefgh"を書き込んだら"ABCDEFGH"になり、読み込んだら"abcdefgh"になりました。
string.strip_tags|string.tolowerのようにフィルタを繋げることもできるので、タグを除去してから小文字にするといった複数の処理を一度に勝手に行ってもらうことができます。

convert.*

convert.*ってスターがついてるからさぞかしいっぱいあるのかと思ったら、2種類の変換復元しかないのかい!こちとらソースまで調べに行ったわ!
というわけでbase64quoted-printableの2種類です。

フィルタ名にconvert.base64-encodeと書けばbase64エンコード、convert.base64-decodeと書けばbase64デコードできます。
convert.quoted-printable-encodeconvert.quoted-printable-decodeも同様です。

convert.iconv.*

これはiconvで想像ついた方がいるかもしれませんが、文字コード変換を行ってくれます。
使い方は、convert.iconv.変換前の文字コード/変換後の文字コードです。
ここで注意するのは、このフィルタ名にURLエンコード(PHPならurlencode関数)をかけなければいけないことです。スラッシュが余計に出てくるので、パースに異常がないようにします。
面倒ならスラッシュを%2Fに置き換えればとりあえず大丈夫ですね。
以下が使用例です。文字コードの一覧はここを参考にしてください。

php://filter/convert.iconv.ISO-2022-JP%2FUTF-8/resource=mojibake.txt

zlib.*

これもzlib.*と書かれていたので色々あるのかと思って調べたらdeflateinflateしかありませんでした。
zlib.deflateは圧縮、zlib.inflateは展開ですね。

bzip2.*

bzip2はbzip2.compressbzip2.decompressの2種類です。
それぞれ圧縮・展開です。

dechunk

PHPのhttp_chunked_decode()に相当する変換をします。chunkedってなんだよって思ってしまったのですが、HTTPヘッダのTransfer-Encoding: Chunkedで送られるデータ形式なんですね。

consumed

調べてみたけど分からなかったので放棄!

mcrypt.*, mdecrypt.*

自分のフィルタ一覧にはなく、暗号化フィルタのページで紹介されていたのですが、ここで使われているmcryptがそもそもdeprecatedになっていたり、使えるとしてURL表記では鍵などを渡せそうになかったことから割愛します。

その他のスキーム

file://

基本的にfopenfile_get_contentsなど、ファイルに対する関数の引数に相対パスや絶対パスを与えるとそのファイルを開きに行きます。
あえて明示的にURLで示すとするなら、file://を使えってことでしょう。
もちろんこの後ろに相対パスを書いても絶対パスを書いても動作します。
おまけですがブラウザのアドレスバーにもfile://を記述できますよね。

http:// ftp://

この辺りは言わずもがなですね。httpやftpを使ってアクセスしに行きます。
デフォルトではhttpはGETリクエスト専用、ftpでは読み込みと新規ファイルの書き込みが行えます。それ以外のアクセスをするにはfopen関数にコンテキストオプションを設定する必要があります。

echo file_get_contents("http://example.com");

data://

RFC2397に準拠した記述ができます。
改めて考えてみたら、MIMEとbase64した文字列さえ渡せばどんなファイルでも渡せるのか、すごいな。

$image = file_get_contents("data://image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYIIK");
file_put_contents("1x1.png", $image);

圧縮ストリーム(zlib://)

compress.zlib://compress.bzip2://php://filterよりもダイレクトにgzファイルやbzip2ファイルを展開できます。
zip://は続けてzipファイルの名前、さらに#をつけてzipファイルの中の開きたいファイルパスを記述します。
以下の例の最後の行ではarchive.zipを開き、その中のdir/file.txtの中身を取得することができます。

compress.zlib://file.gz
compress.bzip2://file.bzip2
zip://archive.zip#dir/file.txt

複数のストリームを組み合わせることも可能です。例えば、php://とzip://を組み合わせてこんなことができます。

php > echo file_get_contents("php://filter/string.toupper/resource=zip://file.zip#content.txt");
CONTENT

phar://

phar内のファイルに直接アクセスするストリームです。
archive.phar内のfile.phpにアクセスするにはphar://archive.phar/file.phpと記述します。
(ざっと調べた限りだと、pharはディレクトリ構成を取っていないんですね。)

ちなみにディレクトリのないzipやtarもなぜかphar://スキームで読み込めます。一体何故なのか。

書き込みをするためにはphp.iniでphar.readonlyを0に設定する必要があります。

ssh2://, rar://, ogg://

またの機会(必ず訪れるとは言っていない)にまとめます…

expect://

expectってのはPECLで入れる必要があるのか、

$ pecl install except
pecl/expect requires PHP (version >= 4.0.0, version <= 5.99.99), installed version is 7.2.12

解散!

検証環境

$ php -v
PHP 7.2.12 (cli) (built: Nov 29 2018 02:53:15) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.12, Copyright (c) 1999-2018, by Zend Technologies
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1618

ArchLinuxじゃなくてMacを使ってるじゃないかって?前のPCが物理的に壊れたんだよ!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です