

从RFC规范看如何绕过waf上传表单 上篇
source link: https://www.anquanke.com/post/id/241265
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从RFC规范看如何绕过waf上传表单 上篇
阅读量 39361 | 评论 1
发布时间:2021-05-20 10:00:53
作者:donky16@360云安全
本文主要讨论,利用waf和后端程序对multipart/form-data的解析差异,造成对waf的bypass。
传统waf以规则匹配为主,如果只是无差别的使用规则匹配整个数据包,当规则数量逐渐变多,会造成更多性能损耗,当然还会发生误报情况。为了能够解决这些问题,需要对数据包进行解析,进行精准位置的规则匹配。
正常业务中上传表单使用普遍,不仅能够传参,还可以进行文件的上传,当然这也是一个很好的攻击点,waf想要能够精准拦截针对表单的攻击,需要进行multipart/form-data格式数据的解析,并针对每个部分,如参数值,文件名,文件内容进行针对性的规则匹配拦截。
虽然RFC规范了multipart/form-data相关的格式与解析,但是由于不同后端程序的实现机制不同,而且RFC相关文档也会进行增加补充,最终导致解析方式各不相同。对于waf来说,很难做到对各个后端程序进行定制化解析,尤其是云waf更加无法实现。
由于篇幅有限,此文分为上下两个部分,第一部分主要介绍multipart/form-data中boundary的格式与解析规范,并分析不同程序对boundary解析的异同,和一些造成waf解析失败或和后端解析产生差异的方法;第二部分会介绍Content-Disposition/Content-Type/Content-Transfer-Encoding的内容,介绍多种绕过waf解析的方式,并对Content-Disposition中的参数和boundary参数的解析进行比较,分析其解析不同的原因。
multipart/form-data相关RFC:
- 基于表单的文件上传: RFC1867
- multipart/form-data: RFC7578
- Multipart Media Type: RFC2046#section-5.1
Flask/Werkzeug解析环境:docker/httpbin
Java解析环境:Windows10 pro 20H2/Tomcat9.0.35/jdk1.8.0_271/commons-fileupload
Java输出代码:
String result = "";
DiskFileItemFactory factoy = new DiskFileItemFactory();
ServletFileUpload sfu = new ServletFileUpload(factoy);
try {
List<FileItem> list = sfu.parseRequest(req);
for (FileItem fileItem : list) {
if (fileItem.getName() == null) {
result += fileItem.getFieldName() + ": " + fileItem.getString() + "\n";
} else {
result += "filename: " + fileItem.getName() + " " + fileItem.getFieldName() + ": " + fileItem.getString() + "\n";
}
}
} catch (FileUploadException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
PHP解析环境:Ubuntu18.04/Apache2.4.29/PHP7.2.24
PHP输出代码:
<?php
var_dump($_FILES);
var_dump($_POST);
POST /post HTTP/1.1
Host: www.example.com:8081
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary=I_am_a_boundary
Content-Length: 303
--I_am_a_boundary
Content-Disposition: form-data; name="name"; filename="file.jsp"
Content-Type: text/plain;charset=UTF-8
This_is_file_content.
--I_am_a_boundary
Content-Disposition: form-data; name="key";
Content-Type: text/plain;charset=UTF-8
This_is_a_value.
--I_am_a_boundary--
此表单数据含有一个文件,name为name,filename为file.jsp,file_content为This_is_file_content.,还有一个非文件的参数,其name为key,value为This_is_a_value.。
httpbin解析结果
{
"args": {},
"data": "",
"files": {
"name": "This_is_file_content."
},
"form": {
"key": "This_is_a_value."
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "deflate, identity;q=0.5",
"Accept-Language": "en",
"Content-Length": "303",
"Content-Type": "multipart/form-data; boundary=I_am_a_boundary",
"Host": "www.example.com:8081",
"Route-Hop": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36"
},
"json": null,
"origin": "10.1.1.1",
"url": "http://www.example.com:8081/post"
}
1. Content-Type
Content-Type: multipart/form-data; boundary=I_am_a_boundary
对于上传表单类型,Content-Type必须为multipart/form-data
,并且后面要跟一个边界参数键值对(boundary),在表单中分割各部分使用。
倘若multipart/form-data
编写错误,或者不写boundary
,那么后端将无法准确解析这个表单的每个具体内容。
2. Boundary
boundary: RFC2046
boundary需要按照以下BNF巴科斯范式
简单解释就是,boundary不能以空格结束,但是其他位置都可以为空格,而且字符长度在1-70之间,此规定语法适用于所有multipart类型,当然并不是所有程序都按照这种规定来进行multipart的解析。
从前面介绍的multipart基础格式可以看出来,真正作为表单各部分之间分隔边界的不仅是Content-Type中boundary的值,真正的边界是由--
和boundary
的值和末尾的CRLF
组成的分隔行,当然为了能够准确解析表单各个部分的数据,需要保证分隔行不会出现在正常的表单中的文件内容或者参数值中,所以RFC也建议使用特定的算法来生成boundary值。
flask解析结果
这里需要注意两个点,第一,最终表单数据最后一个分隔边界,要以--
结尾。第二,RFC规定原文为
也就是说,整体的分隔边界可以含有optional linear whitespace
。
注:本文使用空格的地方[\r\n\t\f\v ]
都可以代替使用,文中只是介绍了使用空格的结果,大家可以测试其他的,waf或者后端程序在解析\n
时,会产生很多不同结果,感兴趣可自行测试。
首先使用boundary的值后面加空格进行测试,flask和php都能够正常的解析出表单内容。
php解析结果
虽然boundary的值后面加了空格,但是在作为分隔行的时候并没有空格也可以正常解析,但是经测试发现如果按照RFC规定那样直接在分隔行中加入空格,效果就会不一样。
对于flask来说是按照了RFC规定实现,无论Content-Type中boundary的值后面是不加空格还是加任意空格,在表单中非结束分隔行里都可以随意加空格,都不影响表单数据解析,但是需要注意的就是,在最后的结束分隔行中,加空格会导致解析失败,下文阐述具体原因。
很有意思的是php解析过程中,在非结束分隔行中不能增加空格,而在结束分隔行中增加空格,却不会影响解析。
可以看到,加了空格的分隔行内的文件内容数据没有被正确解析,而没加空格的非文件参数被解析成功,而且结束分隔行中也添加了空格。
测试的时候偶然发现在如果在multipart/form-data
和;
之间加空格,如Content-Type: multipart/form-data ; boundary="I_am_a_boundary"
,flask会造成解析失败,php解析正常。
正常来说,通过正则进行匹配解析的flask应该不会这样,具体实现在werkzeug/http.py:L406
。
简单来说就是将Content-Type: multipart/form-data ; boundary="I_am_a_boundary"
进行正则匹配,然后将第一组匹配结果当作mimetype,第二组作为rest,由后面处理boundary取值,看下这个正则。
_option_header_start_mime_type = re.compile(r",\s*([^;,\s]+)([;,]\s*.+)?")
为了看着美观,使用regex101看下。
很明显,由于第一组匹配非空字符,所以到空格处就停了,但是第二组必须是[;,]
开头,导致第二组匹配值为空,无法获取boundary,最终解析失败。
boundary的值是支持用双引号进行编写的,就像是表单中的参数值一样,这样在写分隔行的时候,就可以将双引号内的内容作为boundary的值,php和flask都支持这种写法。使用单引号是无法达到效果的,这也是符合上文提到的BNF巴科斯范式的bcharsnospace
的。
测试一下让重复多个双引号,或者含有未闭合的双引号或者双引号前后增加其他字符会发生什么。
Content-Type: multipart/form-data; boundary=a"I_am_a_boundary"
Content-Type: multipart/form-data; boundary= "I_am_a_boundary"
Content-Type: multipart/form-data; boundary= "I_am_a_boundary"a
Content-Type: multipart/form-data; boundary=I_am_a_boundary"
Content-Type: multipart/form-data; boundary="I_am_a_boundary
Content-Type: multipart/form-data; boundary="I_am_a_boundary"aa"
Content-Type: multipart/form-data; boundary=""I_am_a_boundary"
对于php来说相对简单,因为只要出现第一个字符不是双引号,就算是空格,都会将之作为boundary的一部分,所以前四种解析类似,当第一个字符为双引号时,会找与之对应的闭合的双引号,如果找到了,那么就会忽略之后的内容直接取双引号内内容作为boundary的值。
然而如果没有找到闭合双引号,就会导致boundary取值失败,无法解析multipart/form-data。
当然对于最后一种情况,会取一个空的boundary值,我也以为会解析失败,但是很搞笑的是,竟然boundary值为空,php也可以正常解析,当然也可以直接写成Content-Type: multipart/form-data; boundary=
。
大多数waf应该会认为这是一个不符合规范的boundary,从而导致解析multipart/form-data失败,所以这种绕过waf的方式显得更加粗暴。
对于flask来说,可以看下解析boundary的正则werkzeug/http.py:L79
。
_option_header_piece_re = re.compile(
r"""
;\s*,?\s* # newlines were replaced with commas
(?P<key>
"[^"\\]*(?:\\.[^"\\]*)*" # quoted string
|
[^\s;,=*]+ # token
)
(?:\*(?P<count>\d+))? # *1, optional continuation index
\s*
(?: # optionally followed by =value
(?: # equals sign, possibly with encoding
\*\s*=\s* # * indicates extended notation
(?: # optional encoding
(?P<encoding>[^\s]+?)
'(?P<language>[^\s]*?)'
)?
|
=\s* # basic notation
)
(?P<value>
"[^"\\]*(?:\\.[^"\\]*)*" # quoted string
|
[^;,]+ # token
)?
)?
\s*
""",
这个正则可以解释本文的大多数flask解析结果产生的原因,这里看到flask对于boundary两边的空格是做了处理的,对于双引号的处理,都会取第一对双引号内的内容作为boundary的值,对于非闭合的双引号,会处理成token
形式,将双引号作为boundary的一部分,并不会像php一样解析boundary失败。
从上面正则也能看出,对于最后一种Content-Type的情况,flask也会取空值作为boundary的值,但是这不会同过flask对boundary的正则验证,导致boundary取值失败,无法解析,下文会提及到。
以flask的正则中quoted string
和token
作为区分是否boundary为双引号内取值,测试两种转义符的位置会怎样影响解析。
- a.
\
在token
中Content-Type: multipart/form-data; boundary=I_am_a\"_boundary
这种形式的boundary,flask和php都会将
\
认定为一个字符,并不具有转义作用,并将整体的I_am_a\"_boundary
内容做作为boundary的值。 - b.
\
在quoted string
中Content-Type: multipart/form-data; boundary="I_am_a\"_boundary"
对于flask来说,在双引号的问题上,
werkzeug/http.py:L431
中调用一个处理函数,就是取双引号之间的内容作为boundary的值。可以看到,在取完boundary值之后还做了一个
value.replace("\\\\", "\\").replace('\\"', '"')
的操作,将转义符认定为具有转义的作用,而不是单单一个字符,所以最终boundary的值是I_am_a"_boundary
。对于php来说,依旧和
token
类型的boundary处理机制一样,认定\
只是一个字符,不具有转义作用,所以按照上文双引号
中提到的,由于遇到第二个双引号就会直接闭合双引号,忽略后面内容,最终php会取I_am_a\
作为boundary的值。
空格 & 双引号
上文提到使用空格对解析的影响,既然可以使用双引号来指定boundary的值,那么如果在双引号外或者内加入空格,后端会如何解析呢?
- a. 双引号外对于flask来说,依旧和普通不加双引号的解析一致,会忽略双引号外(两边)的空格,直接取双引号内的内容作为boundary的值,php对于双引号后面有空格时,处理机制和flask一致,但是当双引号前面有空格时,会无法正常解析表单数据内容。
解析会和不带双引号的实现一致,此时php会将前面的空格和后面的双引号和双引号的内容作为一个整体,将之作为boundary的值,当然这虽然符合RFC规定的boundary可以以空格开头,但是把双引号当作boundary的一部分并不符合。
- b. 双引号内此时php会取双引号内的所有内容(非双引号)作为boundary的值,无论是以任意空格开头还是结束,其分隔行中boundary前后的空格数,要与Content-Type中双引号内boundary前后的空格个数一致,否则解析失败。
值得注意的是,flask解析的时候,如果双引号内的boundary值以空格开始,那么在分隔行中类似php只要空格个数一致,就可以成功解析,但是如果双引号内的boundary的值以空格结束,无论空格个数是否一致,都无法正常解析。
为什么出现这种状况,看下werkzeug是如何实现的,flask对boundary的验证可以在
werkzeug/formparser.py:L46
看到。#: a regular expression for multipart boundaries _multipart_boundary_re = re.compile("^[ -~]{0,200}[!-~]$")
这个正则是来验证boundary有效性的,比较符合RFC规定的,只不过在长度上限制更小,可以是空格开头,不能以空格结尾,但是用的不是全匹配,所以以空格结尾也会通过验证。
上图使用
boundary= " I_am_a_boundary "
,所以boundary的值为" I_am_a_boundary "
双引号内的内容,而且这个值也会通过boundary正则的验证,最终还是解析失败了,很是奇怪。上文空格
中提到,对于flask来说,在非结束分隔行中boundary后可以加任意空格,不影响最终的解析。原因是解析multipart/form-data具体内容时,为了寻找分割行,将每一行数据都进行了一个
line.strip()
操作,这样会把CRLF去除,当然会把结尾的所有空格也给strip掉,所以当boundary不以空格结尾时,在分隔行中可以随意在结尾加空格。但是这也会导致一个问题,当不按照RFC规定,用空格结尾作为boundary值,虽然过了flask的boundary正则验证,但是在解析body时,却将结尾的空格都strip掉,导致在body中分隔行经过处理之后变为了-- I_am_a_boundary
,这与Content-Type中获取的boundary值(结尾含有空格)并不一致,导致找不到分隔行,解析全部失败。
结束分隔行
在上文空格
内容中提到,php在结束分割行中的boundary后面加空格并不会影响最终的解析,其实并不是空格的问题,经测试发现,其实php根本就没把结束分隔行当回事。
可以看到,没有结束分隔行,php会根据每一分隔行来分隔各个表单部分,并根据Content-Length来进行取表单最后一部分的内容的值,然而这是极不尊重RFC规定的,一般waf会将这种没有结束分隔行的视为错误的multipart/form-data格式,从而导致整体body解析失败,那么waf可以被绕过。
上文提到flask会对multipart/form-data的每一行内容进行strip操作,但是由于结束分隔行需要以--CRLF
结尾,所以在strip的过程中只会将CRLF
strip掉,但是在解析boundary的时候,boundary是不能以空格为结尾的,最终会导致结束分隔行是严谨的--BOUNDARY--CRLF
,当然如果使用双引号使boundary以空格结尾,那么结束分隔行是可以正确解析的,但是非结束分隔行无法解析还是会导致整体解析失败。
从flask的代码能够看出来,支持参数名的quoted string
形式,就是参数名在双引号内。
而对于Java来说,支持参数名的大小写不敏感的写法。
https://github.com/postmanlabs/httpbin
https://www.ietf.org/rfc/rfc1867.txt
https://tools.ietf.org/html/rfc7578
https://tools.ietf.org/html/rfc2046#section-5.1
https://www.php.net/manual/zh/language.variables.external.php
https://www.cnblogs.com/youxin/archive/2012/02/13/2348551.html
Recommend
-
42
严正声明:本文仅限于技术讨论,严禁用于其他用途。 简介 文件上传漏洞是web安全中经常利用到的一种漏洞形式。一些web应用程序中允许上传图片,文本或者其他资源到指定的位置,文件上传漏洞就是利用这些可以上传...
-
17
*本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担 本文写于大年三十,首先祝大家新年快乐!今天感慨颇多,算算今天也是我入驻Freebuf第930天,也是我毕业后的第930天,对Freebuf更多的是感谢,因...
-
9
【网络安全】文件上传绕过思路
-
5
为了绕过阿里云盘上传文件类型限制,一种将压缩包改变成一张图片的方式分享 2021-09-13 技术
-
8
rfc2388表单上传文件NodeJS无依赖版本实现 - TheNorthMemory官方文档小分队犯了一个错误,就是试图用文本语言来表达非字符内容,整得一众开发者迷途了,社区反馈波澜滔滔。这支小分队应该每人扣一个长鹅抱宠,捐给像俺这样努力帮扶开发者的贡献者(😄)。其实rfc...
-
6
WordPress Hosting...
-
12
WordPress Hosting...
-
26
0x01 简介在一次对站点进行安全测试中,遇到任意文件上传的漏洞,但是目标系统存在阿里云WAF和gd库的二次渲染,通过不断尝试绕过,最终拿下目标。 0x02 文件上传点寻找注册登录到某系统后,在多个文...
-
4
1、大致思路 以下是基于先处理图片、后端返回图片地址进行的 存数据 1、将图片信息提交到后端 2、后端处理 3、后端返回前端图片的访问地址 4、前端将图片地址存入要提交的表单中 5、将整个表单...
-
11
每创建一个虚拟环境类似于重新下载了一个纯净的python解释器 但虚拟环境不要创建太多,会占用电脑硬盘空间 python项目1需要使用:django1.11 python38 项目2需要使用:django2.2...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK