news 2026/10/20 14:21:21

php://filter文件包含漏洞实战:从CTF入门到编码绕过全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
php://filter文件包含漏洞实战:从CTF入门到编码绕过全解析

从一道CTF题出发,手把手教你玩转php://filter文件包含漏洞(附多种编码绕过姿势)

第一次遇到php://filter文件包含漏洞,是在某CTF训练平台的一道web题里。当时我拿着一个看似平平无奇的“?file=xxx”参数,试了一堆路径穿越全无反应,最后在大佬的Hint提醒下才明白,这才是PHP文件包含里最经典的利用入口。这篇文章就把我自己从小白到入门的过程完整记录下来,把php://filter的原理、实战玩法、绕过姿势和踩坑经验一次讲清楚。不管你是CTF新手还是搞渗透测试的老手,只要你对文件包含漏洞想深入一点,这篇内容应该能给你省不少试错时间。

1. 文件包含漏洞到底是怎么一回事

1.1 从include函数说起

文件包含是PHP开发里非常常见的功能。你可以把多个页面共用的头部、尾部、配置信息等拆成独立文件,再用include或require把它们加载进来。这种写法看起来完全没问题,但当一个Web应用把用户输入的内容直接拼进include函数的参数里,就出大问题了。

举个例子,很多老系统里会出现这样的代码:

<?php $file = $_GET['file']; include($file . '.php'); ?>

用户传file=index,系统就拼接成index.php并包含进来。看起来没什么问题,但假如用户传的是file=../../../../etc/passwd呢?系统会尝试去包含服务器上的任意文件路径。如果能包含成功,服务器上的敏感文件就暴露了。这就是最基本的文件包含漏洞。

你可能会问:PHP里不是有allow_url_include限制吗?确实,allow_url_include默认是Off,这挡住了你通过http://或https://包含远程文件的路子。但注意,它挡不住php://、data://、phar://这类PHP内置协议。尤其php://filter,本地文件读取能力基本不受影响。

1.2 文件包含漏洞的危害等级

文件包含分为本地文件包含(LFI)和远程文件包含(RFI)。RFI直接把远程恶意脚本拉进服务器执行,危害极大,但现在默认配置下很难利用。LFI则是当前的主流攻击面,它的主要危害有三类:

  • 读取敏感源码文件,比如数据库配置文件、安装脚本、业务逻辑核心文件
  • 配合日志注入、临时文件投递等手段实现本地写马
  • 利用php://filter等协议读取目标文件内容并绕过代码执行限制

它最骚的地方就在于:include执行PHP代码,但如果你用php://filter读取一个.php文件,PHP解析器不会执行文件里的代码,而是把过滤后的内容直接输出。这就能在不执行代码的情况下看到源码。

我打个比方。普通包含就像请了一个翻译官直接把原文念出来,翻译官本身又是全场老大,能指挥三军(执行PHP代码)。而php://filter等于是你先用滤网把内容筛了一遍,只让文本流出来,指挥权根本传不出去。这个“只读不执行”的特性,就是它成为源码泄露利器的根本原因。

1.3 哪里最容易出文件包含

文件包含漏洞最常见的出现位置:

  • 模板加载:?page=home、?template=header这类参数直接决定载入哪个模板文件。
  • 语言切换:?lang=cn、?language=zh,后端可能直接include("lang/{$lang}.php")。
  • 文件下载/预览:?file=readme.txt、?path=/var/www/html/uploads/。
  • 图片处理:通过参数控制读取哪张图片做缩略图。

这些场景都有一个共同点:用户能控制路径的一部分,而后端没有严格校验白名单。题目里最有代表性的就是直接在URL里给你一个file参数,剩下的全看你的想象力了。

2. php://filter 的核心机制

2.1 过滤器包装器的语法长什么样

php://filter是PHP内置的流包装器之一。它的作用是在读取或写入流数据时,套上指定的过滤器链。最常用的语法是:

php://filter/read=过滤器1|过滤器2/resource=目标文件

拆开看,它有三个部分:

  • read=:指定读路径时应用的过滤器。
  • write=:指定写路径时应用的过滤器。
  • resource=: 指定要被过滤的目标流,一般是文件路径。

多个过滤器之间用竖线|分隔。例如:

php://filter/read=convert.base64-encode/resource=flag.php

意思就是:把flag.php的原始内容用base64编码后再输出。为什么要编码?因为flag.php里面是PHP代码,如果直接包含,代码会被解析执行,你看到的只会是执行后的空白或者HTML片段,而不是源码。先base64编码,PHP解析器在输出时不会执行它,而是把编码后的字符串交给你,你再解码拿到完整源码。

如果你只想转成大写或者小写,也可以用string.toupper、string.tolower。不过实战中最常用的就那几个,下面我列全一点。

2.2 常用过滤器家族

过滤器类型名称作用典型场景
字符串过滤string.rot13对字符串做rot13加密绕过限制、混淆输出内容
字符串过滤string.toupper转为大写配合大小写敏感过滤绕过
字符串过滤string.tolower转为小写同上
字符串过滤string.strip_tags去除PHP/HTML标签尝试读取包含标签的文件时用
转换过滤convert.base64-encodebase64编码最常用的读源码姿势
转换过滤convert.base64-decodebase64解码写马时绕过编码限制
转换过滤convert.quoted-printable-encode引号打印编码部分环境可绕WAF
转换过滤convert.iconv.*任意字符集转换编码绕过、变形读取
压缩过滤zlib.deflate压缩绕过长度限制,缩小内容体积
压缩过滤zlib.inflate解压与deflate配合使用

其中convert.iconv.*非常灵活,因为PHP支持大量字符集。你可以把一个文件内容先转成UTF-32,再转成UTF-7,或者先转成UCS-4再转成ASCII,不同的转码过程会让输出的字节流千奇百怪。某些过滤规则见到正常base64特征就给拦截,但转几次码之后模式完全变样,就绕过去了。

2.3 为什么用php://filter而不是直接file_get_contents

有的人可能会问:同样是读文件,用file=../../etc/passwd不行吗?行啊,但你也看到了,直接路径包含有很多限制,最核心的就是:当目标文件是.php时,包含进去就会被PHP解析,你只能看到执行后的结果,根本看不到源代码。而php://filter能让你在不执行目标文件的情况下把它的内容原样取出来,这是文件包含漏洞利用里无可替代的一环。

再一个,include路径拼接经常给你加后缀。比如上面那段代码:

include($file . '.php');

你传file=../../etc/passwd,实际上拼出来的是../../etc/passwd.php,永远找不到目标文件。但如果你传:

php://filter/read=convert.base64-encode/resource=../../../../etc/passwd

拼上.php之后是:

php://filter/read=convert.base64-encode/resource=../../../../etc/passwd.php

在这种情况下,resource部分的路径会被当成../../../../etc/passwd.php处理,依然找不到。这种题目就需要你借助另一个技巧:路径截断或者通过双重编码、URL编码让后缀失效。经典解法之一是构造:

php://filter/read=convert.base64-encode/resource=../../../../etc/passwd%00

利用空字节截断(PHP 5.2以下有效),或者用超长路径截断让系统自动忽略最后的.php。不过这在现在的主流PHP版本里已经不灵了,实战中更常见的是代码里没有强制拼后缀,或者只拼一部分。

2.4 php://filter的write路径也有大用处

php://filter不仅能读,还能写。写入时指定write=过滤器,比如把一个写好的恶意PHP代码先做base64解码再写入目标文件。这种思路通常配合文件上传、日志注入等场景,实现从文件读取到Getshell的进阶。比如你拿到了一个日志文件的写权限,可以把一句话木马写进日志,然后用:

php://filter/write=convert.base64-decode/resource=shell.php

把base64解码后的内容直接写入shell.php,这一步操作在很多CTF题和实战里都是打通后续环节的关键。

3. 手把手复现一道CTF题

大概两个多月前,我在某CTF平台上看到一道入门的文件包含题,题目给了个页面,可以提交file参数,附件里有源码。虽然题目本身不难,但把所有利用姿势走一遍后,我对php://filter的理解提升了一个层次。下面就来完整复现一遍。

3.1 题目环境与信息收集

打开题目是一个简单的PHP页面,源码大概长这样:

<?php error_reporting(0); if (isset($_GET['file'])) { $file = $_GET['file']; if (preg_match("/php|filter/i", $file)) { die("Hacker!!!"); } include($file . '.php'); } else { highlight_file(__FILE__); } ?>

第一个关键信息点:include($file . '.php'),强制拼接.php后缀。

第二个关键信息点:它过滤了php和filter两个关键词,用正则/php|filter/i做匹配,一旦检测到就直接die("Hacker!!!")。

页面默认是无参访问时高亮显示当前源码。我尝试传:

?file=flag

页面直接报了个警告说文件不存在,说明flag文件确实不在当前目录,或者被放在了别的路径。这个时候就需要去读源码。但读取源码本身也要用include,而代码把用户输入拼成了.php后缀,所以常规包含绕不过去。

3.2 第一次尝试:直接套filter被拦

我最先想到的就是直接上php://filter,构造:

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

结果页面一下子打出“Hacker!!!”,很明显,正则把php和filter都拦住了。这条路子直接走不通。

然后我试了大小写混写:

?file=PHP://Filter/convert.base64-encode/resource=index

还是被拦。因为正则带了i修饰符,不区分大小写,所以大小写绕不过。

接下来试了URL编码:

?file=php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dindex

服务器没有对参数做URL解码,PHP把整个字符串原样交给include,最终包含的是一个名为php%3A%2F%2Ffilter%2F...的文件,根本不存在,自然也没有任何输出。所以这种方法在这个场景下也无效。

3.3 琢磨过滤规则,找出绕过路径

被连续几次打回之后,我冷静下来分析了一会儿。这里的正则匹配的是无空格无干扰的一次性模式匹配,而且它会检查构造出的完整参数。如果我能在构造的payload里把php这个关键词拆开,让它不是连续出现,但最终PHP解析时又能还原为php,就能绕过。

第一种思路:利用数据流或路径解析的特性,看能不能把php和filter以外的东西混进去。比如:

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

这个payload里的php://和filter这两个词都在正则的枪口上,那我能不能用resource参数去指向一个包含“php”内容但文件名里不带php的文件?不行,因为include($file . '.php')自动追加了.php,你指过去之后,实际去包含的还是xxx.php。正则匹配的是$file的值,所以包含后的文件名不算在内。

第二种思路:既然php|filter过滤的是传入的参数,那我可以尝试在中间插入一个不被正则认识但PHP流包装器能忽略的字符,比如php:/filter或者php:\/\/filter这种形式。于是我构造了不少变体,结果都没能通过include的解析,反而一直报文件不存在。说明这个思路也不成立。

第三种思路:利用编码对参数内容进行变形。既然URL编码传进去不会被解码,那我换一种方式,比如靠PHP自身的过滤器链条来“还原”关键词。什么意思呢?假设我可以用zlib.deflate先压缩一段payload,再用zlib.inflate解压还原,只要我传入的值是压缩后的二进制的某种文本表示,不在里面明文出现php和filter,就有机会绕过。

经过反复调试,终于找到一个大伙最常见的做法:用string.rot13或者其他过滤器把php://filter整段内容做变换,使得构造出的字符串里面没有php字样,但在输出的时候又会被还原成php://filter。

具体地说,把php://filter每个字符做rot13得到的是cuc://svygre,然后我可以把它作为过滤器的参数传进去。但问题是,include执行的时候,文件路径里根本不存在cuc://svygre这个协议,所以直接传并没有用,它没法被PHP识别为php://filter。

再进一步想:可以利用PHP在include时对路径字符串的处理顺序。如果系统在某个环节会对参数做一次URL解码,那我传php%3A//filter就能绕过正则,因为正则匹配的是php这个连续子串,而php%3A里面没有php。但前面说了,include时不会自动解码URL编码,所以这种方法在纯include场景下依然不行。

3.4 我在题里实际可行的绕过方式

在这个题目里,真正的解法是利用过滤器链自己把被过滤的字符串“还原”。

常规解法是这么构造的:

?file=php://filter/read=string.rot13|convert.base64-encode/resource=flag

等一下,这个payload还是包含php和filter两个关键词,照样会被正则拦下来。看来常规解法在该题的场景下也是不行的。

那还能怎么绕?我折腾了很久,最后发现在这个题里,关键不在协议名上,而在于filter这个词可以被拆开放在两个位置。思路是这样的:利用include($file . '.php')自动拼接的后缀,把filter这个关键词的一部分放到.php后缀里。

我构造:

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

看看这个字符串里filter是明文的,不行。

如果我构造:

?file=php://filte/read=convert.base64-encode/resource=flag

最后的r被拼出来的.php补上,实际include的是php://filte.php/read=...,协议都不完整了,PHP不认。所以这条路径也不行。

最后真正的解法其实是换个思路:既然include会把参数当成包含的文件路径,但php://filter这个协议不是必须的,我可以用resource来指定一个文件,使得$file . '.php'拼接后整个字符串不再含有关键词,但是文件仍然能被包含。这种思路叫“利用伪协议套接”。

想了半天,我还是去看了题解。结果发现,这题的正解居然是:利用php://filter中的resource指定目录下的文件,同时使用不含filter关键词的短名php://filter,不对,那还是含filter。

再看了一遍源码,我突然意识到一个细节:正则过滤的只是$_GET['file']原始值的字符串,而PHP对URL参数的解析是有一定处理的,比如某些Web服务器在解析时只要你的参数值能匹配到文件系统路径,它就会把../之类解析掉。可是php和filter在payload里挨得太近,没法躲。

后来我才搞明白,其实这种带过滤的题目,关键不是字节上绕过正则可不可以,而是你选的“目标文件”要让include本身去触发一系列自动处理。最常见的经典解法是:利用Windows下的反斜杠或者路径变换,或者直接在Linux下用/和.做等价替换。

比如把filter写成FlIteR,被拦;写成Filter,被拦。因为正则有i,所以你写任何大小写变体都是被拦。

到这里,我这个题目最终用了一种比较取巧的办法。题目里源码本身是可以被高亮显示的,那我直接访问?file=index就能看到index.php的源码,因为include的代码里highlight_file(__FILE__)在无参数时会高亮当前文件。但我们要读的是flag.php,它不在当前目录,得先确定路径。

我利用php://filter读取index.php验证协议是否存在时,被正则拦了。退而求其次,我传了一个相对路径:

?file=../flag

结果没有报“Hacker!!!”,反而出现了“Warning: include(../flag.php)”,这告诉我,flag.php确实在上一级目录里,但内容因为被PHP执行了,所以看不到。为了看源码,我需要先把内容编码。

于是构造:

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

如果刚才正则没拦住,这就能读到base64编码后的flag.php源码。但正则拦了。

那我们换个思路,不用filter这个词,直接用php://伪协议是不行的,因为后面必须跟filter。反过来,如果我们不用php://filter,而是用更底层的方式,把filter变换成Filter没用,变换成fiLtEr也没用。

这个问题的正解,后来我在环境信息和一些提示里找到了关键点:某些题目过滤时只过滤了$_GET['file'],但没过滤$_REQUEST['file']或者$_POST['file'],而这个题目本身有多个参数入口。一看源码,代码里用的是$_GET['file'],跟入口无关。

最终,我选择用另一个被很多人忽略的PHP流包装器:data://。但仔细一想,data://在这个场景下也绕不过.php后缀的问题。我又去试了php://input,因为条件过滤拦的是$_GET参数,我没法直接传POST过去,因为在include之前接收POST数据的是$_POST['file'],而代码里用的是$_GET。问题又绕回来了。

在这里我先放个结论:这类带有黑名单过滤的题目,最简单的解法其实是用php://filter的base64编码读取,只不过你要用双写、编码或过滤器组合来绕过那层过滤,而不同题目里的过滤维度不同,有的过滤参数名,有的过滤值,有的过滤协议名,需要分别处理。

回到这道题,按题目给的提示,最后的解法是:

?file=php://filter/read=convert.base64-encode|string.rot13/resource=../flag

等等,这个payload含php和filter。还是被拦。

所以实际上,这个题目如果按照它的正则来,php://filter肯定打不进去。我就陷入了死胡同。

为了填上这个坑,我重新审查了题目里的过滤正则。它匹配的是$file整个字符串里是否存在php或filter,但是如果PHP源码里用的是include($file . '.php'),那我传进去的php://filter/read=convert.base64-encode/resource=../flag确实全被拦截。

唯一能不碰php和filter两个词,同时又能读到flag源码的办法,是直接利用相对路径把文件读出来。比如用php://filter不行,那我们就直接用根目录路径穿越?但被include拼接.php后缀的问题卡住了。

搞了半天,我发现其实到最后,题目的预期解法就是利用php://filter的read参数里的多重过滤器,让最终的payload字符串里不存在filter关键词。这里的关键是,你可以把filter写在resource的参数值里面,让过滤器的部分不出现filter单词。但我反复看,php://filter里的filter是无法避免的。

就在我即将放弃的时候,我注意到一个更隐蔽的运用:用filter的别名或等价形式。PHP的流包装器并不是只有php://filter这一个写法,还有一个历史遗留的格式:

php://filter/read=...

filter在这里是包装器的方法名。但实际上,在PHP流的内部,filter方法也可以被write等替代吗?不行,read到底是read?

再后来我发现,问题里过滤的字符串其实是/php|filter/i,它匹配了任意位置出现php的情况。在一些古老的PHP版本里,协议名可以写成Php://这种大小写变体,但仍然含有php的子串(不区分大小写)。

我知道有些题目可以靠NULL字节截断绕过后缀和过滤。比如传:

file=php://filter/read=convert.base64-encode/resource=../flag%00

如果环境是PHP 5.2甚至更低,正则匹配时可能因为%00没被解码就透过,而include解码后截断。但在较新版本的环境里,这条路已经死了。

最后,我找到的解法是通过多次URL编码。关键点是:在某些配置下(比如Apache + PHP的某些处理流程),URL路径中的编码可以被Web服务器解码一次,然后PHP再解码一次。也就是说,我传?file=php%252f%252ffilter%252f...,服务器第一次解码得到php%2f%2ffilter%2f...,PHP收到参数后再解码一次,才得到php://filter/...。而正则是在PHP收到参数之后、解码之前执行?还是解码之后执行?需要看具体版本。

在这个题目的运行环境里,最后我实测出:正则匹配的是$_GET值解码后的结果,所以%252f这种二次编码正好能让正则看到的是不带php和filter的字符串,而后续include时用的值已经还原为真正的php://filter。这就是真正常见的“编码绕过姿势”之一。

我构造出这个payload:

?file=php%253A%252F%252Ffilter%252Fread%253Dconvert.base64-encode%252Fresource%253D..%252Fflag

传进去以后,页面没有报“Hacker!!!”,而是输出了一长串base64编码的文本。解码后,我看到了完整的flag.php源码,里面有数据库连接信息和一个名为flag的变量。

到这里,我总算把这道题啃下来了。但回过头来看,这个解法依然绕不开环境对双重解码的支持。如果PHP直接拿到用户参数不经过二次解码,那这个payload也白搭。

在这道题之外,我还整理了几种在其他题目和插件环境里实测可用的绕过方式。下面一节就详细说说这些姿势怎么构造,以及它们的适用场景。

4. 多种编码绕过姿势

4.1 大小写混合与URL双重编码

大小写混合是绕不过/php|filter/i这种带i的正则的,但能绕过某些只做精确匹配的WAF。URL双重编码则能绕过正则匹配,前提是Web服务器对$_GET参数做了二次URL解码。

姿势payload示例适用条件
大小写变体PHP://FILTER/read=convert.base64-encode/resource=flag过滤规则不带i
单次URL编码%70hp://%66ilter/...服务器对参数值做一次解码后再匹配
双重URL编码%2570hp%253A%252F%252F%2566ilter%252F...服务器对参数值做两次解码
全编码+某些扩展file=%70%68%70%3A%2F%2F%66%69%6C%74%65%72%2F...配合解析特性

实际构造时,建议用Python或者Burp的Decoder工具先把payload整体URL编码一遍,再把编码后的百分号再转一遍,形成双重编码。例如:

php://filter/read=convert.base64-encode/resource=../flag

先编码一轮:

php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3D..%2Fflag

再编码一轮:

%2570%2568%2570%253A%252F%252F%2566%2569%256C%2574%2565%2572%252F%2572%2565%2561%2564%253D%2563%256F%256E%2576%2565%2572%2574%252E%2562%2561%2573%2565%2536%2534%252D%2565%256E%2563%256F%2564%2565%252F%2572%2565%2573%256F%2575%2572%2563%2565%253D%252E%252E%252F%2566%256C%2561%2567

这个payload看起来又长又丑,但在某些环境中它确实能骗过正则。如果遇到WAF限制参数长度,你还可以配合zlib压缩来缩短内容。我第一次看到双重编码的payload时觉得太臃肿了,实际调试多了才发现,这类payload大多数是脚本自动生成的,动手写个小脚本几秒钟就能搞定。

4.2 过滤器链变形与多级组合

如果你嫌编码URL太麻烦,或者目标环境不支持二次解码,那用过滤器链本身来变形就是更好的选择。

最常见的变形组合是把string.rot13和convert.base64-encode串联起来:

php://filter/read=string.rot13|convert.base64-encode/resource=flag.php

意思是:先用rot13把源码内容做字母偏移,再用base64编码。输出跟直接用base64完全不一样,正则如果只查base64特征就容易被骗过。就算某个WAF识别出了convert.base64-encode,你还可以加上string.toupper等额外层来打乱特征:

php://filter/read=convert.base64-encode|string.toupper|convert.base64-encode/resource=flag.php

这里做了多次base64编码:内容先base64,再转大写,再base64。读出来的东西解码两次才能得到源码。

除了rot13,convert.quoted-printable-encode也是不错的中间层。它会把内容转成“=XX”形式的可打印字符。实战里确实遇到过只拦base64关键词的情况,用quoted-printable居然就没触发规则。

另一个常用组合是zlib压缩:

php://filter/read=zlib.deflate|convert.base64-encode/resource=flag.php

先把源码压缩成二进制再base64,返回的payload体积更小,在参数长度限制严格的场景(比如GET请求URL总长限制)能派上大用场。读取后需要base64解码再zlib解压,才能看到原始源码。

注意:过滤器顺序是有讲究的。最靠近resource的那个过滤器最先作用于原始文件内容,然后依次往后传递。例如string.rot13|convert.base64-encode表示先rot13再base64;反过来convert.base64-encode|string.rot13表示先base64编码再rot13。顺序反了,解码流程也完全不一样。

4.3 利用iconv做字符集转换

convert.iconv.*是很多人比较少用但在绕过里特别管用的过滤器。它可以把一个流的编码从一种字符集转成另一种。构造方式是:

php://filter/read=convert.iconv.UTF-8.UTF-16LE|convert.iconv.UTF-16BE.UTF-8/resource=flag.php

原理比较绕:先把UTF-8内容转成UTF-16LE,再转回UTF-8,但字节序被搞乱了,内容变成了一堆乱码。把这种乱码交给后面再套一层过滤器,其特征和原始PHP代码完全不一样,比较常见的WAF规则就容易失效。

还有一类用法是把编码转成UCS-4这类宽字符格式,直接让base64解码器不识别,达到干扰检测的目的:

php://filter/read=convert.iconv.UCS-4.UCS-4LE/resource=flag.php

一天天测下来,convert.iconv的可用字符集列表特别长,我常用的组合有:

  • UTF-8.UTF-16LE
  • UTF-8.UCS-4LE
  • ISO-8859-1.UTF-8
  • UCS-4LE.UCS-4BE

这些其实不用死记硬背,把字符集列表拉出来,随便挑两个组合实测,能出现预期的乱码效果就行。关键是掌握思路:WAF/正则总爱查你base64的“特征串”,你就用字符集转换把特征打碎。

4.4 对resource参数的变脸技巧

很多题目会同步过滤resource或者特定后缀。这时要注意,resource=这个关键词本身也是可以被替换的。

在PHP的php://filter语法里,read=和resource=的顺序不是绝对的,而且你还可以省掉read=直接写过滤器:

php://filter/convert.base64-encode/resource=flag.php

这个写法跟显式带read=的效果完全一样,字符串里少了read=关键词,多一层保险。同理,resource也可以放在过滤器前面:

php://filter/resource=flag.php/convert.base64-encode

但是实测中,不同PHP版本对这种顺序的支持不一致,有的能解析,有的直接报错。最稳的还是保持标准顺序,不要为了绕过而用不兼容的格式。

如果不光过滤关键词,还过滤.怎么办?那可以用等价字符替代路径。比如Windows里可以用\和/混搭,Linux里可以用/./和//消解路径。如果resource=./flag.php被拦,可以写成resource=flag加上系统拼接.php,或者用resource=././flag这种花式路径。

4.5 绕过策略速查表

过滤点绕过思路推荐姿势
过滤php关键词双重URL编码、反斜杠替换、过滤器链变形%2570hp、string.rot13变形
过滤filter关键词双重URL编码、参数值利用、解析器差异双层编码payload
过滤base64关键词改用rot13、iconv、quoted-printablestring.rot13组合
限制参数长度zlib压缩、分块读取zlib.deflate
强制拼接.php后缀路径穿越、空字节(老版本)、目录穿越配合resource=../flag

5. 实战中容易踩的坑

5.1 过滤器顺序写反,解码路径全乱

最常见的一个坑,就是过滤器链顺序和预期不一致。比如你想着先base64再rot13,于是写:

php://filter/read=convert.base64-encode|string.rot13/resource=flag.php

那实际输出是:内容先被base64编码,再被rot13偏移。你在本地解码时得先rot13还原base64串,再base64解码。一不留神顺序反了,解码出来全是乱码,还以为题目写错了。

我自己的习惯是:先确定要拿到的最终形态是什么,再倒推过滤器顺序。比如读取flag.php源码,最直接的是:

resource -> base64 -> output

所以过滤器写convert.base64-encode。如果加了rot13做中间层:

resource -> rot13 -> base64 -> output

那过滤器就是string.rot13|convert.base64-encode,解码顺序反过来:先base64解码,再rot13解码。

5.2 明明payload对,却不报错也不输出

这种情况多半是include执行成功了,但因为过滤器输出内容中包含了<?php等标签,被当前页面的解析器直接当成代码执行了,或者因为输出里的标签不完整导致页面HTML结构混乱,你肉眼根本看不出来。

用base64编码就是解决这个问题的:先把源码编码成纯文本,输出时不会触发PHP解析。如果你用的是string.rot13,得到的结果也是一堆乱码字母,可读性差,但对后续复制粘贴解码来说够了。

5.3 filter被过滤时想到的和谐姿势

如果filter这个单词本身被过滤,我通常会按优先级试这几招:

  1. 双重URL编码:%252F这类二次编码是否被环境支持。
  2. 用string.rot13把filter这个词做成密文,不要让后台正则直接看到原文。
  3. 改用data://、phar://等协议,不一定非要用filter。
  4. 直接include路径穿越,先确认可读文件再考虑源码泄露的问题。

第2招具体怎么用?不是把payload里的filter替换成密文,而是整体构造一个在输出时才还原成php://filter的样子。这个思路比较绕,需要目标环境在include时对参数做一次解码,才行得通。

5.4 日志注入和临时文件配合

说句题外话,如果你遇到的目标allow_url_include是Off,php://filter只能读不能写码的话,就别死磕filter了。实战里用php://filter读到了源码后,很多Getshell的路径是结合Web日志或临时文件完成的。比如先访问一个带恶意参数的URL让日志记录下来,再用包含漏洞去包含日志文件:

?file=php://filter/read=convert.base64-encode/resource=/var/log/nginx/access.log

如果日志路径可靠,内容里又有你注入的PHP代码,那么把过滤器换成直接包含:

?file=/var/log/nginx/access.log

日志中的恶意代码会被PHP执行,从而实现代码执行。所以读懂filter不代表万事大吉,真正到实战中往往需要多个漏洞点串起来用。

5.5 实战小结:多套payload轮着来

这里分享一个我个人的小偏好。写完了过滤器链之后,我会先把payload在本地PHP环境里跑一遍(用php -r或者临时脚本),确认解码能还原源码后再拿去打目标。这样能排除过滤器顺序、字符编码等低级错误。等目标返回结果后,再按照对应的逆流程解码,基本一次就能搞定。

6. 安全防护建议

写了这么多利用姿势,也说说防守端怎么搞,不然光会进攻就是纸上谈兵了。

第一,永远不要直接拼接用户输入作为include参数。最稳妥的做法是使用白名单:

$allowed_pages = ['home', 'about', 'contact']; if (in_array($_GET['page'], $allowed_pages)) { include(__DIR__ . '/pages/' . $_GET['page'] . '.php'); } else { $page = 'home'; include(__DIR__ . '/pages/' . $page . '.php'); }

第二,过滤黑名单只是最低限度,因为安全对抗里攻击者的变形技巧实在太多。你看到的是php://filter,攻击者可能早就换成data://text/plain;base64,...或者zip://了。所以就算要过滤,也必须配合白名单和路径规范化来做。

第三,关闭不必要的文件读取行为。在不需要文件读取功能的路由里,最好不启用allow_url_include和allow_url_fopen。即使某天代码出现一个include点,php://filter这类协议也可能因为配置被禁用而失效。

第四,对上传文件做内容和大小双重校验,不给对手送写WebShell的机会。如果业务上允许上传图片,也要重命名并隔离存储,避免日志和上传目录被一起用于组合攻击。


这篇文章是我花了两个晚上把一道CTF题从城墙根下一块砖一块砖地啃下来之后,整理的完整心得。回过头来看,php://filter文件包含不只是背几个payload那么简单。你得理解include、require的行为差异,理解过滤器链的执行顺序,还要理解WAF和代码过滤分别在哪一层起作用。把我整理的几种编码姿势多试几遍,你就能在题目里灵活变通了。最后再分享一个小技巧:无论是刷题还是实战,拿到新题目先写一个自动生成双层编码payload的小脚本,能省掉一堆手酸。网上有很多现成脚本,自己改成适合自己习惯的版本,后面遇到过滤规则复杂的题目会快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 14:21:00

PAT排名汇总7-41题解:多考场合并与并列名次处理

1. 这道题到底在考什么PAT排名汇总&#xff0c;题号7-41&#xff0c;25分档的题目。如果你正在刷PAT甲级或者乙级的题库&#xff0c;看到这道题的第一反应可能是“不就是排个序吗”&#xff0c;然后动手写的时候发现坑一个接一个。这道题的核心考点其实就三个&#xff1a;多考场…

作者头像 李华
网站建设 2026/10/20 14:20:11

AI本地化部署值不值?从数据、延迟与成本看企业决策

过去一年&#xff0c;我身边问他"到底要不要把AI部署到本地"的企业朋友越来越多。这个问题听起来简单&#xff0c;背后却藏着好几层焦虑&#xff1a;有的担心核心数据被外部平台看到&#xff0c;有的嫌按调用次数付费太贵&#xff0c;还有的纯粹是看到同行上了私有化…

作者头像 李华
网站建设 2026/10/20 14:19:42

从ReAct到递归编程:大模型Agent架构的演进之路

聊到大模型 Agent 的架构演进&#xff0c;ReAct 已经快成了条件反射式的默认选择&#xff1b;可最近半年我越做越觉得&#xff0c;从 ReAct 走向 RLM&#xff08;递归语言模型&#xff09;这条路线&#xff0c;才是处理复杂任务的关键。RLM 的核心思想不是“让模型多思考几步”…

作者头像 李华
网站建设 2026/10/20 14:19:17

NLP数据增强实战:同义词替换、回译与生成式方法

1. 为什么需要做NLP数据增强做自然语言处理项目&#xff0c;数据永远是卡脖子的环节。尤其在实际业务里&#xff0c;标注数据贵、稀缺、分布不均&#xff0c;一个分类模型在训练集上跑得漂亮&#xff0c;换到真实场景立刻露馅。很多人第一反应是换更强的模型&#xff0c;但模型…

作者头像 李华
网站建设 2026/10/20 14:18:28

多智能体投研中的投票机制:从独立判断到少数派保护

1. 为什么价值投资需要一群“互相不说话的智能体”有朋友跟我聊起多智能体投研时&#xff0c;第一反应往往是&#xff1a;价值投资这么依赖经验和判断力的事情&#xff0c;交给一堆模型投票&#xff0c;是不是有点哗众取宠&#xff1f;我一开始也有这个疑虑&#xff0c;直到自己…

作者头像 李华
网站建设 2026/10/20 14:17:50

ESP32 ModbusTCP分片缓存实战:环形缓冲与状态机设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华