
第一次在攻防世界刷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比如?pagephp://filter/readconvert.base64-encode/resourceflag.php提交后页面果断回了一个“no”。原因就是strstr($page, php)检测到了php://里的php直接拦下。有人会想把php://换成PHP://行不行这就是关键突破口我后面单独讲。但现在先冷静分析一下就算大小写换成PHP://这个payload还有没有别的问题有。resourceflag.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/readconvert.base64-encode/resource目标文件比如我要读当前目录下的flag.php源码理论上应该写php://filter/readconvert.base64-encode/resourceflag.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 /?pagephp://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做检查不是只检查协议前缀。后面跟的resourceflag.php里的小写php一样会被发现。如果只改协议部分提交?pagePHP://filter/readconvert.base64-encode/resourceflag.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用代码?pagePHP://filter/readconvert.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?pagedata://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?pagedata://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://目标环境/?pagePHP://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?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8PHP拿到的是PD9waHAgc3lzdGVtKCdpZCcpOz8末尾变成空格base64解码失败或者解出残缺内容。解决办法是把编码成%2B?pagedata://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); ? | base64Windows 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的输入参数必须做白名单校验比如固定允许pagehome、pageabout其它值直接拒绝。第二个层次如果确实要动态包含也应该限制在本地指定目录用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。