
如果你打过CTF新生赛大概率遇到过这样一道让人又爱又恨的题目页面看起来很干净就一个链接点过去URL变成了类似/?fileflag.php但页面却什么都没有。别急着怀疑自己这就是“[ACTF2020 新生赛]Include 1”这道题最典型的样子。作为ACTF 2020新生赛的Web入门题它把PHP文件包含漏洞藏在一个非常不起眼的参数里考察的知识点很单一就是include函数的滥用和php://filter伪协议的使用。这篇文章我会从信息收集开始把这道题的完整解法、背后的原理、以及做同类题时容易踩的坑全部过一遍适合刚接触CTF、正在为新生赛题库发愁的朋友。1. 拿到题目后的第一步不要急着点链接1.1 页面和URL里藏着什么信息打开题目靶场常见的ACTF新生赛Web题界面非常简洁几乎没有任何多余按钮只有一个类似“Click Me”的链接或者一句提示语。我当时的反应是先点一下链接地址栏立刻从题目根路径变成了类似下面的格式http://此处是靶场地址/?fileflag.php页面内容依然很简单甚至可能没有任何变化。这一步其实已经暴露了大量信息一个名为file的GET参数被传给了后端并且这个参数的值是flag.php说明后端正在用某种方式读取或加载这个文件。URL参数是Web应用接收外部输入的常规渠道后端脚本拿到参数后往往直接参与业务逻辑所以看到file、page、path这类具有“包含/读取”语义的参数名就要立刻想到文件操作相关功能。很多新手看到页面空白会觉得题目出错了其实恰恰相反这是一个非常典型的设计真正的关键信息不在页面上而在URL和代码逻辑里。我在做这类题时第一步永远是观察URL结构把参数名和参数值拆出来然后思考“如果我把它换成其他值会怎样”。这种主动改参数的习惯在后面的利用环节会非常有用。1.2 识别“文件包含”的经典特征这里需要引入一个经典概念文件包含File Inclusion。在PHP中include、include_once、require、require_once这些函数可以引入另一个文件并把文件内容当作PHP代码执行。如果开发者在写代码时让外部输入直接拼接进include函数就构成了文件包含漏洞。我习惯用一个拼盘比喻来理解它include就像把指定的水果直接放进盘子PHP解释器会把这个水果当成“可执行内容”来处理。如果水果本身只是普通数据它会原样显示如果水果内部带了一段PHP逻辑PHP就会把这段逻辑运行一遍。刚才的fileflag.php就相当于让include去加载flag.php文件但这个文件内部很可能只定义了一个变量或注释掉了flag运行之后不会向页面输出任何内容所以页面没有变化。这道题的名字叫“Include 1”其实已经把考点写在标题里了。看到include、加载、模板、翻译这一类词基本可以锁定“文件包含”方向。与其对着空白页面发呆不如立刻去想如何把include的文件内容“弄出来”看了一眼这就引出了后面最核心的工具——php://filter协议。1.3 信息收集的正确姿势在CTF里拿到一道Web题之后不急着打先做信息收集是基本素养。对于这道题建议按以下顺序检查第一查看网页源代码看看有没有注释或隐藏链接第二在开发者工具的Network面板里刷新页面看请求和响应头确认是否存在跳转第三尝试手动修改file参数为index.php、config.php、flag.php等常见文件名第四如果修改后页面有变化就说明包含逻辑确实生效。我见过很多人在这个环节直接卡住原因是只看浏览器渲染后的页面忽略了地址栏和网络请求。实际上这道题的信息收集工作非常简单唯一需要记住的要点是不要把目光只停留在“看到什么”而是去想“后端到底拿这个参数做了什么”。HTML页面、注释、响应头、URL参数这些都是可以被我们利用的信息入口。等这一步做完你就会发现下一步的目标非常明确让后端把我们想看的文件内容以源码形式吐出来。2. 文件包含漏洞的核心原理include到底做了什么2.1 include/require的执行逻辑为什么直接访问flag.php看不到flag这得从include的执行机制说起。include和require是PHP中非常常用的文件引入函数它们会把指定文件的内容加载进来并当作PHP代码执行。用一个最简单的易受攻击代码来说明?php $file $_GET[file]; include($file); ?如果这段代码运行在一个Web环境里攻击者就可以通过/?fileflag.php让include去加载flag.php。假设flag.php里面的内容是?php $flag flag{actf_include_1_fake_flag}; // 真正的flag也许就在这但不输出 ?include会把这段代码当作PHP执行。它做了一件事定义了一个变量$flag然后没有echo没有print。所以页面不会出现任何内容。这就是为什么我们直接点链接或者访问?fileflag.php时页面常常是空白的因为flag被“执行”了而不是被“显示”了。如果flag.php里写的是echo hello;那页面就会显示hello。可真正的flag通常不会那么傻它会藏在变量、注释、或者另一个文件里所以我们必须换一种思路不要求include执行它而是要求include把文件的原始内容读出来给我们看。2.2 为什么需要php://filter来读源码在PHP中有一些以php://开头的伪协议专门用来处理输入输出流。其中php://filter是一个非常实用的工具它可以在读取文件内容时对数据流进行过滤最常用的操作就是convert.base64-encode。我们构造这样的参数filephp://filter/readconvert.base64-encode/resourceflag.php它的含义是让include去读取flag.php这个文件但不直接当作PHP代码执行而是先把文件内容通过base64编码后再输出。于是页面上会返回一段看起来乱七八糟的字符串比如PD9waHAKJGZsYWcgPSAiZmxhZ3thY3RmX2luY2x1ZGVfMV9mYWtlX2ZsYWd9IjsKPz4K这段字符串再经过base64解码就能还原出flag.php的源代码。为什么非要绕一圈base64因为如果直接读取源码并原样输出PHP解释器会在读取到?php时认为这是PHP代码的开始从而再次执行它我们还是看不到源码。base64编码的作用相当于把源码“翻译”成一种不会被PHP执行的字符串等我们拿到手再“翻译”回来。2.3 文件包含漏洞的常见危险写法这类漏洞之所以在CTF中频繁出现本质上是开发者过度信任用户输入。除了刚才那种直接把参数传给include的写法在实际业务里还有很多类似的形态比如做主题切换功能时手动拼接模板路径$page $_GET[page]; include(./templates/ . $page . .php);这种代码虽然加了目录前缀和文件后缀但仍然可以通过伪协议、路径穿越、特殊编码等方式绕过。更危险的是如果PHP配置了allow_url_includeOn攻击者甚至可以直接包含一个远程URL执行远程服务器上的代码这就是远程文件包含RFI。而在[ACTF2020 新生赛]Include 1这道题里我们只需要用到本地文件包含加php://filter读源码难度非常适合新生入门。3. 完整复现[ACTF2020 新生赛]Include 1 拿flag全流程3.1 环境准备与第一步探测在做题之前先确认你有一个合法的CTF靶场环境。大多数CTF比赛平台都提供在线复现进入题目页面后直接运行即可不需要本地部署。如果你拿到的是题目源码也可以在本地用PHPStudy、Docker或XAMPP搭建一个简易环境把index.php和flag.php放到根目录就能测试。这道题只需要最基本的PHP环境甚至不需要数据库。拿到题目后我的做法是先用浏览器访问首页记录下初始URL和页面内容然后点击页面上唯一能点的链接。点击之后URL变成了/?fileflag.php页面没有明显输出。紧接着我会尝试几个常规的探测参数例如把file改成index.php、flag、phpinfo.php等观察响应差异。如果改成index.php后页面变得和首页很相似说明include确实在加载并执行文件。如果改成不存在的文件名出现报错那更好了报错信息会直接告诉我们脚本路径和文件拼接方式是极好的信息泄露。这里有一个容易忽略的点用浏览器直接访问不同参数时一定要观察返回的HTML源代码而不是只看可视化页面。有时候后端在包含文件时已经把额外的调试信息写进了注释里这个信息只有查看源代码才能看见。信息收集越细致后面利用起来就越省力。3.2 构造php://filter读取flag.php源码经过第一步探测我们确信题目存在文件包含点接下来直接使用read协议读取flag.php。完整的请求URL如下http://题目地址/?filephp://filter/readconvert.base64-encode/resourceflag.php这里我建议先不要用浏览器地址栏直接用curl请求因为浏览器可能会对URL中的部分字符做特殊处理。命令是curl http://题目地址/?filephp://filter/readconvert.base64-encode/resourceflag.php如果环境正常响应体就是一段base64编码的字符串。这是我用示例环境得到的返回内容PD9waHAKJGZsYWcgPSAiZmxhZ3thY3RmX2luY2x1ZGVfMV9mYWtlX2ZsYWd9IjsKPz4K拿到这段字符串后在终端里执行echo PD9waHAKJGZsYWcgPSAiZmxhZ3thY3RmX2luY2x1ZGVfMV9mYWtlX2ZsYWd9IjsKPz4K | base64 -d解码后的内容就是flag.php的完整源码?php $flag flag{actf_include_1_fake_flag}; ?看到这里题目就解决了。把花括号里的内容复制到CTF平台的提交框里完成。整个过程看起来只有一步但这一步背后包含了对include执行机制和伪协议过滤机制的理解没有这些积累拿到payload也只是照葫芦画瓢。3.3 如果flag.php读不到就读index.php有时候不同题目的flag存放方式不太一样有的直接把flag藏在index.php有的在flag.php中做了混淆。如果读flag.php返回的base64解码后没有flag不要慌先去看index.php的源码确认包含逻辑。请求方式完全相同curl http://题目地址/?filephp://filter/readconvert.base64-encode/resourceindex.php得到的index.php源码大概率是这样的?php error_reporting(0); if ($_GET[file]) { include($_GET[file]); } else { show_source(__FILE__); } ?从这个源码可以看出代码里没有对file参数做任何过滤和验证直接include。所以无论flag放在哪个文件里我们都可以用filter协议把它以明文的方式读出来。关键不是死记一个文件名字而是先读源码再从源码里找到flag的藏身位置。3.4 用Burp Suite或curl提高效率在实际做题时浏览器地址栏最大的问题是显示长URL和特殊字符容易出错尤其是base64编码输出中可能出现加号在URL里会被解析成空格导致解码失败。所以我在拿到这种题目后基本都是先用curl发请求再用管道直接解码curl -s http://题目地址/?filephp://filter/readconvert.base64-encode/resourceflag.php | base64 -d如果响应内容里混入了HTML标签可以先保存到文件再处理curl -s http://题目地址/?filephp://filter/readconvert.base64-encode/resourceflag.php -o flag.txt cat flag.txt用Burp Suite就更直观了把请求发送到Repeater修改file参数发送后在响应包裹里选中base64串用Inspector或Decoder直接解码效率很高。工具不是重点重点是理解原理后端include了什么我们就能通过filter看到什么。4. 同类题与扩展从Include 1走到更远4.1 PHP流协议家族一览php://filter并不是唯一的伪协议理解整个PHP流协议家族对后续打文件包含类题目非常重要。我这里把经常出现的几个协议整理在一个表格里方便对照协议作用是否受allow_url_include影响php://filter读取文件源码并做过滤处理否php://input把POST请求体当作输入流是data://把字符串当作数据流是expect://直接执行系统命令需要expect扩展phar://读取Phar归档文件否在Include 1这道题里核心就是php://filter不需要考虑allow_url_include。但在后续的题目中如果直接包含了filter但仍然无法读取可以试一试php://input或者data://前提是目标环境开启了allow_url_include这个配置可以在phpinfo页面中查看。4.2 文件包含的常见进阶利用文件包含漏洞的利用方式远不止读源码这一种。如果在目标环境中allow_url_includeOn可以构造data://协议直接执行代码filedata://text/plain,?php phpinfo(); ?也可以把payload提交到代理访问日志或临时上传的文件中再通过包含日志或临时文件来执行代码这是本地文件包含拿Shell的经典手法。还有phar反序列化、包含图片马等姿势都是一环扣一环的组合利用。对于刚接触CTF的新生来说先从读源码开始逐步理解“包含一个会自动执行的代码文件”和“包含一个能展示源码的过滤器”之间的区别比盲目追求高大上的利用链要重要得多。4.3 防御思路如何避免自己的站点踩坑做安全题目最终目的是帮助自己写出更安全的代码。这一类漏洞的防御其实非常清晰不要相信用户输入的文件路径。最简单的做法是维护一个白名单比如?php $allow_pages array(home home.php, about about.php); $page $_GET[page]; if (isset($allow_pages[$page])) { include($allow_pages[$page]); } else { include($allow_pages[home]); } ?这样用户只能传入home或about这样的索引不能直接控制路径和协议。如果无法避免动态引入至少要做严格的过滤禁止php://、file://、../等特殊字符。另外生产环境里尽量关闭allow_url_include同时把不需要的PHP危险函数加入disable_functions列表。防御看起来很简单但很多真实漏洞恰恰是因为少了一个校验而导致的。5. 避坑指南我在做这道题时踩过的坑5.1 为什么filter输入后页面一片空白我第一次做这道题的时候照着别人的提示输入php://filter协议结果页面上什么都没有一度怀疑题目或环境坏了。后来排查发现问题出在浏览器对URL中冒号和斜杠的解析上部分浏览器会把//当成协议分隔符。更常见的坑是目标代码可能拼接了后缀导致实际读取的是flag.php.php。遇到这种情况可以尝试把resource参数里的文件后缀去掉或者使用Burp Suite直接修改请求。如果环境允许也有老版本的%00截断技巧不过现在的CTF环境基本都更新到PHP 7以上不建议依赖这个旧特性。5.2 如何判断伪协议是否可用判断php://filter是否可用的方法比较简单请求?filephp://filter/resourceindex.php如果页面变化了说明filter协议通路没问题再用带convert.base64-encode的完整filter读取如果页面出现base64字符串说明可以正常读源码。如果返回空白或报错考虑是不是disable_functions或open_basedir限制。CTF环境的限制通常较少但本地测试时一定要开启相关扩展。5.3 base64解码后乱码或带杂质怎么办使用浏览器直接访问filter协议时返回的响应体里可能包含前端HTML模板代码导致base64串夹杂其他内容直接复制到在线解码工具会失败。我的处理方法是把响应保存到本地文件再用命令提取纯base64部分。比方说curl -s http://题目地址/?filephp://filter/readconvert.base64-encode/resourceflag.php -o resp.txt grep -o [A-Za-z0-9/]\{30,\} resp.txt | head -1 | base64 -d这个命令可以把响应中较长的连续base64字符提取出来并解码非常实用。如果输出里有多段base64就多试几个总有一段是我们要的源码。5.4 平台显示flag不正确拿到flag后提交多次提示错误这种时候先检查是不是复制多了引号、空格或者分号。很多题目的flag长成flag{xxx}我们只需要提交完整的花括号内容不要带PHP中用于字符串定义的引号。另外要注意flag可能是动态生成的每次打开题目环境都可能不同所以一定要从当前靶机的源码中取flag而不是网上随便找一个Writeup里的内容硬贴。6. 从这道题延伸出的个人复盘6.1 我在这道题里学到的三个关键点这道题虽然简单但对新手的训练价值很高。第一动态参数是Web题的核心线索看到file参数的第一反应不是点链接而是想“它会被拿去干什么”第二php://filter是文件包含类题目中最基础的读源码手段必须熟练掌握写法理解base64编码存在的意义第三遇到页面空白不要慌先读源码再判断很多flag就是藏在源码里等着被读出来的。6.2 接下来怎么练做完Include 1建议接着做Include 2或者找几道类似的LFI题目巩固。也可以自己动手写一个带include漏洞的PHP页面放到本地环境里反复练习filter、data和php://input几种协议。我在实际测试中发现本地环境用Docker起一个PHP容器是最顺手的可以随时改配置、看报错、开debug对理解漏洞原理帮助非常大。如果你已经有PHP环境直接扔两个文件进去也能练核心代码不超过十行非常适合当作实验田。我个人更喜欢在这种简单的LFI题上多花一点时间因为很多复杂的RCE链都是从“一个不受信任的include参数”延伸出去的。能把Include 1的每一步都解释清楚后面的路会顺畅很多。