本地文件包含(LFI)在CTF Web方向属于最典型的送分题,同时也是实战里最容易被人忽略的突破口。BUU LFI Course 1 1这道题,几乎是把LFI最标准的利用方式——php://filter读文件——摆在台面上了。我见过不少新手在这个关卡卡住,不是不知道LFI是什么,而是拿到一个参数后不知道下一步该干什么。这篇文章就沿着我实际做这道题的流程走一遍:从参数探测、源码恢复,到用伪协议读flag,再到后面几个容易踩的坑,一次性讲清楚。
1. 这道题到底在考什么:先从include函数说起
1.1 文件包含的本质
LFI全称Local File Include,翻译过来是本地文件包含。要理解它,先得知道PHP里include、require、include_once这类函数是干什么的。它们的作用就是把一个文件的内容引入当前脚本,然后当作PHP代码执行。正常开发中,开发者经常用这个机制做模板加载、公共函数引入,比如include "header.php",这样做本身没什么问题。
问题出在当文件路径来自用户输入的时候。如果后端代码写成include $_GET['file'],用户传什么,服务端就include什么。这里的"用户"不仅仅是普通访客,还可能是一个带着恶意目的的测试者。用一个生活化的类比:你让前台小哥去柜子里拿一份标着"A"的合同,结果小哥问都不问就把"柜子里任意一份文件"递给了你。更危险的是,PHP的include不是简单地把文件内容打印出来,它会先把内容当作PHP代码执行。这意味着如果被包含的文件是服务器上的敏感文件,你有可能看到内容;如果被包含的文件可以被你控制,那甚至能直接执行代码。
在BUU LFI Course 1 1这个场景里,核心考点就是"参数可控的include函数"。它基本会有一个PHP页面,接收一个file类型的参数,然后用include去加载。能不能把任意文件带出来,取决于这个参数有没有过滤、过滤得严不严。入门版本一般只有最基础的过滤甚至没有过滤,所以解法相对固定。
1.2 为什么"能读文件"就是突破口
很多人刚到这道题时会觉得奇怪:我明明能访问这个页面,为什么还要花心思去"读文件"?这是因为Web服务器和底层文件系统之间存在一层隔离。你看到的是经过PHP渲染后的HTML,服务器上有什么文件、每个文件里写了什么,默认情况下你完全看不到。而LFI恰好打破了这个隔离——它让脚本去读取并输出服务器自身的文件内容。
放在CTF题目背景下,flag通常不会明文写在网页上,而是放在某个PHP文件里,或者放在服务器某个路径下,比如/flag、/var/www/html/flag.php这种位置。既然服务器本身能读取这些文件,那么只要你找到让服务器帮你打开它们的方式,flag自然就出来了。读懂文件包含漏洞,相当于拿到了一个"服务器文件系统浏览器的钥匙"。
这里还要纠正一个常见误解:很多人以为只有include直接执行危险,其实readfile、file_get_contents、fopen这类读取函数在参数可控时同样会造成文件读取,只是它们默认不会执行PHP代码。LFI的判断标准很简单——"有没有一个参数,间接决定了服务器去读写哪个文件"。弄明白这一点,再回头做BUU这道题,思路会清晰很多。
2. 拿到题目后的第一轮探测:参数、回显与代码还原
2.1 题目入口与参数猜测
BUU LFI Course 1 1的页面打开后,通常是一个非常简洁的界面,有的版本带一个输入框,有的版本直接就是一个跳转链接,但核心都落在URL的file参数上。如果页面完全没有提示,你可以在地址栏手动加参数试试:?file=index.php。加了之后如果页面出现源码高亮或者多了一些内容,那就说明参数生效了。
这类题目最常使用的参数名有file、page、filename、path、include、template等,它们都是开发者习惯性用来传文件名的字段名。拿到题目后不要急着上工具,先把这些常见参数都手动测一遍,观察页面变化。这一步属于信息收集阶段,做得越细,后面判断越准。我见过有人一上来就打几个大payload,结果参数名根本没对上,浪费了时间。
参数探测时,可以用浏览器地址栏直接改URL,也可以用Burp Suite的Repeater反复发包。我更推荐后者,因为Burp可以保留每次请求的历史记录,方便对比不同参数下的回显差异。对新手来说,只要能坚持"每次只改变一个变量,其他都保持不变",浏览器手测也完全够用。
2.2 回显特征反推后端逻辑
传不同值看回显,本质是在做黑盒探测。你不需要看到源码,就能通过现象猜出后端大概长什么样。这里有几个非常典型的回显特征:
- 传入
?file=index.php后页面出现了带语法高亮的PHP源码,说明后端可能直接highlight_file($_GET['file']),但这种情况很少作为LFI题出现。 - 传入不存在的文件名,比如
?file=nonexist.php,页面报Warning: include(nonexist.php)一类的错误,这等于直接暴露了include函数的存在,是非常明确的漏洞信号。 - 传入
?file=/etc/passwd,页面把这台容器里用户信息文件的内容原样打出来了,那说明文件内容被当作普通文本输出,漏洞可以打通。 - 传入一个PHP文件时,页面却空白或只显示部分HTML内容,说明PHP文件被include后当作代码执行了,源码没有原样输出——这种情况恰好是LFI最典型的表现。
把这些现象综合起来,你基本可以断定:后端代码肯定存在一个include或require,并且参数没有做严格的路径限制。这个结论就是后续利用的基础。如果页面没有任何报错也不输出内容,也别急,穷举几个关键路径再观察。
2.3 正常情况下后端代码会长什么样
结合BUU Course 1这道题的难度,最可能的后端代码就几行:
<?php if (isset($_GET['file'])) { include $_GET['file']; } else { highlight_file(__FILE__); }这段代码的逻辑很简单:只要有file参数,就把参数值交给include去处理;没有参数,就把当前文件高亮显示。看起来人畜无害,恰恰是这种直白的题目最适合教学。真实环境中很多CMS的模板加载、主题切换功能,本质上就是这段代码的复杂包装。
这里还想多说一句:黑盒测试时,拿到现象直接打payload是没错,但如果题目稍微加了过滤,你就必须靠后面的源码确认过滤规则。所以"先探测参数-再读源码-再决定利用方式"这条链路,越早养成越好。BUU LFI Course 1 1这道题的设置就是为了让你在这个流程上走通一遍。
3. 用php://filter读出源码:静态审计的关键一步
3.1 为什么先读源码而不是直接读flag
做Web题有个原则:先拿到源码再谈其他。虽然LFI的直接目标可能是flag,但在很多变种题目里,flag路径是被隐藏的、文件名是随机的、过滤规则是未知的,直接盲打效率太低。通过读取后端代码,你能确认四件事:用的是include还是file_get_contents、有没有过滤函数、过滤了哪些关键词、flag文件大概在哪个目录。
放到这道题上,直接读flag可能也能成,但如果你把index.php先读出来,解码看到源码,就掌握了全部信息。这个习惯在BUUCTF后续LFI题目中特别有价值,因为后面的题往往会在源码里埋坑、做黑名单过滤,或者要求组合利用。源码就是地图,没有地图就去闯迷宫,运气好能过,运气差就卡死。
3.2 完整的Payload构造过程
读源码要用到PHP流包装器,也就是常说的伪协议。PHP提供了一个php://filter协议,可以在读取文件内容时对数据做一层处理。最常用的组合是convert.base64-encode,原因是它对内容做了Base64编码,原文中的PHP标签、特殊字符全部变成了字母、数字和少量符号,既不会被执行,也不会因为特殊字符导致输出截断。
构造payload时,直接这样写:
GET /index.php?file=php://filter/read=convert.base64-encode/resource=index.php HTTP/1.1 Host: target服务端收到后,PHP会把index.php的内容读出来,经过convert.base64-encode过滤器编码,再输出到页面。你看到的是一串形如PD9waHAg...的Base64文本。这里有个细节要留意:resource=后面跟的是路径,这个路径是相对于当前工作目录的。如果当前脚本是index.php,resource=index.php读取的就是当前目录下的文件;如果脚本在子目录,可能要写相对路径或者绝对路径。
有时候在BUU题目环境里,当前工作目录不一定等于脚本所在目录,所以如果相对路径没读出来,可以尝试绝对路径。常见的Web目录有/var/www/html/index.php、/app/index.php、/var/www/index.php这几类,但具体哪个得靠试。从我个人经验看,BUUCFF的LFI系列里/var/www/html/index.php出现频率最高。
3.3 读到Base64之后如何处理
拿到Base64文本后,解码方式有很多。如果你用的是浏览器,可以复制到CyberChef或者任意在线解码工具;如果在命令行,最简单的操作是:
echo 'PD9waHAg...' | base64 -d也可以写成文件再解码,避免终端自动折行影响结果:
echo 'PD9waHAg...' > b64.txt base64 -d b64.txt > index.php cat index.php解码后看到的代码,基本就是开头那段include结构。如果代码里有str_replace、preg_match、in_array这类函数,那就说明题目加了过滤规则,下一步就要围绕这些规则设计绕过。如果解码出来啥都没有,先把响应完整保存下来再看看,因为有些环境会在Base64前拼接HTML头,导致你复制的时候丢掉一半内容。
4. 从源码里找flag:两种读取路径的取舍
4.1 直接include可能踩到执行问题
新手最容易犯的一个错误是:知道?file=flag.php可以访问flag文件,就直接这样发了,结果页面一片空白。原因很简单——flag.php里面写的是<?php flag{...}; ?>之类的PHP代码,php://filter协议在读取文件时会对内容做Base64编码,然后原样输出,编码后的数据里没有PHP标签,所以不会执行,也不会被吞掉。这种方式既能拿到源码,又能避免执行陷阱。
还有一种情况是flag直接存成/flag或者flag.txt这类纯文本文件。这种时候用include包含它,因为它里面没有PHP标记,就会被当作普通文本输出,直接?file=/flag或?file=../flag就能看到内容。所以不是"不能直接include",而是要分情况判断:目标文件是纯文本,直接include就能读;目标文件是PHP文件,则应改用Filter方式读取。
在BUU LFI Course 1 1这道题的正常流程里,解码源码后你会看到flag文件的名字,接下来只需要把resource=后面的路径从index.php换成flag路径就行了。比如:
GET /index.php?file=php://filter/read=convert.base64-encode/resource=flag.php HTTP/1.1再把输出解码,flag就在那里。
4.2 用Filter读指定资源的安全姿势
如果flag文件名不确定,可以先尝试读取当前目录的文件列表。但这个功能靠PHP的include函数本身做不到,毕竟include只能打开单个文件,不能列目录。这时候就要靠猜名字或者读.index.php的备份、flag.php、flag.txt、f1ag.php这些常见命名。
BUU这道题给的提示就是"Course 1",难度定位在入门,flag文件名大概率就叫flag.php或者flag。如果文件名是纯flag(没有扩展名),那么payload就是:
GET /index.php?file=php://filter/read=convert.base64-encode/resource=flag HTTP/1.1这里有个非常实用的小技巧:在resource=后面可以同时使用多个过滤器,用管道符|隔开。比如你想把Base64再解回原文,可以用convert.base64-decode,但这个操作在只需读flag时一般用不到。真正用得上的是对过滤规则敏感的场景:如果题目把php://、filter、base64这些关键词挡掉了,你就得换filter的等价写法,比如去掉read=写成php://filter/convert.base64-encode/resource=flag.php,或者换大小写变体、换编码方式。
4.3 路径差异与目录穿越的基本功
读文件时绕不开路径问题。Linux环境下,常见目录结构差异很大,BUUCTF的题目多数跑在Docker容器里,容器内的根目录和宿主机根目录不一样。同一个/flag路径,在Docker容器里可能真的存在,但在某些题目里flag会被放在Web目录下的./flag.php或./../flag。
如果不确定路径,可以先用filter读/etc/passwd验证文件包含的有效性,再读/proc/self/cmdline看启动命令,甚至读/proc/self/environ看环境变量——不过这些属于进阶操作,Course 1一般用不到。我在这道上更推荐的做法是:先用相对路径读当前目录文件,因为BUU平台很多题目就把flag放在Web根目录下,flag.php直接就躺在index.php旁边,没必要绕。
目录穿越也很值得顺带记住。假如题目过滤了路径但我们想让include去读更上层的文件,可以用../往回跳。注意在URL里直接写../有时会被浏览器规范处理,稳妥做法是URL编码成..%2f,如果后端还做了二次解码,还会用%252e%252e%252f这种双编码。Course 1里如果没过滤,就直接写../flag,一旦碰到过滤,这就是后续绕过的地基。
5. 做题过程中常见的几个坑
5.1 Base64输出被截断或遮挡
这个坑我踩过一次,带新人时也见不少人踩。当目标文件比较大,比如要读一个包含大量代码的PHP文件时,输出的Base64很长,页面结构会把内容分成多个区块,浏览器渲染时还可能折叠。你复制到的可能只是一部分,解码出来永远只有半截代码,怎么都找不到flag。
解决方法是不要从浏览器渲染后的页面复制,而是右键查看源代码,或者用curl直接拿响应:
curl "http://target/index.php?file=php://filter/read=convert.base64-encode/resource=flag.php"拿到完整响应后,再对Base64片段做处理。最好把整段复制到本地文件,再base64 -d,避免终端换行把Base64切断。另外,有些Base64字符串里会包含+、/、=这种符号,在URL传输时要留意,不过从页面输出中复制下来的通常不会丢失。
5.2 浏览器和URL编码对特殊符号的处理
当你准备尝试更复杂的payload时,URL编码会成为一个隐形陷阱。例如?file=php://filter/read=convert.base64-encode/resource=/var/www/html/flag.php,其中/在大多数情况下不用编码,浏览器会原样发送。但路径里如果出现#,它会被浏览器识别为锚点,直接截断后面的内容,导致请求地址变了,payload失效。遇到这种情况,需要把#编码成%23。
同样,?在URL里有特殊含义,如果你要读的文件名里带着问号,必须先编码成%3F。空格要么编码成%20要么用+,取决于后端解析方式。虽然BUU Course 1题目里用到的字符比较简单,但只要你开始尝试绝对路径,或者想读取带空格的日志文件,编码就一定会出现。我给自己定的规矩是:所有payload先在Burp里构造,由Burp自动处理标准编码,这样能少踩很多不必要的坑。
5.3 容器环境与本地环境的文件路径差异
做这道题时,最让我难受的是本机测试环境跑得好好的payload,放到BUU的在线题目环境里却读不出内容。后来才发现,问题基本都出在路径假设上。本地开发时,你的Web目录可能是/Applications/XAMPP/htdocs/或者C:/phpstudy_pro/WWW/,而BUU的容器环境通常是/var/www/html/。如果payload里写死了本地路径,远程环境自然读不到。
所以拿到题目后,第一件事不是猜测flag的绝对路径,而是先读index.php源码,从源码里的__DIR__、define等常量去判断真实目录结构。如果源码里没有线索,就用filter依次尝试常见的Web根目录。你的目标是找到"服务器本身认得出"的路径,而不是你熟悉的路径。这个概念在真实渗透中也一样:同一个漏洞,在Windows和Linux上利用路径完全不同,提前搞清楚环境属于哪一类能省下大量时间。
6. 打通Course 1之后,LFI还能怎么学
6.1 从文件读取到远程命令执行的桥梁
很多人做完Course 1这道题,觉得LFI不过如此——读个文件而已。但实际上,LFI在真实攻击链里最让防守方头疼的一点是它往往能升级成远程代码执行。只要你能让服务器包含一个"内容可控"的文件,那就不再是读文件,而是直接执行任意代码。
最常见的桥是日志注入。Nginx的访问日志会记录请求的User-Agent,如果你在User-Agent里写入<?php system($_GET['cmd']); ?>,然后通过LFI包含/var/log/nginx/access.log,这段PHP代码就会被include当作代码执行。再例如,临时上传文件的路径、/proc/self/environ里的User-Agent注入、Session文件包含,都是同一套路变种。Course 1里用到的php://filter只是第一步,学会它再做日志注入,思路会天衣无缝。
也可以尝试php://input配合POST数据:如果PHP配置允许allow_url_include,你可以直接用php://input把POST的原始请求体当作文件内容包含,快得像开挂。不过这个配置默认是关闭的,很多CTF题为了卡人也会刻意关掉,用它之前最好先看源码确认。
6.2 进阶题型和练习路线
打通Course 1后,我推荐继续往下刷BUUCTF的LFI系列后续课程。后面的题会逐步加难题面:有的过滤php://、有的过滤base64、有的把file参数用str_replace清洗、有的需要组合伪协议过滤器链。练习的目标不是记住某个固定payload,而是能把"路径控制+协议封装+过滤器叠加+编码绕过"这件事灵活组合。
如果只想练基本功,建议按这个顺序走:先在本地搭一个LFI漏洞环境,源码里故意放几道障碍;然后尝试读取/etc/passwd验证最基础包含;接着用php://filter读自己的源码;再试试php://input和日志注入;最后升级到无回显情况下的盲注思路。每一步都理解了,再回头看BUU的题会非常轻松。
我在带新人刷这道题时经常说一句话:LFI题目看起来是一个点,背后却是一张网。它连接着文件系统、PHP运行时、协议解析、过滤规则、编码转换,任何一个环节理解不到位,都会被看似奇怪的现象卡住。把Course 1这道题吃透,等于把这张网的锚点打牢了,后面再遇到各种变种,都只是在这张网上添枝加叶而已。做完题之后,不妨再回头想想,如果你来写这道题的过滤代码,你会怎么设障碍,这比单纯把flag刷出来更有收获。