
本地文件包含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参数上。如果页面完全没有提示你可以在地址栏手动加参数试试?fileindex.php。加了之后如果页面出现源码高亮或者多了一些内容那就说明参数生效了。这类题目最常使用的参数名有file、page、filename、path、include、template等它们都是开发者习惯性用来传文件名的字段名。拿到题目后不要急着上工具先把这些常见参数都手动测一遍观察页面变化。这一步属于信息收集阶段做得越细后面判断越准。我见过有人一上来就打几个大payload结果参数名根本没对上浪费了时间。参数探测时可以用浏览器地址栏直接改URL也可以用Burp Suite的Repeater反复发包。我更推荐后者因为Burp可以保留每次请求的历史记录方便对比不同参数下的回显差异。对新手来说只要能坚持每次只改变一个变量其他都保持不变浏览器手测也完全够用。2.2 回显特征反推后端逻辑传不同值看回显本质是在做黑盒探测。你不需要看到源码就能通过现象猜出后端大概长什么样。这里有几个非常典型的回显特征传入?fileindex.php后页面出现了带语法高亮的PHP源码说明后端可能直接highlight_file($_GET[file])但这种情况很少作为LFI题出现。传入不存在的文件名比如?filenonexist.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?filephp://filter/readconvert.base64-encode/resourceindex.php HTTP/1.1 Host: target服务端收到后PHP会把index.php的内容读出来经过convert.base64-encode过滤器编码再输出到页面。你看到的是一串形如PD9waHAg...的Base64文本。这里有个细节要留意resource后面跟的是路径这个路径是相对于当前工作目录的。如果当前脚本是index.phpresourceindex.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可能踩到执行问题新手最容易犯的一个错误是知道?fileflag.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?filephp://filter/readconvert.base64-encode/resourceflag.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?filephp://filter/readconvert.base64-encode/resourceflag HTTP/1.1这里有个非常实用的小技巧在resource后面可以同时使用多个过滤器用管道符|隔开。比如你想把Base64再解回原文可以用convert.base64-decode但这个操作在只需读flag时一般用不到。真正用得上的是对过滤规则敏感的场景如果题目把php://、filter、base64这些关键词挡掉了你就得换filter的等价写法比如去掉read写成php://filter/convert.base64-encode/resourceflag.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?filephp://filter/readconvert.base64-encode/resourceflag.php拿到完整响应后再对Base64片段做处理。最好把整段复制到本地文件再base64 -d避免终端换行把Base64切断。另外有些Base64字符串里会包含、/、这种符号在URL传输时要留意不过从页面输出中复制下来的通常不会丢失。5.2 浏览器和URL编码对特殊符号的处理当你准备尝试更复杂的payload时URL编码会成为一个隐形陷阱。例如?filephp://filter/readconvert.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刷出来更有收获。