资讯动态

从Web_php_include看PHP文件包含漏洞:伪协议利用与防御

发布时间:2026/10/14 4:26:31 来源:尧图企业网站定制
在CTF圈子里Web方向的入门题里Web_php_include大概是很多人第一次被“文件包含”四个字按在地上摩擦的题目。刷题平台上它常年挂在Web入门模块题目本身不复杂但围绕它展开的php://filter、php://input、data://这些伪协议玩法几乎是后面所有文件包含题的地基。这篇writeup我尽可能写得“胎教”一点从题目拿到手怎么读代码开始到三种主流解法、绕过思路、本地复现一路讲到防御侧怎么修争取让零基础的朋友也能完整走通这条链路。先交代一下场景在某CTF练习平台的Web模块里题目名称就是Web_php_include属于典型的LFI本地文件包含漏洞利用题。题目会给一个PHP页面页面本身直接输出了一段源码提示flag藏在同目录的flag.php里同时页面接收一个page参数并通过include引入。看到这里整道题的核心矛盾已经摆在面前怎么让include替你把flag.php的内容带出来或者直接替你在服务器上执行代码。下文全部围绕这个目标展开所有操作都在题目靶场的授权范围内进行放心食用。1. 先搞懂这道题到底在考什么文件包含与伪协议基础很多新手上来就急着找payload结果?pageflag.php被拦一下就直接卡住。要真正理解这道题最好先把背后的两个基础知识点补上文件包含机制和PHP伪协议。这两个东西单独看不难但组合在一起就成了出题人埋坑的地方。1.1 文件包含漏洞的本质PHP里include、include_once、require、require_once四个函数都是把外部文件内容“拿进来”执行。它们的区别不大require在文件加载失败时会直接报致命错误终止脚本include只会出警告继续跑include_once和require_once则多了一个“只加载一次”的判断。在漏洞利用层面它们的行为基本一致关键是只要参数可控文件包含函数就能被用来加载任意文件。用一个生活化的类比include就像一个“配送员”你告诉它“去取某个文件”它就老老实实把文件取回来放到当前代码里。正常情况下取的是公司自己的文件但如果地址是攻击者提供的配送员也照样取这就失控了。文件包含漏洞的根源就是“配送地址”没有做任何校验。1.2 PHP伪协议到底是一组什么魔法PHP提供给文件处理函数一套特殊的“协议”让它们不仅能打开本地文件路径还能打开各种“虚拟流”。这些协议统称为伪协议最常用的是下面这几个协议用法示例作用备注php://filterphp://filter/convert.base64-encode/resourceflag.php读取文件源码并做指定编码处理不需要allow_url_include开启php://inputinclude(php://input)把POST请求体当作文件内容读入需要allow_url_includeOndata://data://text/plain;base64,xxxx把字符串当作文件流处理需要allow_url_includeOnfile://file:///etc/passwd访问本地文件系统一般默认可用http://http://example.com/shell.txt远程包含外部文件需要allow_url_includeOn这里面最关键的一个开关叫allow_url_include。PHP配置文件php.ini里默认它是Off一旦设为On就允许include去加载http://、ftp://这类远程URL同时也允许php://input和data://在include中被使用。CTF出题人为了让题目可解经常会把环境里的这个配置打开所以php://input和data://在很多题里都能直接打穿。1.3 为什么php://filter是个特殊的存在php://filter在文件包含题里地位特殊因为它根本不依赖allow_url_include。它的作用是在读取文件内容时套一层“过滤器”例如convert.base64-encode就是把原始内容编码成base64再返回。用它读取flag.php时PHP不是把flag.php当PHP代码执行而是当作普通文件读取经过base64编码后原样输出于是源码就暴露了。这个特性非常重要因为题目里的flag.php是PHP代码如果直接include它PHP会把里面的?php ?部分当成代码执行flag文本可能藏在变量里根本看不到。用php://filter读文件则绕过了“执行代码”这一步只做“读内容”。这也是它成为文件包含题首选手段的原因。2. 拿到题目先别急着打逐行读初始代码看清过滤规则打开题目页面直接显示出一段PHP源码。不同版本可能略有区别但我遇到的这个版本很有代表性代码大致长这样?php show_source(__FILE__); echo flag在flag.php中; $page $_GET[page]; if (!preg_match(/flag/i, $page)) { include $page; } ?这段代码信息量很大。第一行show_source(__FILE__)是把当前文件源码输出到页面上等于出题人主动把代码展示出来省去翻源码的步骤新手友好的同时也说明这题的重点不在信息收集而在利用方式。第三行从GET参数里取page第五行用正则表达式检查$page里是否包含“flag”三个字符注意后面的i修饰符表示不区分大小写所以FLAG.php、Flag.php都会被拦。最后一行include $page;就是漏洞点。2.1 直接尝试?pageflag.php会发生什么我用浏览器直接访问/index.php?pageflag.php结果是什么页面上没有任何flag输出或者说页面结构和之前一样因为preg_match(/flag/i, flag.php)匹配成功了条件不成立include根本没有执行。这就是题目设下的第一道坎参数里不允许出现flag字样。但这里有个容易忽略的细节过滤只盯着page参数本身的字符串内容根本没限制伪协议的写法。换句话说只要URL里不直接出现flag四个字符能让include最终读到flag.php就算绕过成功。2.2 从代码里提炼出的攻击面我习惯把这类题目的攻击面列成清单方便后面逐条试可控点GET参数page值会被include使用。过滤规则page值不能包含flag不区分大小写。目标拿到flag.php里的内容或在服务器上执行命令。可用工具include支持的文件流类型取决于PHP环境配置。由此延伸出来的思路很直接把flag.php的路径拆开、隐藏、改写让正则匹配不到“flag”同时include又能正确解析出目标文件。php://filter恰好就是干这个的。2.3 一个练手小实验先验证环境配置在正式尝试payload之前可以先用无害的参数确认伪协议是否可用。比如访问/index.php?pagephp://filter/convert.base64-encode/resourceindex.php如果页面里出现一段base64文本解码后正好是当前页面的源码说明php://filter这条路通着环境没有对filter协议做额外限制。这一步实验花不了十秒但能立刻验证“这套打法在当前环境是否成立”后续思路也能笃定很多。我每次刷题都会先做这种“探路请求”避免费半天劲构造payload到头来被环境配置卡死。3. 最稳的一条路php://filter读源码拿flag不费劲确认php://filter可用之后直接构造读取flag.php的payload。关键是URL里不能有“flag”字样而php://filter/convert.base64-encode/resourceflag.php这串字符串里明晃晃地带着flag.php直接放上去照样会被正则拦掉。所以第一步要想办法把flag.php这个文件名做一次“伪装”。3.1 为什么resourceflag.php会被识别但URL还能绕过这里的关键是payload里的文件名仍然会被preg_match检查。也就是说?pagephp://filter/convert.base64-encode/resourceflag.php里的整个page值是php://filter/convert.base64-encode/resourceflag.php它包含flag四个连续字符正则匹配成功include不会执行。所以以上写法是不行的。那怎么在不出现flag字符串的情况下指定flag.php常见的做法是借助PHP文件路径的解析特性flag.php可以写成./flag.php这不带flag吗还是带。写成fla%67.php在URL参数里%67会被解码成g然后include接收到的还是flag.php正则匹配的是解码前的还是解码后的如果PHP使用的是$_GET[page]而$_GET里的值已经完成了一次URL解码正则检查的是解码后的字符串flag.php所以%67这种写法躲不过检查。那究竟怎么绕最常见的解法是用php://filter的过滤器列表和相对路径技巧把flag拆开。比如/index.php?pagephp://filter/convert.base64-encode/resource./fla g.php这种写法行不通因为路径里有空格但思路对——用./前缀和字符拆分。更实际的方案是使用resourcephp://filter/...套娃吗也不是。正确的答案是不需要让URL里不出现flag只要让preg_match匹配不到就行。有人会想到大小写混写fLaG.php但正则带了i修饰符大小写混写照样匹配。还有人会想到在flag中间插入/或%00截断但任何字符都会改变文件名include找不到文件。那到底怎么绕其实答案藏在php://filter的过滤器语法里。把完整目标写成php://filter/convert.base64-encode/resourceflag.php里头的flag.php一定会被匹配到但我们可以换一种协议写法把flag.php放到过滤器的resource部分然后用另一个过滤器的名称来“容纳”部分字符。比如使用zlib相关的过滤器组合但最通用的其实是利用resource参数指向一个路径其中flag被拆成两部分拼接而php://filter不支持这种动态拼接。所以实际CTF题里这道题最主流的解法根本不去纠结“怎么让URL不出现flag”而是直接接受会被拦的现实换用完全不同的协议例如php://input和data://。这两个协议里系统执行的代码根本不包含flag.php字样。比如用php://input时pagephp://input不包含flag正则放行然后POST数据里写命令命令里包含cat flag.php但那是对system执行来说的跟preg_match检查的$page没有关系。正则只检查page的值检查不到POST body里的内容。data://同理pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waGAnKTs/Pg这段字符串里没有明文flag字样base64编码后的内容自然也不含flag四个字符所以正则放行。这正是这道题设计的精妙之处它过滤的是“参数里不许出现flag”而不是“不许读flag.php”。只要换个协议把敏感文件名藏进编码或请求体里就直接绕过。3.2 所以php://filter真的不能用吗也不是。如果执意用php://filter有一个实际可用的技巧先包含一个不含flag字符串的文件再通过过滤器链读flag.php——但PHP的include只有一次解析机会没法读完一个文件后继续指定下一个文件。所以php://filter在本题里直接读flag.php的写法确实会被正则拦。那为什么很多WP里还是能看到php://filter的解法因为很多版本的环境正则过滤条件不同。有的版本过滤规则是preg_match(/flag/i, $_GET[flag])检查的是另一个参数有的版本过滤规则相对宽松允许resource./flag.php出现。我这个版本遇到的过滤规则恰好针对整个page参数的值所以直接filter读flag.php走不通需要换思路。写这篇WP时我把两种情况都讲清楚免得不同环境下照着抄会翻车。3.3 如果环境允许filter读flag的完整操作步骤假设你遇到的版本允许page里出现flag.php例如过滤条件存在其他绕法或者直接引入了$page后再检查$page中不含php标准payload是/index.php?pagephp://filter/convert.base64-encode/resourceflag.php页面会输出一段base64例如PD9waHAgJGZsYWc9IkNURntwaHBfZmlsdGVyX2lzX3VzZWZ1bH0iID8。拿到后把它丢到base64 -d命令里或者用任何在线解码工具就能看到flag.php的源码flag一眼就能找到。这种方法的优点是不依赖allow_url_include在绝大多数PHP环境里都能用属于文件包含题的“通用解”。4. 命令执行路线php://input和data://两种经典玩法当php://filter的写法被正则拦住时题目真正的正解通常是php://input或data://。这两个协议都需要allow_url_include开启CTF出题环境基本默认开启所以可以直接打。4.1php://input用POST请求体冒充文件内容php://input的性质很特别当include(php://input)被执行时PHP会把请求的原始body当作要包含的内容。也就是说你可以控制include加载的“文件内容”。这就好比配送员说“你要的文件我没找到但这里有一张纸条内容是我现写的”然后代码就真把纸条当文件执行了。操作流程如下构造URL/index.php?pagephp://inputpage的值是php://input不包含flag正则检查通过。同时用POST方式发送原始数据。发送的body内容是一段PHP代码目的是执行系统命令读取flag.php?php system(cat flag.php); ?这里cat flag.php是Linux下查看文件内容的命令Windows靶场可以用type flag.php。CTF靶场基本都是Linux所以习惯性用cat。发送请求后PHP会执行这段代码system(cat flag.php)的返回结果会直接输出到页面flag自然就出来了。用curl命令可以一条龙完成curl -d ?php system(cat flag.php); ? http://目标地址/index.php?pagephp://input注意-d参数会默认以application/x-www-form-urlencoded格式发送但对php://input来说读取的就是原始body所以-d完全够用。如果想更严谨可以加-H Content-Type: text/plain。这段payload里的system(cat flag.php)本身包含flag字符串但它不在page参数里正则扫描不到所以完全没问题。这就是用php://input绕过的本质把敏感信息从URL参数层转移到请求体层。4.2data://把代码塞进URL的base64区data://协议和php://input思路类似但实现方式不同。它允许你在URL里直接嵌入一段“数据流”include会把它当作文件内容处理。格式是data://text/plain;base64,base64编码的PHP代码同样用system(cat flag.php)先对它做base64编码。可以用命令行echo -n ?php system(cat flag.php); ? | base64得到的base64串是PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTsgPz4。因为编码后的字符串里不包含明文的flag所以直接拼进page参数不会触发正则/index.php?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTsgPz4访问这个URL服务端执行了嵌入的PHP代码flag.php内容被cat出来。两种方式的效果一样都能拿到flag区别只在于php://input把代码放在请求体data://把代码编码后放在URL里。4.3 为什么这两条路都依赖allow_url_include如果php.ini里allow_url_includeOff那么include(php://input)和include(data://...)都会直接报错因为PHP出于安全考虑禁止include加载这类数据流。这个配置项是区分“能不能玩”的关键。CTF靶场为了帮新手理解漏洞经常会把allow_url_include打开所以这类题目在平台上普遍可解。遇到真的开不了的情况才需要用下一章的进阶思路。4.4 三条路怎么选适用场景对比方法核心payload依赖条件优点缺点php://filter?pagephp://filter/convert.base64-encode/resourceflag.php无特殊配置通用、稳定参数容易触发字符串过滤php://input?pagephp://input POST代码allow_url_includeOn参数干净、易绕过滤需要发送POST请求data://?pagedata://text/plain;base64,...allow_url_includeOn单请求即可完成base64串较长URL不够美观实际做题时我一般先试php://filter因为不依赖特殊配置如果发现filter的payload触发了正则就立刻转php://input用curl一次性打完基本都能解决。4.5 拿到的flag怎么确认无论用哪种方法输出里可能会包含一些PHP警告信息例如Warning: system(): Cannot execute a blank command之类不用慌找到形如CTF{...}或flag{...}格式的字符串就是题目要求的flag把它复制到平台的提交框即可。有时候cat输出的内容直接是明文flag有时候输出的是flag.php源码里面能看到$flag ctf{...}之类的赋值语句取引号里的值就行。5. 如果常见姿势全被拦进阶绕过思路盘点题目环境如果做了额外加固比如allow_url_includeOff或者正则过滤升级成不许出现php://和data://那php://input和data://就直接失效。这时候需要换一条更“野”的路。这些思路在房间里不常用但理解了能应对很多同类题目变种。5.1 正则绕过大小写、编码、截断为什么大多无效先试最基础的想法Flag.php用大小写绕过滤正则带了i修饰符无效。fla%67.php靠URL编码绕$_GET已经解码过无效。flag.php%00用空字节截断PHP 5.3.4以后已经修复了空字节截断问题无效。fla\g.php用反斜杠分隔字符正则里\g不是一个匹配单元路径里\g也不是有效分隔符无效。这些“经典绕过”在这道题里基本全军覆没原因很简单正则匹配的是解码后的完整参数值任何试图在文件名上做手脚的写法都会先被正则判死就算正则侥幸放过include也找不到对应路径。5.2 日志投毒把代码写进访问日志再包含当伪协议和远程包含全部失效时还有一招在很多默认配置下依然有效日志投毒。原理是Web服务器会把每次请求的User-Agent、访问路径等记录到日志文件里比如Apache的access.log、Nginx的access.log。如果在User-Agent里写入一段PHP代码日志文件里就多了一行“代码”然后利用include去包含日志文件这段代码就会被当作PHP执行。具体到这道题可以这样操作先用普通请求确认日志路径。常见的路径有/var/log/apache2/access.log、/var/log/nginx/access.log。用php://filter配合resource/var/log/apache2/access.log有时能直接读到日志内容虽然看不到flag但能验证文件存在。用恶意User-Agent发一个请求curl -A ?php system(cat flag.php); ? http://目标地址/index.php?pageindex.php这一步仅仅是把代码写进日志页面正常返回看不到任何输出。再发起一个请求让include去加载日志文件/index.php?page/var/log/apache2/access.logPHP会把日志文件内容当PHP代码执行之前写在User-Agent里的?php system(cat flag.php); ?部分会被执行flag输出到页面上。日志投毒能成立有几个前提日志路径可猜测、日志可读、include能包含路径、代码片段没有被日志格式破坏。很多题环境里日志写得很规整User-Agent里的尖括号会被原样记录所以可行性不错。这类思路属于“伪协议失效时的备胎”理解了原理碰到改了配置的变种题也能多一条活路。5.3 临时文件包含与session文件包含思路大于操作还有另外两条延伸思路。一条是包含上传的临时文件上传文件后PHP会在/tmp/目录生成一个临时文件比如/tmp/phpXXXXXX文件名随机且存在时间极短通常需要结合条件竞争来打复杂度比较高。另一条是包含session文件PHP的session数据默认存储在/tmp/sess_session_id如果能控制session值里的内容也能把代码写进session文件再包含。这两种思路在入门题里用不上但多了解一点遇到进阶题心里就不慌。5.4 这些思路的适用边界日志投毒、临时文件包含、session包含这类手法本质上都是**“找服务器上可利用的落盘文件”**属于文件包含漏洞利用的高阶分支而且日志路径、权限、session文件位置都跟具体环境强相关做题时属于“摸奖式”尝试。普通入门题里正解百分之九十以上还是php://input或data://所以优先把伪协议练熟再去看这些偏门路子。6. 从解题者变防御者本地复现这道题并把它修好刷题不能只图一个flag把环境搬到本地手动复现一遍再想想怎么修收获会比单纯做题大得多。我每次遇到这类经典题都会顺手搭个最小环境既能验证payload又能换个视角理解漏洞成因。6.1 两分钟搭一个复现环境条件允许就装个Docker不想装Docker的用PHP自带开发服务器也能跑。先建一个工作目录写入两个文件index.php漏洞主文件?php show_source(__FILE__); echo flag在flag.php中; $page $_GET[page]; if (!preg_match(/flag/i, $page)) { include $page; } ?flag.php存放预期flag?php $flag ctf{test_flag}; ?然后在同一个目录运行php -S 0.0.0.0:8080浏览器访问http://localhost:8080/index.php就能看到页面。但注意php -S默认allow_url_include状态取决于php.ini本地环境一般默认Off。想让php://input和data://可用需要在启动命令里指定配置php -d allow_url_includeOn -S 0.0.0.0:8080这样环境就和CTF靶场基本一致了。复现完可以试着用curl分别打php://input和data://观察两种方式的效果差异再关掉allow_url_include试试日志投毒这个过程能直观感受到“配置项如何影响漏洞可利用性”。6.2 修复思路这道题该怎么防看明白漏洞成因之后防御其实不难。最粗糙的修法是给page加白名单?php $page $_GET[page]; $allow array(home.php, about.php, contact.php); if (in_array($page, $allow, true)) { include $page; } else { echo 非法参数; } ?白名单后include只接收预设的几个文件名任何伪协议、路径穿越直接失效。这是文件包含最有效的防御姿势。如果业务上确实需要动态包含文件那就至少做这几件事用basename()提取文件名、禁止路径中的目录跳转字符、校验文件后缀必须是.php白名单、设置open_basedir限制include可访问的目录。open_basedir是PHP提供的一个目录约束配置设置了之后文件访问被限制在指定目录内越界路径直接拒绝能有效阻断/var/log/apache2/access.log这类跨目录包含。另外不要把allow_url_include打开。这个配置项默认关闭是有道理的只有极少场景才需要动态包含远程文件正常业务基本用不到。保持关闭php://input和data://在include里就不可用攻击面瞬间少了一大半。6.3 站在出题人视角看这道题的设计回过头来看这道题的考察点分布其实是三层递进的设计第一层考文件包含的基础认识看到代码能识别include $page是漏洞点第二层考preg_match过滤的绕过逻辑让新手意识到“过滤参数值不等于过滤最终效果”第三层考伪协议的实际运用把php://input、data://、php://filter这些工具串起来为后续复杂题打基础。理解了这三层这道题的价值才真正拿到手。它没有堆砌高深知识却把Web安全最核心的“输入进去、输出出来、过滤绕过”闭环完整走了一遍难怪被放在入门位置。最后再说两句实操体会这道题我自己刷了两遍一遍是老老实实用php://input打的一遍是复盘时把环境搬进本地、关了allow_url_include又试了一圈日志投毒。两道流程走下来最大的感受是做题时先小步试探别一上来就堆payload。每次刷Web题我先花十几秒看源码里过滤条件到底过滤了什么、过滤作用于哪个变量然后用一个无害请求验证协议是否可用最后才构造真正的payload。这个习惯帮我少走了很多弯路。再分享一个小技巧用php://input打不到命令时优先检查system是否被禁用。可以在payload里先执行phpinfo()看配置确认哪些执行函数能用再换成passthru、exec、shell_exec或者反引号。这些执行函数在CTF靶场里未必全开但试出来的成功率高很多。希望这篇WP能帮刚开始接触文件包含的朋友少踩几个坑也建议大家亲手把题目环境复现一遍运行起php -S那一刻很多原理不用死记就通了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑