CTFSHOW的Web入门模块有个很讨喜的设计同一个知识点它不是只出一两道题就完而是按难度排成一长串刷完基本等于把这个知识点从头到尾过了一遍。文件包含这个系列就是典型代表从web78一路排到web88中间还夹着两道116、117的加餐。这十几道题看起来每道都只是几行PHP代码但把它们连起来看恰好就是一条从“任意读文件”到“过滤器链构造”的完整学习曲线。如果你刚开始刷CTF Web或者刷到中间某几题卡了很久这篇文章值得看一下。我不按题号给你贴writeup而是把这组题背后的攻击思路、绕过逻辑和坑点拆开讲清楚——看懂这套逻辑后面遇到任何文件包含题都能更快找到入口。1. 先看懂这组题在考什么1.1 从一道最朴素的包含题说起文件包含这类题目的起点往往简单得让人怀疑人生。典型代码长这样?php $file $_GET[file]; include $file; ?就三行。没有过滤、没有白名单、没有后缀拼接。第一次刷到这种题目的人很多人会愣一下就这么简单然后随手试一下?file/etc/passwd页面真的把系统文件内容吐出来了这才反应过来——原来include和file_get_contents完全是两回事。include在PHP里不是简简单单把文件内容读出来而是把文件内容当成PHP代码去解析执行。所以可控的include参数本质上就是一个“代码执行入口”。这也是为什么文件包含会被归类到“代码执行”而不是“任意文件读取”漏洞。后面所有题目不管加了多少层过滤核心都是围绕这个“执行入口”做文章。1.2 题目梯队与难度递进把web78到web88加上116、117放在一起看能明显感觉到出题人的编排思路——它不是让你背payload而是让你在连续刷题中建立起一套“遇到include先想什么”的条件反射。题目区间考察重点典型特征web78 - web79基础伪协议利用无过滤或仅简单拼接后缀web80 - web81关键字过滤绕过禁用部分协议名或关键字web82 - web84可控写入点利用日志包含、Session包含web85 - web88组合限制与综合利用路径限制、多种过滤叠加web116 - web117过滤器链进阶常用协议全部ban掉只能玩filter这个递进关系很像真实的渗透流程先拿到一个入口发现能读文件然后想办法执行代码执行不了就换思路找可控写入点最后在极端限制下还得能把include机制本身玩出花来。刷完这一串文件包含这个知识点基本就吃透了。1.3 刷题前先建立四个问题我在刷完这套题之后回头看发现几乎所有题目都围绕四个问题展开可控点在哪里参数名是什么是不是只有file一个有没有过滤关键字、协议名、符号各自过滤了什么有没有拼接代码里是不是带了.php后缀有没有写入点当前环境里有没有可以往文件里写内容的地方后文讲的每一种利用手法本质上都是这四个问题的变体。有的题考“如何绕过过滤”有的题考“如何制造写入口”有的题考“如何在入口被堵死后另辟蹊径”。做题的时候不要急着试payload先把这四个问题的答案摸清楚很多题的思路直接就浮出来了。2. 文件包含为什么危险伪协议选型与后缀绕过2.1 include和file_get_contents的区别不少人一开始会混淆“读文件”和“包含文件”。打个比方file_get_contents是拿别人的日记本翻看看完了合上内容只是给你展示include不一样它是把人家的日记内容当成你自己的台词当着全班同学的面念出来。如果日记内容里混着别的“台词指令”念的时候就会一起执行。在代码层面include进来看似是一个.txt文件但如果里面有?php ... ?标签PHP就会执行里面的代码如果没有PHP标签就当成普通字符串输出。这个特性直接决定了文件包含漏洞的利用思路一切手段的最终目的都是让include在执行时碰巧遇到一段PHP代码。这也是为什么后面会反复用到伪协议。伪协议是PHP内置的“特殊文件通道”它们不是磁盘上的真实文件而是由PHP运行时动态生成的“虚拟文件”。比如php://filter可以读取文件并同时做格式转换php://input可以读取HTTP请求体data://可以直接把字符串当作文件内容传入。利用这些通道攻击者就能把“include一个参数”变成“执行任意代码”。2.2 PHP伪协议家族一览这一组题里反复出现的伪协议主要就下面这几个建议直接记牢。伪协议用途依赖配置php://filter读源码、构造过滤器链一般默认可用php://input把POST体当作文件内容需要allow_url_includeOndata://把数据流当作文件内容需要allow_url_includeOnfile://读本地文件系统默认可用phar://访问phar压缩包内文件一般默认可用zip://访问zip压缩包内文件一般默认可用php://filter的标准姿势是?filephp://filter/readconvert.base64-encode/resourceindex.php这个请求的意思是把index.php的内容转成base64后再输出。为什么要转base64因为include会把PHP文件解析掉如果直接读源码里的?php就会被当作代码执行你根本看不到内容。先转base64源码就变成一串无害的字符串拿到后用工具解码就能还原出PHP源码。data://的玩法是直接在URL里塞代码?filedata://text/plain,?php phpinfo();?php://input则是用POST请求体带代码跟data://效果类似只是传参方式不同。2.3 include被拼接后缀怎么办这组题里非常容易出现的一个变体是代码写成这样?php $file $_GET[file] . .php; include $file; ?也就是不管你传什么后面都会被强制加上.php。很多人第一次遇到就懵了那我用php://filter读源码resourceindex变成index.php倒还好但如果想读一个不带后缀的flag文件岂不是永远读不到这里有几个思路可以走通用data://或php://input因为它们本质上不是路径拼接的.php会变成数据流的一部分不影响代码执行。读源码时在resource里直接写上已知的PHP文件名比如resourceindex最终变成index.php非常自然。老版本PHP里可以用%00空字节截断把后面的.php吃干净但PHP 5.3.4之后这个姿势已经失效新题基本不给这个口子。所以遇到拼接后缀不用慌优先试前两种。这也是为什么刷题时建议先确认“有没有后缀”——它直接决定了你能走哪条路。3. 从读源码到执行代码实操全流程3.1 信息收集先确认可控点和入口拿到题目先别急着塞payload。我习惯先做这几步打开页面逛逛有没有注释、超链接、按钮很多题把源码藏在注释里。看URL参数。题目提示是文件包含那大概率有一个?file或者类似的参数。随便传一个不存在的路径比如?fileabc观察报错信息。很多环境开着display_errors报错会直接给出绝对路径甚至部分源码。传一个真实存在的文件比如?fileindex.php看页面输出有没有变化。这几步做完题目的基本盘就摸清了。以这一系列最常见的形态为例URL是?filexxx页面只有一行字或者一个功能按钮源码没直接给。这时候第一件事就是用php://filter把源码拖出来。3.2 读源码php://filter的标准姿势构造这样一个请求GET /index.php?filephp://filter/readconvert.base64-encode/resourceindex.php如果页面输出了一长串base64字符恭喜源码到手。解码后你会看到类似这样的东西?php error_reporting(0); if(isset($_GET[file])) { include $_GET[file]; } else { highlight_file(__FILE__); } ?看到include $_GET[file]说明这题就是最基础的伪协议利用。下一步就要想办法执行代码。为什么整个系列里这是最标准的第一步因为你只有看到源码才能确定过滤规则是什么、后缀拼接是什么、有没有其他的坑。盲打payload是撞运气读源码才是解题。如果你用php://filter没有反应可以检查一下路径大小写、URL编码但更常见的原因是题目过滤了php://。中间梯队的题经常干这种事这时候就需要看下面的绕过思路。3.3 执行代码php://input和data://拿到源码确认没有过滤之后执行代码的姿势通常有两种。先看php://input的用法POST /index.php?filephp://input Content-Type: application/x-www-form-urlencoded ?php system(ls);?注意请求体里的代码不需要再进行URL编码但要保证allow_url_include处于开启状态这个选项控制的是php://input和data://能不能被include使用。CTF环境里通常默认开着本地自己搭环境时容易踩这个坑后面会单独说。再看data://的用法GET /index.php?filedata://text/plain,?php system(ls);?因为GET请求里不能有空格和特殊符号实际传入时要进行URL编码GET /index.php?filedata://text/plain,%3C%3Fphp%20system(ls)%3B%3F%3E如果代码里用了单引号也要一起编码。当然也可以用base64来省事?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz8从执行ls到读取flag整个链路就通了。刷到web78、web79的题目时这套流程基本就是标准答案。3.4 让报错信息帮你探路做题时很多人忽略报错信息。实际上在文件包含题里报错信息是最廉价的“服务器探针”。比如传?file/etc/passwd如果页面没反应不要急着放弃可以试试传一个肯定不存在的文件比如?file/nonexistent。如果环境开了报错你大概率会看到类似Warning: include(/nonexistent): failed to open stream: No such file or directory in /var/www/html/index.php on line 5这行报错直接告诉你三件事服务器根目录的真实路径、源码文件名、include在哪一行。后面做日志注入这类操作时路径信息就是必需的。有的题目甚至能在报错里看到过滤规则的提示比如“php://is not allowed”之类的字样这比自己去猜过滤列表高效得多。4. 被过滤与限制逼出来的绕过思路4.1 关键字和协议名被ban之后的绕过从中间梯队开始题目不再让你舒舒服服地用标准payload了。最典型的就是过滤关键字。比如源码里写了?php $file $_GET[file]; if(preg_match(/php|flag/i, $file)) { die(hacker); } include $file; ?这时候你用php://filter直接被拦用resourceflag.php也被拦。怎么办分两步拆解。第一协议名被拦。php://被过滤时可以试试data://它是另一个独立的协议很多过滤规则只盯住php://就会漏掉它。同理如果只是精确匹配小写可能大小写变体就能过但稳妥起见别依赖这个。还可以用双重URL编码有些过滤是取$_GET[file]的原始值有些是先解码再过滤抓到参数传入顺序的差异也能绕。第二文件名字被拦。要读flag.php但flag是敏感词可以利用通配符和路径解析。比如Linux下问号可以匹配单个字符fla?.php就能绕过简单的字符串匹配。还有一层思路是php://filter的resource支持相对路径和嵌套路径比如resourceindex.php/../flag.php虽然看起来绕但实际解析时能指向正确的文件。这里要说一句过滤规则五花八门不要死记每一种绕过而是先判断过滤是“黑名单”还是“白名单”。黑名单意味着只要找到一个漏网姿势就能过白名单则更难比如只允许包含index.php或home.php那思路就得转向“在允许名单里制造可控内容”也就是后面讲的日志注入和Session包含。4.2 日志注入没有上传点时怎么制造可控文件当data://、php://input全都被ban又不能直接上传文件时还有一个非常经典的思路利用服务器自己的日志文件当跳板。Web服务器会把每个请求记到访问日志里请求头、请求行、User-Agent都会被存下来。如果我们把PHP代码写在User-Agent里发出去这段代码就会被“存”进日志文件。之后用文件包含去包含这个日志文件代码就会执行。具体步骤是这样第一步确定日志路径。Nginx常见的路径是/var/log/nginx/access.logApache常见的是/var/log/apache2/access.log。如果不知道路径可以先用php://filter或报错信息摸一下服务器类型和目录结构也可以试试常见的几个路径。第二步构造带PHP代码的请求。以Nginx为例curl -A ?php system(ls);? http://target/?file/var/log/nginx/access.log注意这一步其实包含了两层操作-A指定了User-Agent代码随请求写进日志后半部分的URL又去包含这个日志文件。如果一切顺利页面里会直接打印出ls的结果。这个手法的关键坑有两个。第一个日志文件可能非常大包含的时候PHP解析整个文件响应速度会慢到让人怀疑是不是失败了。解决思路是把带代码的请求尽量放在前面发让日志里的代码片段靠前有时候还需要用Burp之类工具发一个压缩版的代码进去。第二个有些环境对日志目录设置了open_basedirinclude根本进不去这种情况下日志注入就直接pass。4.3 Session包含与临时文件另类可控写入点日志注入的本质是在服务器允许我们写入内容的地方“种”一个包含PHP代码的文件。顺着这个思路还有两个变体。一个是Session文件包含。PHP默认会把Session数据序列化后存成文件路径通常在/tmp/sess_加上会话ID。如果应用里存在能把用户输入写进Session的逻辑比如把用户名存到$_SESSION[username]那我们就可以往Session文件里塞PHP代码。实操流程大概是先用浏览器记住一个PHPSESSID访问携带?name?php system(ls);?的入口PHP会把输入写进Session文件然后再访问?file/tmp/sess_你的PHPSESSID代码就可能被include执行。注意Session文件里除了我们的代码还有其他序列化前缀但PHP语法上普通字符串先输出、?php ... ?再执行一般不影响。另一个是临时文件包含。PHP接收上传文件时会先落盘到临时目录文件名形如/tmp/phpXXXXXX用完之后自动删除。这个文件名是随机生成的看起来没法预测。但如果目标环境里有phpinfo()页面页面上会直接打印出$_FILES[file][tmp_name]的完整值。经典的进阶操作就是先构造一个带文件上传的请求同时用脚本快速去读phpinfo页面拿到临时文件名再立刻用文件包含包含它。这个手法在网上有成熟脚本属于“临时文件生命周期”的攻防博弈真实渗透里也非常实用。5. 进阶内容过滤器链与116/117的思路5.1 什么时候必须上过滤器链刷到web116、web117的时候难度会明显上一个大台阶。前面的题好歹还留了data://、php://input、日志这些口子这两道题把常规的“执行入口”基本封死了。你会看到过滤规则里躺着php://、data://、http://、ftp://等一堆黑名单甚至可能还限制了只能包含某个固定目录下的文件。这时候能用的只剩php://filter。但如果只是用php://filter读文件那只能读不能执行flag还是拿不到。于是题目考察的东西就变成了能不能用php://filter本身构造出一条“代码执行链”答案是可以而且这正是文件包含里最硬核的部分——过滤器链。5.2 过滤器链到底在干什么php://filter的过滤器是可以叠加的。标准读源码是套一层convert.base64-encode但过滤器列表其实是管道式的可以写成这样php://filter/read过滤器1|过滤器2|过滤器3/resource目标文件被包含的文件内容会依次经过所有过滤器的处理最后才进入include的解析流程。过滤器链的原理简单说就是利用多个过滤器对文件内容做一连串编码转换最终让“任意输入”经过转换后变成攻击者想要的PHP代码。听起来很玄我打个比方你把一句话丢进一个“压缩→编码→反转→再编码”的机器链条机器最后吐出来的已经不是原来的话而是一串看似乱码但实际包含特定意义的字符串。在这个场景里这串字符串经过convert.base64-decode解码后刚好是一句合法的PHP代码。回看过滤器链的经典套路核心步骤通常是先用一批convert.iconv.*过滤器对文件内容做字符集转换反复折腾让内容变成一串特定的base64字符最后用convert.base64-decode把它还原成PHP字节码。里面的iconv过滤器是把文本从一种字符集转到另一种字符集不同的转换组合会有不同的变换效果把这套变换玩明白了才能造出可控的输出。5.3 链路构造为什么最终要依赖工具听到这里你应该能感觉到手算过滤器链几乎不现实。一个能用的过滤器链payload动辄几百上千字符中间经过了十几次甚至几十次转换人工去枚举组合显然是灾难。所以社区里早就有自动化工具了。思路非常直接在本地起一个PHP环境把目标文件的内容当作种子输入然后随机组合一批过滤器挨个试看哪一组过滤器处理完的结果经过base64-decode之后能变成目标PHP代码。只要能跑通就把这组过滤器序列记录下来生成最终的payload。这套流程用伪代码描述大概是输入种子文件内容、目标PHP代码 循环 随机选择一组过滤器组合 对种子内容应用过滤器得到中间结果 如果中间结果经base64-decode后包含目标PHP代码则记录该组合并输出实际用的时候把这个工具生成的payload直接填到?filephp://filter/...里配合一个已知可读的源码文件作为种子就能在完全禁止其他伪协议的情况下执行代码。这种题刷起来真正的难点反而不是理解每一条过滤器的数学变换而是你能不能在拿到题之后快速判断应该走过滤器链还是找可控写入点还是赌某个过滤规则有漏网之鱼。这依赖对过滤规则的通盘评估做题时建议把被禁的关键词和协议列个清单一目了然。5.4 实战判断先摸清过滤规则再选路线我刷116、117这一类题时临场判断的习惯是先做一张清单允许哪些协议php://filter还在不在data://、php://input是否被禁允许哪些路径只能包含当前目录还是任意路径都行文件后缀是否拼接include后面带不带.php是否存在额外要求是让读源码还是让执行命令还是要求写文件把这些列完再决定走哪条路线。如果只是禁了部分关键字但php://filter还在优先想过滤器链如果存在可控写入点比如Session、日志先试写入点它们的payload比过滤器链简单得多如果php://filter也被ban了那就得回到伪协议和路径解析的缝里去抠。很多人在116、117这种题上卡住不是因为不懂过滤器链而是因为没把“可用条件”和“目标”对应起来。先做减法再动手比上来就塞一个大payload靠谱得多。6. 避坑速查常见报错与排查记录6.1 常见报错现象与排查方向速查现象可能原因排查方向包含后页面空白目标文件不存在或没有权限换个路径开报错再看报failed to open stream路径错误或拼接后缀导致文件读不到确认路径用/etc/passwd试探php://filter没反应过滤规则ban掉了php://换data://或其它伪协议data://没反应allow_url_include关闭本地环境用php -d allow_url_include1include日志文件超时日志太大解析太慢把代码写在更靠前的请求里精简代码包含Session文件不执行路径不对或Session内容被编码破坏查session.save_path确认写入格式flag文件找不到文件名不是flag.php先读源码看文件名或用通配符猜过滤器链报语法错误payload里的特殊字符被URL解码吞掉对payload整体编码后再传这个表基本涵盖了我刷这组题遇到的大部分“卡住”瞬间。多数问题不是思路错而是环境细节没对上。6.2 我踩过的几个真坑第一个坑是本地测试data://完全不生效。后来发现是PHP默认的allow_url_include是 Off不是题目环境特殊而是我自己没开。用内置服务器时记得加参数php -d allow_url_include1 -S 127.0.0.1:8000第二个坑是日志注入时我在User-Agent里写了带双引号的PHP代码结果日志里代码被服务器转义了include之后吐出来的是一堆html实体。后来习惯用不含引号的payload或者用单引号并且外层套双引号时注意转义规则。第三个坑是过滤器链payload。生成器给的payload里有大量|和特殊符号直接放在URL里经常被解析出错后来统一养成习惯先把payload用URL编码工具整体编码再放进参数里避免中间某个空格或者符号被服务端当成参数分隔符。第四个坑也是最隐蔽的——有些题目的过滤逻辑是先trim再匹配有些是先匹配再trim。同样是php://filter在一种环境里能过在另一种环境里就被拦原因就是处理顺序不同。遇到“明明没过滤怎么就过不去”的情况可以试着在payload前面加空格、换行、注释符说不定就绕过去了。6.3 本地复现一个能跑起来的最小环境刷完这套题我建议本地搭一个最小环境亲手验证每一种姿势。代码很简单?php if (isset($_GET[file])) { include $_GET[file]; } else { highlight_file(__FILE__); } ?再放一个flag.txt内容随便写点东西。启动命令php -d allow_url_include1 -S 127.0.0.1:8000然后你就可以在浏览器里验证php://filter读源码、data://执行代码、php://input配POST工具执行、日志包含、甚至用现成工具跑过滤器链。所有姿势先在本地跑通再回题目里去用效率会高很多也避免在题目环境里反复试错浪费时间。我个人刷完这一整个系列的体会是文件包含题其实很“讲道理”。它不像有些漏洞点靠天马行空的想象力它的每一步都有明确的逻辑链条——可控点、过滤规则、拼接方式、写入点四要素列清楚解法基本就锁定了。web78到web88这套题的价值在于它逼着你把这套思维方式练成本能。以后在代码审计或实战里看到include $_GET[xxx];第一反应不再是“我要用哪个payload”而是“先看过滤、再看后缀、再找写入点”。这套条件反射比背下来一百个payload都值钱。最后再分享一个小技巧刷题卡住的时候不要盯着payload硬想回去读一下源码把过滤规则一条条列出来。很多时候你会发现在列清单的过程中那条被忽略的绕过路径自己就跳出来了。文件包含题尤其如此——它考的不是工具多而是对PHP文件包含机制本身的理解有多细。