type
Post
status
Published
date
Apr 4, 2026
slug
summary
tags
CTF
category
category (1)
icon
password
comment

php://filter基础知识

php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。
php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范
notion image
使用如下代码本地测试
读取文件
notion image
写入文件
notion image
过滤器类别有字符串过滤器、转换过滤器、压缩过滤器,以下进行了总结
类别
过滤器名称
对应函数 / 逻辑
功能说明与核心参数
字符串
string.rot13
str_rot13()
执行 ROT13 编码(常用于简单的文本混淆)。
string.toupper
strtoupper()
将所有流经的数据实时转换为 大写
string.tolower
strtolower()
将所有流经的数据实时转换为 小写
string.strip_tags
strip_tags()
剥离 HTML/PHP 标签(PHP 7.3+ 弃用,不建议使用)。
编解码
convert.base64-encode
base64_encode()
转为 Base64。支持 line-lengthline-break-chars 参数。
convert.base64-decode
base64_decode()
将 Base64 编码还原为原始数据。
convert.quoted-printable-encode
quoted_printable_encode()
转为 Quoted-Printable 编码(常见于邮件传输)。
convert.quoted-printable-decode
quoted_printable_decode()
将 Quoted-Printable 还原。支持 line-break-chars 参数。
字符集
convert.iconv.*.*
iconv()
实时字符集转换。语法:convert.iconv.<from>.<to>(需 iconv 扩展)。
压缩
zlib.deflate
gzdeflate()
实时进行 DEFLATE 压缩(需 zlib 扩展)。
zlib.inflate
gzinflate()
实时解压 DEFLATE 压缩流。
bzip2.compress
bzcompress()
实时进行 Bzip2 压缩(需 bz2 扩展)。
bzip2.decompress
bzdecompress()
实时解压 Bzip2 压缩流。

利用php://filter进行文件包含

漏洞代码
被包含文件test.php内容
notion image
直接进行包含看不到源代码只能看到被包含文件的执行结果,所以我们需要借助php://filter的编码的方式读取出文件的内容
notion image
这样就能得到源码进行base64编码后的结果,读取出源代码的信息

利用php://filter绕过file_put_contents中的exit

<?php exit; ?> 这个代码表示退出当前的代码执行,不会执行之后的代码在这样的情况下无论写入的shell是否成功都不会执行传入的恶意代码,因为在恶意代码执行之前程序就已经结束退出了,导致shell后门利用失败。
实际漏洞挖掘当中主要会遇到以下两种限制:
  • 写入shell的文件名和内容不一样(前后变量不同)
  • 写入shell的文件名和内容一样(前后变量相同)
针对以上不同的限制手法所利用的姿势与技巧也不太一样,不过原理都是一样的,都需要利用相应的编码解码操作绕过头部限制写入能解析的恶意代码。下面主要针对脚本里面的exit限制手段进行探索与绕过。

Bypass 不同变量

这种情况主要是针对写入shell的文件名和内容的变量不一样的时候进行探索绕过,其中的文件名主要是传入我们的php伪协议,而文件内容则传入我们精心构造的恶意数据,最终也就是通过php伪协议和内容数据进行相应的编码解码绕过自身的头部限制,使传入的恶意代码能够正常解析也就达到了目的。
关于这种情景下的利用分析主要参考最早P牛16年发布的一篇文章:谈一谈php://filter的妙用,里面介绍了当写入shell并且内容可控的时候绕过exit头部限制,具体分析介绍如下:
恶意代码
分析代码可以看到,$content在开头增加了exit,导致文件运行直接退出!!
在这种情况下该怎么绕过这个限制呢,思路其实也很简单我们只要将content前面的那部分内容使用某种手段(编码等)进行处理,导致php不能识别该部分就可以了。

用编码转化过滤器进行绕过

在上面的介绍中我们知道php://filterconvert.base64-encodeconvert.base64-decode使用这两个过滤器等同于分别用 base64_encode()和 base64_decode()函数处理所有的流数据。
在代码中可以看到$_POST['filename']是可以控制协议的,既然可以控制协议,那么我们就可以使用php://filter协议的转换过滤器进行base64编码与解码来绕过限制。所以我们可以将$content内容进行解码,利用php base64_decode函数特性去除“exit”。
首先我们还需要清楚的了解到base64函数解码的原理和php中的base64_decode函数一些特性
解码原理:将base64编码数据根据编码表分别索引到编码值,然后每4个编码值一组组成一个24位的数据流,解码为3个字符。对于末尾位“=”的base64数据,最终取得的4字节数据,需要去掉“=”再进行转换。
php中base64_decode函数特性:base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
知道php base64解码特点之后,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符< ? ; > 空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有”phpexit”和我们传入的其他字符。
由于,”phpexit”一共7个字符,但是base64算法解码时是4个byte一组,所以我们可以随便再给他添加一个字符(Q)就可以,这样”phpexitQ”被正常解码,而后面我们传入的webshell的base64内容也被正常解码,这样就会将<?php exit; ?>这部分内容给解码掉,从而不会影响我们写入的webshell。
最后的payload为
其中PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4= 解码后就是shell代码<?php eval($_POST['cmd']); ?> 开头的Q是为了和phpexit进行组合而被base64_decode函数解码成不可见字符从而绕过
最后post提交payload为
notion image
notion image
notion image
可以看到shell.php已被正常写入并能执行rce

字符串过滤器绕过

在一些低版本的php中还存在string.strip_tags 这个过滤器,这个过滤器可以过滤掉<?php ?>之间的所有内容,所以当然也能过滤掉<?php exit; ?>这部分内容。但是如果单纯的中使用这一个过滤器去写文件内容的话,自己写的木马也会被过滤掉,这个时候需要使用到多个过滤器来配合,具体的payload如下
先去除原有的带php标签的exit代码,然后通过解码的方式写入木马文件
notion image
因为这个需要php版本<7.3所以这个方法不具有普遍性,用得也比较少
在字符串过滤器中我们还可以利用string.rot13 这个过滤器来进行过滤
str_rot13() 函数对字符串执行 ROT13 编码。
ROT13 编码是把每一个字母在字母表中向前移动 13 个字母得到。数字和非字母字符保持不变。
编码和解码都是由相同的函数完成的。如果您把一个已编码的字符串作为参数,那么将返回原始字符串。
分析利用php://filter中string.rot13过滤器去除”exit”。string.rot13的特性是编码和解码都是自身完成,利用这一特性可以去除exit<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,不过这种利用手法的前提是PHP不开启short_open_tag
notion image
虽然官方说的默认开启,但是在php.ini中默认是注释掉的,也就是说它还是默认关闭。
相应的payload如下
notion image
从服务上可以看到已经生成shell.php,同时<?php exit; ?>这部分已经被string.rot13编码所处理掉了。

Bypass 相同变量

这种情况主要是针对写入shell的文件名和内容的变量一样的时候进行探索绕过,这个时候文件名和内容就是同一个变量,而不像第一种方式那样可以比较容易的绕过,这种方式需要考虑文件名变量内容和文件内容数据的兼容性。
关于这种情景下的利用分析主要参考最近安全圈的一些研究:
  • 4月份,安恒发布的一篇渗透测试文章一次“SSRF—>RCE”的艰难利用里面提到了使用convert.iconv.UCS-xxx.UCS-xxx转换器进行绕过【重点是字符串的单元组反转->导致本身正常的代码因编码转换而失效】。
  • 4月份,P牛知识星球他所提到的一种手法使用string.trip_tags|convert.base64-decode进行绕过【重点是构造头部标签的闭合->恶意代码的编码->标签的剔除->恶意代码的解码】。
  • 3月份,先知的一篇文章关于 ThinkPHP5.0 反序列化链的扩展中使用string.trip_tags|convert.base64进行绕过<?php exit(); ?>【重点是构造标签的闭合(闭合特殊字符)->恶意代码的编码->标签的剔除->恶意代码的解码】。
  • 4月份,cyc1e师傅blog的一篇文章关于file_put_contents的一些小测试里面也有很多技巧【重点里面写入正常文件名技巧这个思路很好:过滤器里面写入payload、../跨目录写入文件。其它也就是一些过滤器的组合使用进行绕过,思路和上面已有的文章一样】
具体分析介绍如下:
恶意代码
分析代码可以看到,这种情况下写入的文件,其文件名和文件部分内容一致,这就导致利用的难度大大增加了,不过最终目的还是相同的:都是为了去除文件头部内容exit这个关键代码写入shell后门。
convert.base64
在上面不同变量利用base64构造payload的基础上,可以针对相同变量再次构造相应payload,在文件名中包含,满足正常解码就可以。
构造payload
正常情况下都会想到使用上述payload进行利用,但是这样构造发现是不可以的,因为构造的payload里面包含'='符号,熟悉base64编码的应该知道'='号在base64编码中起填充作用,也就意味着后面的结束,正是因为这样,当base64解码的时候如果字符'='后面包含有其他字符则会报错。具体见上文提到的先知的两篇文章里面都有提到使用base64编码所遇到的问题,里面也有提到即使关键字read、write可以去除,但是resource关键字不能少,也就导致不能直接使用这种方式去绕过。
string.strip_tags
这里关于string.strip_tags分两种环境去测试分析:这里限制条件同样在相同变量下
第一种
由于上面Bypass-不同变量这种情况下的限制代码直接就是<?php exit; ?>可以直接利用strip_tags去掉,但是现在这种情况下的限制代码和上面的有点不一样了,少了一段字符?>,其限制代码为<?php exit;,不过构造的目的是相同的最终还是要把exit;给去除掉。
分析两者限制代码的不同,那么我们可以直接再给它加一个?>字符串进行闭合就可以利用了
构造payload
代码合并
分析合并后的代码文件内容,发现成功的构造php标签<?php xxxx ?>,同时也可以发现代码中的字符等号’=’也包含在php标签里面,那么在经过strip_tags处理的时候都会去除掉,之后就不会影响base64的正常解码了。
载荷效果
notion image
可以看到payload请求成功,在服务器上生成了相应的文件,同时也正常的写入了webshell
虽然这样利用成功了,但是会发现这样的文件访问会有问题的,采用@Cyc1e师傅里面介绍的方法,利用../重命名即可解决。
利用技巧
?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+作为目录名(不管存不存在),再用../回退一下,这样创建出来的文件名为Qftm.php,这样创建出来的文件名就正常了。
notion image
有一个缺点就是这种利用手法在windows下利用不成功,因为文件名里面的? >等这些是特殊字符会导致文件的创建失败,关于这个问题上面先知的第一篇文章中也有提到这个问题,以及对于这种问题的解决方法:使用convert.iconv.utf-8.utf-7|convert.base64-decode进行绕过;同时也可以借鉴@Cyc1e师傅文章里面介绍的方法,利用非php://filter过滤器写入payload进行绕过,生成正常文件名;利用这两种方法既可以在Linux下写入文件也可以在Windows下写入文件,具体见下面相关过滤器利用。
第二种
对于这种情况主要是限制代码本身是闭合的同时有关变量相同,对于这种问题可以借鉴上面先知的第二篇文章有关解决方法,使用string.trip_tags|convert.base64过滤进行绕过。
  • 构造payload
关于过滤器里面写入<|这个过滤器,虽然php://filter里面没有但是不会爆出致命错误具有一定的兼容性。由于base64解码受字符'='的限制,那么则可以将其闭合在标签里面进行剔除,然后再进行base64解码。
  • 利用
notion image
这种方法同样无法在Windows下写入文件只限于Linux下(特殊字符的存在导致)。
string.rot13
这种方法在Bypass-不同变量中的短标签过滤器可以直接拿到使用进行绕过,间接性构造相应的payload,这里的限制不需要闭合exit;也可以利用。
构造payload
载荷效果
notion image
可以看到payload利用成功,生成目标恶意代码文件,同时恶意代码文件访问执行成功
notion image
针对string.rot13这种Bypass手段,还有另一种方法可以生成正常文件,也就是上面提到的,利用非php://filter过滤器写入payload进行绕过,生成正常文件名,虽然该过滤器不存在但是php://filter处理的时候只会显示警告信息不影响后续代码流程的执行(关于这个方法原理也就类似上面string.strip_stags的第二种情况通过将<不存在的过滤器写入过滤器中闭合后续标签剔除特殊字符)。
构造payload
这种构造可以使得恶意代码不会存在文件名中,避免了一下文件名因包含特殊字符而出错,当然这种构造在windows下一样可以正常利用。
载荷效果
notion image
其他还有一些奇奇怪怪的编码,这边就不再赘述了,如果想要了解可以自己去搜索

利用filter过滤器的编码组合构造RCE

Base64编码中只包含64个可打印字符A-Za-z0-9/+=,而PHP在解码base64时,遇到不在其中的字符包括不可见字符、控制字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。例如:
因此利用 PHP Filter Base64 可以去掉一些特殊字符。参考p神文章:谈一谈php://filter的妙用
学习了这个就可以开始尝试构造一句话木马RCE了
首先我们都知道include "php://filter/convert.base64-decode/resource=./flag.php";这里包含的是 flag.php 的内容经过base64编码后的结果。除了这个filter,PHP Filter 当中还有一种 convert.iconv 的 Filter ,可以用来将数据从字符集 A 转换为字符集 B。可以通过命令 iconv -l 列出支持的字符编码,虽然列出的字符编码比较多,但一些实际上是其他字符集的别名。
这时候,奇妙的东西出现了,convert.iconv.UTF8.CSISO2022KR 将始终在字符串前面添加\x1b$)C\x1b是不可见字符可以看到这个 UTF8.CSISO2022KR 编码形式,并且通过这个编码形式产生的字符串里面, C 字符前面的字符对于 PHP Base64 来说是非法字符,所以接下来我们只需要 base64-decode 一下就可以去掉不可见字符了,但是与此同时,我们的 C 字符也被 base64-decode 解码了,这时候我们需要再把解码结果使用一次 base64-encode 即可还原回来原来的 C 字符了。事实真是如此吗,no!经过测试可以发现,当 C 后面没有 base64 有效字符时,并没有将 C 还原回来。
这时候该怎么办呢(思考脸),既然刚刚UTF8.CSISO2022KR可以捏造出base64有效字符,那先捏造出来一些垃圾数据不就可以了。为了得到满满的有效字符,可以直接再base64编码一手,那么代码就长这样
这样就还原出了字符C,这里使用convert.iconv.UTF8.UTF7的原因是 有时候会出现convert.base64-decode 过滤器失败的情况:如果它在意想不到的时候遇到等号,幸运的是可以再次使用 iconv 并从 UTF8 转换为 UTF7,这会将字符串中的任何等号转换为某个 base64有效字符
因此只要编码规则用得好,其实resource的文件内容是什么无关紧要,只要有文件,哪怕是个空文件,也能无中生有制造垃圾数据作为基础数据进行编码转换。
可利用的exp脚本文件如下
漏洞代码
执行脚本得到payload
notion image
成功执行payload
notion image

利用filter进行基于oracle的文件读取攻击

下面是受影响的PHP函数的部分列表。
getimagesize
getimagesize($_POST[0]);
md5_file
md5_file($_POST[0]);
sha1_file
sha1_file($_POST[0]);
hash_file
hash_file('md5', $_POST[0]);
file
file($_POST[0]);
parse_ini_file
parse_ini_file($_POST[0]);
copy
copy($_POST[0], '/tmp/test');
file_put_contents (only target read only with this)
file_put_contents($_POST[0], "");
stream_get_contents
$file = fopen($_POST[0], "r"); stream_get_contents($file);
fgets
$file = fopen($_POST[0], "r"); fgets($file);
fread
$file = fopen($_POST[0], "r"); fread($file, 10000);
fgetc
$file = fopen($_POST[0], "r"); fgetc($file);
fgetcsv
$file = fopen($_POST[0], "r"); fgetcsv($file, 1000, ",");
fpassthru
$file = fopen($_POST[0], "r"); fpassthru($file);
fputs
$file = fopen($_POST[0], "rw"); fputs($file, 0);
除此之外,其他模块中的其他功能也可能受到影响。例如,exif模块中的 exif_imagetype 函数也可以这样利用,核心是只要对文件内容执行了任何操作,那么该函数就可能受到 php://filter 包装器的影响。
漏洞代码
PHP函数file读取一个文件,但不输出其内容,这意味着Apache服务器的响应中不会显示任何内容。
在windows下进行利用docker还需要注意Dockerfile的编写,以下是我编写的Dockerfile确保能够正确的复现这个漏洞
利用溢出漏洞来读取文件内容,相关exp:https://github.com/synacktiv/php_filter_chains_oracle_exploit
脚本命令
notion image
可以看到成功利用并且读取了文件

LFI2RCE

php进行字符集转换的时候,它会去调用iconv()函数。这是一个使用转换描述符将输入缓冲区中的字符转换为输出缓冲区的api,反正就是缓冲区的东西。而在linux上,这个api采用glibc实现,iconv()函数如下:

如果输出缓冲区不够大,iconv()就会报错,这个时候可以重新分配outbuf并且再次调用iconv(),确保永远不会从输入缓冲区读取超过或者向输出缓冲区写入超过inbytesleft或者outbytesleft的数据。也就是通过分配,能够确保不会触发溢出问题
但是有一个字符集打破了这一永远。
没错,又是我们的中文字符。果然中华文字博大精深(x)
该字符集存于:glibc/iconvdata/iso-2022-cn-ext.c

看两个else if*outptr。它会将要转换的字符输入转换成四字节的输出,并且没有做任何的检验就输出。
这样有可能会产生出六种输出:

这里会出现什么问题呢,直接转换成四字节,就有可能会对我们的iconv所限制的空间产生溢出。
一个简单的poc:

其中hexdump就是打印出输出的字节,没有其他含义。
主要看我们的main函数。main这里的input是一个AAAAA劄,其中这个我们利用python来打印一下他的字节:

notion image

原本是一个三字节的字符,但是经过该字符集转换后会转换成四字节的输出。
因此即使限制了soutputstrlen(input),也就是8的时候,也会溢出一字节。编译并运行该poc会得到如下的结果:
notion image
可以看到我们确实溢出了1bytes。
要想将其利用,我们还得需要php heap(php堆),简单地说就是读取/proc/self/maps,并且从中提取到PHP堆地址libc库文件名,接着下载libc二进制文件并得到system函数的地址并且打rce即可。
不必在意其深层原理,相应的exp在
vulhub下载一个docker-compose.yml部署,或者自己部署一个,把index.php改成这个:
notion image

可以读取/etc/passwd文件。
使用payload:

由于我的docker是在本机(windows)上开的,所以我打算在docker内部安装并且执行exp.py,此时又要将一些安装python3和pip的小知识了:

安装完之后就能用python3了
一键利用:
notion image
notion image
 
0xGame2024厦门三日游
Loading...