前一篇对SSRF的总结,实际上里面也是涉及到一些协议的,这里也在自己之前学习PHP伪协议时的笔记基础上进行扩充,对PHP伪协议进行一下总结。

PHP 支持的伪协议

php伪协议,事实上是其支持的协议与封装协议。而其支持的协议有:

php:// — 访问各个输入/输出流(I/O streams)
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

php:// 协议

使用条件:

不需要开启`allow_url_fopen`
仅`php://input、 php://stdin、 php://memory 和 php://temp `需要开启`allow_url_include`。

php://访问各个输入/输出流I/O streams),在CTF中经常使用的是php://filterphp://inputphp://filter用于读取源码,php://input用于执行php代码。

下面对其进行详细介绍:

php://filter

CTF中常用的伪协议,可以用来读取文件,是一种元封装器,设计用于数据流打开时的筛选过滤应用。其中一种最典型的利用方式如下:

index.php?file=php://filter/read=convert.base64-encode/resource=index.php

// 这里读的过滤器为convert.base64-encode,就和字面上的意思一样,把输入流base64-encode。
// resource=upload.php,代表读取upload.php的内容

上述代码意为使用base64编码的形式将index.php读取出来。对于上述利用方式具体每部分的含义,可以参照下图

在这里,涉及到一个过滤器的概念,PHP过滤器用于验证和过滤来自非安全来源的数据,比如用户的输入。在这里之所以可以使用过滤器读取文件,相当于是将文件作为过滤器输入,获取其经过处理之后的数据流,而在PHP中,过滤器有很多种,分别为:

字符串过滤器

string.rot13
// 进行rot13转换
// 自 PHP 4.3.0 起,使用此过滤器等同于用 str_rot13()函数处理所有的流数据。

string.toupper
// 将字符全部大写

string.tolower
// 将字符全部小写

string.strip_tags
// 去除空字符、HTML 和 PHP 标记后的结果。
// 功能类似于strip_tags()函数,若不想某些字符不被消除,后面跟上字符,可利用字符串或是数组两种方式。
  • 示例-string.rot13
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.rot13');
fwrite($fp, "This is a test.\n");
?>

/* Outputs: Guvf vf n grfg. */
// string.toupper(自 PHP 5.0.0 起)使用此过滤器等同于用 strtoupper()函数处理所有的流数据。
  • 示例-string.toupper
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.toupper');
fwrite($fp, "This is a test.\n");
?>

/* Outputs: THIS IS A TEST. */
// string.tolower(自 PHP 5.0.0 起)使用此过滤器等同于用 strtolower()函数处理所有的流数据。
  • 示例-string.tolower
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.tolower');
fwrite($fp, "This is a test.\n");
?>

/* Outputs: this is a test. */

转换过滤器

如同string.*过滤器,convert.*过滤器的作用就和其名字一样。转换过滤器是PHP 5.0.0 添加的。

以常用的convert.base64-encodeconvert.base64-decode为例,使用这两个过滤器等同于分别用base64_encode()base64_decode()函数处理所有的流数据。

convert.base64-encode支持以一个关联数组给出的参数。如果给出了line-lengthbase64输出将被用line-length个字符为 长度而截成块。如果给出了line-break-chars,每块将被用给出的字符隔开。这些参数的效果和用base64_encode()再加上chunk_split()相同。

  • 示例-convert.base64-encode & convert.base64-decode
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode');
fwrite($fp, "This is a test.\n");
fclose($fp);

/* Outputs: VGhpcyBpcyBhIHRlc3QuCg== */


$param = array('line-length' => 8, 'line-break-chars' => "\r\n");
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode', STREAM_FILTER_WRITE, $param);
fwrite($fp, "This is a test.\n");
fclose($fp);
/* Outputs: VGhpcyBp
: cyBhIHRl
: c3QuCg== */


$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-decode');
fwrite($fp, "VGhpcyBpcyBhIHRlc3QuCg==");
fclose($fp);

/* Outputs: This is a test. */
?>

除此之外,还有convert.quoted-printable-encodeconvert.quoted-printable-decode两个过滤器。

使用此过滤器的decode等同于用quoted_printable_decode()函数处理所有的流数据。没有和convert.quoted-printable-encode对应的函数。

convert.quoted-printable-encode支持以一个关联数组给出的参数。除了支持和convert.base64-encode一样的附加参数外,convert.quoted-printable-encode还支持布尔参数binaryforce-encode-firstconvert.base64-decode只支持line-break-chars参数作为从编码载荷中剥离的类型提示。

  • 示例-convert.quoted-printable-encode & convert.quoted-printable-decode
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-encode');
fwrite($fp, "This is a test.\n");

/* Outputs: =This is a test.=0A */
?>

压缩过滤器

正如名字所提到的,其作用也是类似,虽然在PHP伪协议中,有压缩封装协议(zlib://, bzip2://, zip://),提供了在本地文件系统中 创建 gzip 和 bz2 兼容文件的方法,但不代表可以在网络的流中提供通用压缩的意思,也不代表可以将一个非压缩的流转换成一个压缩流。对此,压缩过滤器可以在任何时候应用于任何流资源。

另外,需要注意的是,压缩过滤器不产生命令行工具如gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。其中主要有zlib.* , bzip2.* 两类压缩过滤器:

zlib.deflate(压缩)和zlib.inflate(解压)实现了RFC 1951中的的压缩算法。 zlib.* 压缩过滤器自 PHP 版本 5.1.0起可用,在激活 zlib的前提下。也可以通过安装来自PECLzlib_filter包作为一个后门在5.0.x版中使用。此过滤器在PHP 4中不可用。

其中,deflate过滤器最多可以接受三个参数。分别为:

  • level 定义了压缩强度(1-9)。数字更高通常会产生更小的载荷,但要消耗更多的处理时间。存在两个特殊压缩等级:0(完全不压缩)和 -1(zlib 内部默认值,目前是 6)。

  • window 压缩回溯窗口大小,以二的次方表示。更高的值(大到 15 —— 32768 字节)产生更好的压缩效果但消耗更多内存,低的值(低到 9 —— 512 字节)产生产生较差的压缩效果但内存消耗低。目前默认的 window大小是 15。

  • memory 用来指示要分配多少工作内存。合法的数值范围是从 1(最小分配)到 9(最大分配)。内存分配仅影响速度,不会影响生成的载荷的大小。

下面为该压缩过滤器的示例:

  • 示例-zlib.deflate和 zlib.inflate
<?php
$params = array('level' => 6, 'window' => 15, 'memory' => 9);

$original_text = "This is a test.\nThis is only a test.\nThis is not an important string.\n";
echo "The original text is " . strlen($original_text) . " characters long.\n";

$fp = fopen('test.deflated', 'w');
stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
fwrite($fp, $original_text);
fclose($fp);

echo "The compressed file is " . filesize('test.deflated') . " bytes long.\n";
echo "The original text was:\n";

/* Use readfile and zlib.inflate to decompress on the fly */
readfile('php://filter/zlib.inflate/resource=test.deflated');

/* Generates output:

The original text is 70 characters long.
The compressed file is 56 bytes long.
The original text was:
This is a test.
This is only a test.
This is not an important string.

*/
?>
  • 示例-zlib.deflate简单参数用法
<?php
$original_text = "This is a test.\nThis is only a test.\nThis is not an important string.\n";
echo "The original text is " . strlen($original_text) . " characters long.\n";

$fp = fopen('test.deflated', 'w');
/* Here "6" indicates compression level 6 */
stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE, 6);
fwrite($fp, $original_text);
fclose($fp);

echo "The compressed file is " . filesize('test.deflated') . " bytes long.\n";

/* Generates output:

The original text is 70 characters long.
The compressed file is 56 bytes long.

*/
?>

bzip2.compressbzip2.decompress工作的方式与上面讲的zlib.*过滤器相同。 自PHP 5.1.0起可用,在激活 bz2支持的前提下。也可以通过安装来自PECLbz2_filter包作为一个后门在5.0.x版中使用。此过滤器在PHP 4中 不可用。

bzip2.compress过滤器接受最多两个参数:

  • blocks 从 1 到 9 的整数值,指定分配多少个 100K 字节的内存块作为工作区。
  • work 0 到 250 的整数值,指定在退回到一个慢一些,但更可靠的算法之前做多少次常规压缩算法的尝试。调整此参数仅影响到速度,压缩输出和内存使用都不受此设置的影响。将此参数设为 0 指示 bzip 库使用内部默认算法。

bzip2.decompress过滤器仅接受一个参数,可以用普通的布尔值传递,或者用一个关联数组中的small单元传递。当small设为&true; 值时,指示bzip库用最小的内存占用来执行解压缩,代价是速度会慢一些。

下面为其使用示例:

  • 示例-bzip2.compress和 bzip2.decompress
<?php
$param = array('blocks' => 9, 'work' => 0);

echo "The original file is " . filesize('LICENSE') . " bytes long.\n";

$fp = fopen('LICENSE.compressed', 'w');
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE, $param);
fwrite($fp, file_get_contents('LICENSE'));
fclose($fp);

echo "The compressed file is " . filesize('LICENSE.compressed') . " bytes long.\n";

/* Generates output:

The original text is 3288 characters long.
The compressed file is 1488 bytes long.

*/
?>

加密过滤器

加密过滤器为mcrypt.*mdecrypt.*,使用libmcrypt提供了对称的加密和解密。这两组过滤器都支持mcrypt扩展库中相同的算法,格式为mcrypt.ciphername,其中ciphername是密码的名字,将被传递给mcrypt_module_open()。有以下五个过滤器参数可用:

  • 示例-用 3DES 将文件加密输出
<?php
$passphrase = 'My secret';

/* Turn a human readable passphrase
* into a reproducable iv/key pair
*/
$iv = substr(md5('iv'.$passphrase, true), 0, 8);
$key = substr(md5('pass1'.$passphrase, true) .
md5('pass2'.$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);

$fp = fopen('secert-file.enc', 'wb');
stream_filter_append($fp, 'mcrypt.tripledes', STREAM_FILTER_WRITE, $opts);
fwrite($fp, 'Secret secret secret data');
fclose($fp);
?>
  • 示例-读取加密的文件
<?php
$passphrase = 'My secret';

/* Turn a human readable passphrase
* into a reproducable iv/key pair
*/
$iv = substr(md5('iv'.$passphrase, true), 0, 8);
$key = substr(md5('pass1'.$passphrase, true) .
md5('pass2'.$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);

$fp = fopen('secert-file.enc', 'rb');
stream_filter_append($fp, 'mdecrypt.tripledes', STREAM_FILTER_WRITE, $opts);
$data = rtrim(stream_get_contents($fp));
fclose($fp);

echo $data;
?>

php://input

php://input是个可以访问请求的原始数据的只读流,将post请求中的数据作为PHP代码执行。当传进去的参数作为文件名变量去打开文件时,可以将参数php://input,同时post方式传进去值作为文件内容,供php代码执行时当做文件内容读取

利用条件:

allow_url_include = On
allow_url_fopen = On/Off

利用姿势:

index.php?file=php://input

POST:
<?php phpinfo();?>/<? phpinfo();?>

需要注意的是,在PHP 5.6之前php://input打开的数据流只能读取一次; 数据流不支持seek操作。 不过,依赖于SAPI的实现,请求体数据被保存的时候, 它可以打开另一个php://input数据流并重新读取。 通常情况下,这种情况只是针对 POST 请求,而不是其他请求方式,比如PUT或者PROPFIND

php://伪协议中,除了上述两种在CTF中常用的之外,还有一些其他的,比如下面这些:

  • php://output 一个只写的数据流, 允许以printecho一样的方式 写入到输出缓冲区。
  • php://fd 允许直接访问指定的文件描述符。 例如php://fd/3引用了文件描述符3
  • php://memory/php://temp 一个类似文件包装器的数据流,允许读写临时数据。
  • php://stdin/php://stdout/php://stderr 允许直接访问 PHP 进程相应的输入或者输出流

http(s):// 协议

用以访问HTTP(s)网址,允许通过HTTP 1.0GET方法,以只读访问文件或资源。 HTTP请求会附带一个Host: 头,用于兼容基于域名的虚拟主机。 如果在php.ini文件中或字节流上下文(context)配置了user_agent字符串,它也会被包含在请求之中。使用需要满足以下条件:

allow_url_fopen:on
allow_url_include :on

用法:

http://example.com
http://example.com/file.php?var1=val1&var2=val2
http://user:password@example.com
https://example.com
https://example.com/file.php?var1=val1&var2=val2
https://user:password@example.com
  • 示例-检测重定向后最终的 URL
<?php
$url = 'http://www.example.com/redirecting_page.php';

$fp = fopen($url, 'r');

$meta_data = stream_get_meta_data($fp);
foreach ($meta_data['wrapper_data'] as $response) {

/* 我们是否被重定向了? */
if (strtolower(substr($response, 0, 10)) == 'location: ') {

/* 更新我们被重定向后的 $url */
$url = substr($response, 10);
}
}
?>

ftp(s):// 协议

用以访问FTP(s) URLs,允许通过FTP读取存在的文件,以及创建新文件。 如果服务器不支持被动(passive)模式的FTP,连接会失败。

打开文件后你既可以读也可以写,但是不能同时进行。 当远程文件已经存在于ftp服务器上,如果尝试打开并写入文件的时候, 未指定上下文(context)选项overwrite,连接会失败。 如果要通过FTP覆盖存在的文件, 指定上下文(context)overwrite选项来打开、写入。 另外可使用FTP扩展来代替。

需要注意的是:如果设置了php.ini中的from指令,这个值会作为匿名(anonymous)ftp的密码。

zlib:// & bzip2:// & zip:/ 协议

php伪协议中的压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀(jpg png gif xxx)等等。其使用条件为:

allow_url_fopen:off/on
allow_url_include :off/on
  • 示例-使用zip://压缩phpinfo.txtphpinfo.zip ,压缩包重命名为phpinfo.jpg ,并上传
http://127.0.0.1/include.php?file=zip://home/test/WWW/phpinfo.jpg%23phpinfo.txt
  • 示例-使用compress.bzip2://file.bz2压缩phpinfo.txtphpinfo.bz2并上传(同样支持任意后缀名)
http://127.0.0.1/include.php?file=compress.bzip2://home/test/WWW/phpinfo.bz2
  • 示例-使用compress.zlib://file.gz压缩phpinfo.txtphpinfo.gz并上传(同样支持任意后缀名)
http://127.0.0.1/include.php?file=compress.zlib://home/test/WWW/phpinfo.gz

data:// 协议

PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。使用条件为:

allow_url_fopen:on
allow_url_include :on

可以支持明文或编码,用法为:

data://text/plain,XXX
data://text/plain;base64,XXX

格式为:
data://资源类型;编码,内容
  • 示例-使用明文方式读取phpinfo()
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>

  • 示例-使用base64方式(CTF中可用以绕过waf)读取phpinfo()
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b
  • 示例-打印data://的内容
<?php
// 打印 "I love PHP"
echo file_get_contents('data://text/plain;base64,SSBsb3ZlIFBIUAo=');
?>

glob:// 协议

查找匹配的文件路径模式, 自PHP 5.3.0起开始有效。下面为其基本用法:

<?php
// 循环 ext/spl/examples/ 目录里所有 *.php 文件
// 并打印文件名和文件尺寸
$it = new DirectoryIterator("glob://ext/spl/examples/*.php");
foreach($it as $f) {
printf("%s: %.1FK\n", $f->getFilename(), $f->getSize()/1024);
}
?>

output:

tree.php: 1.0K
findregex.php: 0.6K
findfile.php: 0.7K
dba_dump.php: 0.9K
nocvsdir.php: 1.1K
phar_from_dir.php: 1.0K
ini_groups.php: 0.9K
directorytree.php: 0.9K
dba_array.php: 1.1K
class_tree.php: 1.8K

phar:// 协议

用以PHP归档,数据流包装器自PHP 5.3.0起开始有效,与zip://类似,同样可以访问zip格式压缩包内容,比如下面这个例子:

http://127.0.0.1/include.php?file=phar://home/test/WWW/phpinfo.zip/phpinfo.txt

该伪协议在CTF中比较常见,主要用于反序列化和文件包含,此文中只对其用于文件包含进行介绍,反序列化在后面将会单独拿出来讲。在文件包含中,该协议主要用于支持zip、phar格式的文件包含,用法如下:

?file=phar://[压缩包文件相对路径]/[压缩文件内的子文件名]
?file=phar://[压缩包文件绝对路径]/[压缩文件内的子文件名]
  • 示例-配合文件上传漏洞,当仅可以上传zip格式时
index.php?file=phar://index.zip/index.txt

index.php?file=phar://home/test/WWW/FI/index.zip/index.txt
  • 示例-配合文件上传漏洞,当仅可以上传图片格式时,phar://不管后缀是什么,都会当做压缩包来解压。
index.php?file=phar://head.png/head.txt

index.php?file=phar://home/test/WWW/FI/head.png/head.txt

file:// 协议

用于访问本地文件系统,可以使用相对路径或绝对路径来访问文件系统文件,其使用样例为:

/path/to/file.ext
relative/path/to/file.ext
fileInCwd.ext
C:/path/to/winfile.ext
C:\path\to\winfile.ext
\\smbserver\share\path\to\winfile.ext
file:///path/to/file.ext

该伪协议在CTF中通常用来读取本地文件,因为其在双off的情况下也可以正常使用,不受allow_url_fopenallow_url_include的影响。

  • 示例-使用文件的相对路径和文件名
http://127.0.0.1/include.php?file=./phpinfo.txt
  • 示例-使用文件的绝对路径和文件名
http://127.0.0.1/include.php?file=file://home/test/WWW/phpinfo.txt

ssh2:// 协议

Secure Shell 2,默认没有激活,如果需要使用ssh2.*://封装协议,必须安装来自PECLSSH2扩展,主要形式有:

ssh2.shell://
ssh2.exec://
ssh2.tunnel://
ssh2.sftp://
ssh2.scp://

该伪协议除了支持传统的URI登录信息,ssh2封装协议也支持通过URL的主机(host)部分来复用打开连接,用法如下所示:

ssh2.shell://user:pass@example.com:22/xterm
ssh2.exec://user:pass@example.com:22/usr/local/bin/somecmd
ssh2.tunnel://user:pass@example.com:22/192.168.0.1:14
ssh2.sftp://user:pass@example.com:22/path/to/filename

下面为一个示例,用以从一个活动连接中打开字节流:

<?php
$session = ssh2_connect('example.com', 22);
ssh2_auth_pubkey_file($session, 'username', '/home/username/.ssh/id_rsa.pub',
'/home/username/.ssh/id_rsa', 'secret');
$stream = fopen("ssh2.tunnel://$session/remote.example.com:1234", 'r');
?>

ogg:// 协议

音频流协议,用以读取OGG/Vorbis格式的压缩音频编码,并能通过该伪协议写入或追加压缩音频数据,默认未激活,使用需要安装PECL中的OGG/Vorbis扩展。用法如下:

ogg://soundfile.ogg
ogg:///path/to/soundfile.ogg
ogg://http://www.example.com/path/to/soundstream.ogg

expect:// 协议

用以处理交互式的流,由expect://封装协议打开的数据流PTY提供了对进程stdiostdoutstderr的访问,默认未开启,使用须安装PECL上的Expect扩展。用法如下:

expect://command

参考资料

PHP 支持和封装的协议
php:// 官方文档
PHP 可用过滤器列表

扩展阅读

php伪协议实现命令执行的七种姿势
PHP文件包含漏洞利用思路与Bypass总结
利用 phar 拓展 php 反序列化漏洞攻击面