☰
PHP文件包含漏洞实战:Web_php_include伪协议绕过全解析
2026/10/12 5:05:54 网站建设 项目流程

第一次在攻防世界刷Web题的人,大概率会撞上Web_php_include这道题。它不要求你懂多高深的内网渗透,也不涉及复杂的密码学,核心就考一件事:你对PHP文件包含和几个伪协议到底熟不熟。这篇WP我按“胎教”标准来写,哪怕你现在只会用浏览器、连Burp都没装过,跟着一步步做也能把flag拿到。代码我会掰开揉碎,为什么能绕过、为什么这样构造payload,我会讲清楚,而不是甩一个最终payload就完事。

练习环境提示:这篇WP里的所有操作都基于CTF竞赛靶场,目标是你自己账号下的题目环境。网络安全技术请只用在自己有授权的目标上,别拿去对公网网站做任何测试。

1. 先动手试:环境长什么样,这题在考什么

1.1 打开页面就能看到的东西

把题目环境打开,页面没有复杂的前端交互,一进来就是一大段高亮显示的PHP代码。很多新手看到这一步就懵了:“怎么把源码直接放出来了?”其实这恰恰是题目在放水。

代码大概是这样的:

<?php show_source(__FILE__); echo "<br/>"; $page = $_GET['page']; if (isset($page)) { if (strstr($page, "php")) { echo "no"; } else { include($page); } } else { echo "please input page parameter"; } ?>

第一行show_source(__FILE__),作用就是把当前文件源码以高亮形式输出到页面上。也就是说,题目故意让你看到完整源码。做CTF题的第一步永远是看源码,这一行直接把这一步省了。

继续往下看,$page = $_GET['page'];是在接收URL里的page参数。这里的逻辑也很简单:如果没有传page参数,就提示“please input page parameter”;如果传了,就判断$page里面有没有php这个字符串,有就输出no,没有就include($page)。

看到这里,考点已经浮出来了:include一个用户可控的变量,这是非常典型的文件包含漏洞模型。

1.2 一行一行读PHP代码,漏洞点在哪里

新手容易在strstr这个函数上卡住,我先把它讲明白。strstr($page, "php")做的事情是:在$page这个字符串里查找有没有"php"这三个字符,如果有,返回从"php"开始到结尾的字符串;如果没有,返回false。这里把它放到if条件里,意思就是:只要$page里面出现了小写php,直接被拦下,页面回显一个“no”。

也就是说,题目本意是想阻止你用php://开头的伪协议。因为php://filter、php://input这类地址天然就带php,一多半的常规解法会被这个过滤挡住。

同理,include这边是漏洞点。include($page)会把$page当作文件路径去包含。如果$page是本地路径,比如/etc/passwd,它会把系统文件内容读出来;如果$page是一个URL或伪协议,它就会走PHP的流包装器。

我建议先做一个最简单的本地包含测试,在URL后面拼上:

?page=/etc/passwd

如果页面上出现了root:x:0:0:root:...这一类内容,说明本地文件包含已经成立。这时就可以正式进入下一步思考:怎么绕过php过滤,把这个include变成能拿flag的武器。

1.3 第一次尝试为什么会失败

新手的第一反应通常是构造常规payload,比如:

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

提交后页面果断回了一个“no”。原因就是strstr($page, "php")检测到了php://里的php,直接拦下。有人会想,把php://换成PHP://行不行?这就是关键突破口,我后面单独讲。

但现在先冷静分析一下:就算大小写换成PHP://,这个payload还有没有别的问题?有。resource=flag.php这个文件名末尾也有php三个字符。也就是说,strstr会连文件名一起检查。只要最终URL里出现小写php,都会被拦。这个坑很多题解都没提,但它恰恰决定了这道题不能靠“大小写+filter读flag.php”一步到位。

所以真正要解决的是:怎么在URL参数里彻底不出现php这三个字符,又仍然能达到“包含并执行PHP代码”的效果。

2. 不懂伪协议,这题就是死路:三个协议一次讲清

2.1 include怎么会和协议扯上关系

很多没接触过PHP流包装器的人会有个疑问:include平时不是用来包含文件的吗,为什么还能塞一个php://、data://这种看起来不像路径的东西?

因为在PHP里,include支持“URL包装器”。你可以把php://filter、php://input、data://当成一种特殊格式的“文件路径”。当PHP看到://,它会调对应的协议处理器来打开数据流。include一个数据流时,如果流里的内容是PHP代码,PHP会在当前上下文中执行这些代码;如果是普通文本,就直接输出。

这个机制非常厉害。普通文件包含最多让你读到文本,有了伪协议,直接可以把“包含一个文件”变成“执行一段任意代码”,也就是从LFI往RCE升级。

搭配题目的过滤条件,我们要找的协议,是在自身名字和参数里都不出现小写php,或者能把php藏起来的方案。

2.2 php://filter:读源码的“透视镜”

php://filter是最常用的读文件协议之一,它的标准格式是:

php://filter/read=convert.base64-encode/resource=目标文件

比如我要读当前目录下的flag.php源码,理论上应该写:

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

它会先把flag.php内容经过base64_encode转换,再把结果作为数据流交给include输出。为什么要转成base64?因为include直接读PHP文件时,会把里面的PHP代码当成代码执行,你最终看不到源码;而把内容base64编码后,PHP解释器会把它当成普通字符串,原原本本显示出来。拿到base64字符串后再自己解码,就能看到flag.php的原始内容。

但在这个题目里,php://filter这个串本身就带php,直接用会被拦。要绕过,就得利用PHP对协议名称大小写不敏感的特性,把它改写成PHP://filter。

2.3 php://input:把请求体当文件读

php://input是一个只读流,负责读取请求体。也就是说,如果URL指向php://input,而POST请求体里放了一段PHP代码,include就会去执行这段代码。

使用方式很好理解:

POST /?page=php://input HTTP/1.1 Host: 目标环境 <?php system('cat flag.php'); ?>

不过同样,php://input字符串里有小写php,又会被拦。于是又得用大小写改写,比如PHP://input。注意,POST请求体里的代码不受strstr检查影响,因为过滤只检查URL参数$page。

这个方案的好处是代码不用base64编码,逻辑清晰,适合在Burp或curl里操作。

2.4 data://:自带代码执行的新鲜玩法

data://是第三个重要协议,它允许你把一段数据直接写进URL里。常见用法有两种:

data://text/plain,明文代码 data://text/plain;base64,base64编码后的代码

第一种方式很容易翻车,因为明文代码里一旦出现?php这种片段,strstr($page, "php")会直接拦下。比如:

data://text/plain,<?php system('ls');?>

这段URL里含有?php,正好包含小写php,必然被拦截。

所以要用base64编码版本,把代码整个藏起来:

data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw==

这样URL里就没有php字样了,可以绕过过滤。这是这道题目前最优雅的解法,也是网上一票题解的主推方案。

2.5 三个协议怎么选,先看这张表

协议是否受allow_url_include影响本题目中能否绕过strstr过滤用途
php://filter否改写为PHP://filter可绕过,但不能在resource中出现小写php文件名读源码
php://input是改写为PHP://input可绕过执行任意代码
data://text/plain;base64是base64内容不出现php即可直接绕过执行任意代码

这里的allow_url_include是PHP配置项,决定include能不能包含远程URL和部分伪协议。php://filter属于内置流,不受它控制;data://和php://input则需要它处于开启状态。所以在CTF环境里,如果data://方案不生效,优先检查是不是配置限制。

3. 第一招:用大小写绕过PHP://filter看源码

3.1 大小写绕过到底为什么成立

strstr($page, "php")是区分大小写的。它只认小写的p-h-p三个字符,换成PHP、Php、pHp它都不认识。

PHP的流包装器名称则相反,官方手册里明确了它不区分大小写。也就是说,php://filter和PHP://filter对PHP来说是同一个协议,都能正常解析。

于是绕过逻辑就通了:strstr区分大小写的特性,配PHP协议大小写不敏感的特性,我们把所有php改成PHP,过滤就失效了。

但这里必须再强调一次:strstr是对整个$page做检查,不是只检查协议前缀。后面跟的resource=flag.php里的小写php一样会被发现。如果只改协议部分,提交:

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

大概率仍然回显“no”,因为flag.php里藏着小写php。这就是我在1.3埋下的坑。

3.2 实操:把index.php源码安全读出来

既然不能直接读flag.php,那第一步先读一个名字里没有php的文件,最合理的目标就是当前页面文件。有些题目环境里文件叫index.php,等等,这名字也有php。

这就尴尬了,index.php照样有小写php,读它也会被拦。那要读什么才能绕开?可以读info.php吗?也有php。只要文件后缀是.php,文件名里必带php。

所以结论是:在这道题的过滤规则下,PHP://filter读不出任何.php文件。这步操作的唯一价值,是帮我们验证大小写绕过的思路行不行得通。怎么验证?读一个非PHP文件,比如/etc/passwd,用代码:

?page=PHP://filter/read=convert.base64-encode/resource=/etc/passwd

如果返回一段base64,解码出来是root:x:0:0:root:...,就证明大小写绕过成功,协议解析正常。

3.3 为什么这条路没法直接读flag.php

把原因再拆开说一遍:strstr($page, "php")查的是整个$page值,flag.php文件名的.php后缀躲不掉。只要目标是.php结尾文件,大小写方案就会二次撞墙。

有同学会想:那我把它再编码一次,比如把flag.php写成flag%2Ephp,%2E是URL编码的点。问题是PHP拿到$page时,URL解码已经完成,%2E会还原成.,最终$page里还是会出现flag.php。换成fl%61g.php同理,解码后照样有php。过滤发生在参数值解码之后,所以这种二次编码逃不掉。

既然读源码这条路被文件名后缀卡死,那更聪明的思路是:不读文件,直接让include执行代码。这就引出第四部分的两种拿flag方式。

4. 真正解题:两种姿势把flag拿下来

4.1 姿势一:data://base64一条URL执行命令

先想清楚目标:拿到flag最常见的方式,就是执行命令ls看当前目录有哪些文件,然后cat flag.php把内容打出来。但命令里只要出现flag.php这个文件名,整个payload就又包含php了。

解决方案很巧妙:把整段PHP代码做成base64,塞进data://协议里。strstr看到的是base64字符集(字母、数字、+、/、=),里面没有小写php,所以过滤直接绕过。

第一步,在本地把代码转成base64:

<?php system('ls'); ?>

我在本地命令行执行:

echo -n '<?php system("ls"); ?>' | base64

得到:

PD9waHAgc3lzdGVtKCJscyIpOw==

(如果你是Windows环境或不想用命令行,也可以用Python,见5.5节。)

然后把这段base64拼进URL:

?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOw==

我截图般地描述一下结果:页面上会直接输出当前目录的文件列表,其中能看到类似flag.php、index.php这样的文件。到了这里,RCE已经成功。

第二步,读flag。把命令换成cat flag.php,但要避免URL里出现php,所以继续用base64:

<?php system('cat flag.php'); ?>

对应的base64为:

PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==

最终payload:

?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==

提交后,页面会打印出flag.php的源码内容,里面通常直接写着flag{...}。如果页面只输出了一部分,可以顺手用cat flag.php | base64再打一次,避免特殊字符显示问题。

4.2 姿势二:PHP://input配合POST body

另外一个思路是用PHP://input。URL里写PHP://input,大小写绕过了strstr检查,然后在POST请求体里塞PHP代码。

用curl来演示最直观。假设目标环境地址是http://目标环境/,命令如下:

curl -X POST -d '<?php system("cat flag.php"); ?>' \ "http://目标环境/?page=PHP://input"

这里-d指定POST数据,PHP://input会让include直接读取刚上传的请求体,也就是<?php system("cat flag.php"); ?>这段代码。PHP解释器会在包含时执行它,页面自然回显flag。

如果手边有Burp Suite,也可以拦截请求后手动改成POST,在消息体里写代码,效果一样。

这个方案最大的优点是POST body不参与strstr($page, "php")的判断,代码怎么写都行,完全不受“不能出现php字符”的制约。缺点是操作步骤比data://多一步,要用curl或Burp,不能像data://那样直接在浏览器地址栏完成。

4.3 两种方式怎么选,更适合什么场景

对比项data://base64PHP://input
是否需要额外工具不需要,浏览器地址栏直接打需要curl/Burp等能发POST的工具
代码是否需要编码必须先base64编码不用,明文直接写
URL里是否会出现phpbase64内容可控,不会出现只写PHP://input,没有小写php
适合场景快速验证、答题、用hackbar需要跑较复杂PHP代码、多行脚本时

我个人的使用习惯是:答题时优先data://base64,快、稳、不用装任何东西;调试复杂代码时优先PHP://input,代码可读性高,不用反复编解码。两道经典payload打完,这道题的flag已经到手了。

5. 新手易踩的坑:实测排查记录

5.1 base64里的+号莫名其妙变成了空格

这是我见过最多新手翻车的地方。base64编码结果完全有可能包含+字符,而URL查询参数里,+会被解析成空格。一旦base64字符串中的+变成空格,解码出来的内容就是错的,include要么报错,要么执行不了。

举个例子,对<?php system('id');?>做base64,结果是:

PD9waHAgc3lzdGVtKCdpZCcpOz8+

最后就是一个+。直接放进URL:

?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8+

PHP拿到的是PD9waHAgc3lzdGVtKCdpZCcpOz8(末尾变成空格),base64解码失败或者解出残缺内容。解决办法是把+编码成%2B:

?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8%2B

更稳妥的做法是拿到base64后,整段做一次URL编码再放进地址栏。用Burp的话,直接把请求发给Repeater,选中base64部分按一下自动URL编码,很方便。

5.2 访问flag.php返回空白,以为题目坏了

如果你直接用浏览器访问http://目标环境/flag.php,大概率看到一片空白,或者只有一行空内容。这不是靶场坏了,而是flag.php里的内容本身是PHP代码:

<?php $flag = "flag{test_flag}"; ?>

它被PHP解释器执行了,但没有输出任何可见内容。$flag这个变量藏在内存里,页面上什么都不显示。

所以,要想看到flag,必须借助php://filter这类“读源码”手段,或者执行cat命令把文件原文打出来,而不是直接访问.php文件。这个认知能让你少走很多弯路。

5.3 include报错“failed to open stream”怎么办

如果提交data://或PHP://input后报错:

Warning: include(): URL file-access is disabled in the server configuration

或者:

failed to open stream: no suitable wrapper could be found

基本可以判定环境把allow_url_include关掉了。data://和php://input都依赖这个配置项,关掉就废了。CTF平台一般会为这题开这个选项,但如果碰到变种题,就必须换思路。

php://filter不受allow_url_include影响,所以“读源码”这条路通常还是通的。如果data://不能RCE,优先尝试用php://filter读源码,把能读的文件都读一遍,flag很可能就藏在源码注释或配置文件里。再不行,就要考虑日志包含、Session文件包含这些进阶手法。

5.4 过滤函数里还有哪些类似的小坑

这道题的过滤函数是strstr,它区分大小写,给了我们绕过的空间。但如果是strpos呢?strpos($page, "php")一样区分大小写,可以用PHP://绕过。如果过滤条件改成stripos($page, "php"),它不区分大小写,大小写招就失效了。

还有一种经典坑:有人自己写过滤时用strpos($page, "php") == false来判断“不含php”。PHP里strpos在php出现在字符串开头时返回整数0,而0 == false在弱比较下是成立的,于是直接把合法的php://给放过或者拦错方向。幸好这题用的是strstr,没有这个坑,但你在以后审计别人代码时,遇到strpos的===和!==一定要多留个心眼。

5.5 本地秒算base64的三种方式

答题时反复编解码很常见,我常用的三种方式:

Linux/macOS下直接用管道:

echo -n '<?php system("ls"); ?>' | base64

Windows PowerShell下:

[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('<?php system("ls"); ?>'))

Python一句搞定,跨平台:

python3 -c "import base64; print(base64.b64encode(b'<?php system(\"ls\"); ?>').decode())"

顺手提醒一句,别用网页在线base64工具去转换包含payload的代码,首先容易有数据泄露风险,其次粘贴复制也容易出错。本地命令转,干净利落。

6. 赛后复盘:下次遇到include题怎么秒

6.1 拿到include题先列三件事

做完这题我复盘了一套自己的解题流程,再遇到文件包含类题目,我会先列三件事:

第一,确认过滤规则。过滤了哪些关键字、是协议名还是文件名、是不是大小写敏感、是不是strstr或stripos。这决定了用大小写绕过还是编码绕过。

第二,判断可用的伪协议。当前环境下data://和php://input是否可用,不行就看php://filter能不能读源码。配置错误提示本身也是一种信息。

第三,确定目标是读源码还是RCE。如果flag写在PHP文件里且没有其他出口,RCE是最稳的;如果环境限制多,老老实实读源码找flag。

这三件事列完,这道题的解题路径基本就浮出水面了。

6.2 但凡是include题,都可能有哪些变种

文件包含的变种多到写不完,但常见套路就那么几类:

过滤协议名,比如php://、data://,可以大小写绕过,也可以换协议、加编码。

过滤://,这种更难缠。但可以试试用协议名称前缀变形,比如data:text/plain;base64,xxx在部分PHP版本里也能识别,或者退一步走日志包含。

限制包含路径,比如必须包含某个目录下的文件。这种情况可以试目录穿越../../../../etc/passwd,或者结合文件上传包含上传后的图片马。

还有一类是把用户输入放进文件名模板,比如include("pages/" . $page . ".php")。这种就必须用php://filter配合截断老技巧,或者利用现有文件做二次包含。

每次看到变种题,都把它和我上面列的三件事对照一遍,发现哪个身份不对路,立刻换思路。

6.3 防御视角:这个洞是怎么堵上的

赛后从防御角度看,题目故意留的漏洞其实很好堵。第一个层次,include的输入参数必须做白名单校验,比如固定允许page=home、page=about,其它值直接拒绝。第二个层次,如果确实要动态包含,也应该限制在本地指定目录,用realpath校验包含路径是否在范围内,杜绝伪协议和穿越。第三个层次,生产环境务必把allow_url_include置为Off,并配合open_basedir限制PHP可访问的目录范围。

不过话说回来,CTF题目的意义就是故意打开一扇门让我们看清楚攻击手法长什么样。真正到真实环境,你会遇到更多组合拳,比如过滤、白名单、WAF一起上。正因为靶场给了我们安全试错的空间,才要在答题之外多想一步:这种漏洞是怎么被发现的、怎么被利用的、换成我是运维会怎么防。带着这三层视角去刷题,收获会比单纯拿flag大得多。


最后说点个人体会。我第一次刷这道题时,就是败在flag.php文件名里的php上,当时死活不明白为什么已经用了PHP://filter还是被拦,后来才意识到strstr检查的是整个参数值,不只是协议前缀。想通这一点后,整个题的所有解法都串起来了:过滤规则要求URL里不能出现php,那我就让URL里一段php都没有,用PHP://input,用data://base64,思路一下就通透了。新手做CTF题,别急着背payload,先搞明白过滤依据是什么,再顺着依据找绕法,比自己闷头试要快得多。这道题就很适合作为你Web文件包含方向的入门第一题,建议上手敲一遍,卡住了再回来看这篇WP。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询