この記事は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
ファイル識別子番号n
をphp://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種類の変換復元しかないのかい!こちとらソースまで調べに行ったわ!
というわけでbase64とquoted-printableの2種類です。
フィルタ名にconvert.base64-encode
と書けばbase64エンコード、convert.base64-decode
と書けばbase64デコードできます。convert.quoted-printable-encode
、convert.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.*
と書かれていたので色々あるのかと思って調べたらdeflate
とinflate
しかありませんでした。zlib.deflate
は圧縮、zlib.inflate
は展開ですね。
bzip2.*
bzip2はbzip2.compress
とbzip2.decompress
の2種類です。
それぞれ圧縮・展開です。
dechunk
PHPのhttp_chunked_decode()
に相当する変換をします。chunkedってなんだよって思ってしまったのですが、HTTPヘッダのTransfer-Encoding: Chunked
で送られるデータ形式なんですね。
consumed
調べてみたけど分からなかったので放棄!
mcrypt.*, mdecrypt.*
自分のフィルタ一覧にはなく、暗号化フィルタのページで紹介されていたのですが、ここで使われているmcryptがそもそもdeprecatedになっていたり、使えるとしてURL表記では鍵などを渡せそうになかったことから割愛します。
その他のスキーム
file://
基本的にfopen
やfile_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が物理的に壊れたんだよ!
コメント
[…] 。詳しくは私のブログを参照してください(宣伝)。https://www.ryotosaito.com/blog/?p=112 […]
[…] 局ここの文字列置換で持ち出せないように対策されている。これをかいくぐるためにbase64のストルームラッパーを用いる。使い方は以前このブログで公開したので是非参考にしてほしい。 […]