在去年某次红队评估项目中遇到一个Java的fckeditor上传接口,测试过程中存在WAF基于数据包的拦截。在通过审计上传组件commons-fileupload的源码绕过WAF后,才想起来fckeditor java版本是有白名单的,虽然没能从这个点GetShell,但这个过程还是比较有意思。于是记录下commons-fileupload组件中可能可以用来绕过WAF的点。在22年看过Java文件上传大杀器-绕waf(针对commons-fileupload组件)这篇文章,不过就记得一个MIME编码了,而且当时没能成功绕过,误打误撞又自己分析了一遍。最近又看了下,当时绕过的方法反而是文章中一个比较简单的废弃的方法,想着再详细梳理下commons-fileupload的解析过程,于是有了这篇文章。
整体过程
首先看org.apache.commons.fileupload.FileUploadBase#parseRequest(org.apache.commons.fileupload.RequestContext)
函数,先解析当前文件列表
1 | FileItemIterator iter = this.getItemIterator(ctx); |
然后遍历文件列表,根据解析好的文件信息以及文件流等进行复制
1 | while(iter.hasNext()) { |
所以解析的逻辑主要在org.apache.commons.fileupload.FileUploadBase.FileItemIteratorImpl#FileItemIteratorImpl
函数中
可能用于绕过的点
multipart/前缀绕过
在解析之前通过Servlet上下文获取Content-Type头,然后通过判断其内容是否为multipart/
开头,如果是则继续后续的逻辑
所以我们可以通过添加多余空格的方式绕过:Content-Type: multipart/ form-data;
之前的WAF绕过用的正是这个技巧
大小写混合绕过
同时可以看到,在进行比较的时候,组件对Content-Type的值统一转换成了小写,所以可以进行大小写的变形:Content-Type: MulTIPaRt/form-data;
boundary解析过程中的分割符绕过
在bounndary相关解析逻辑中:
1 | org.apache.commons.fileupload.FileUploadBase#getBoundary |
为了将Content-Type的内容解析成key-value参数,在这个过程中会扫描其内容,并以;
、,
以及=
作为分割符号从而解析出key和value
1 | protected byte[] getBoundary(String contentType) { |
根据org.apache.commons.fileupload.ParameterParser#parse(java.lang.String, char[])
的逻辑中,选择;
和,
中第一个出现的字符作为分割符,并且由于后续的代码逻辑,如果添加多个分割符的话需要需要保证第一个和最后一个均为相同的分割符号
1 | public Map<String, String> parse(String str, char[] separators) { |
比如,我选择多个,
作作为分割符,可以这样
Content-Type: multipart/form-data,,,;;;;,hello-world,,;;;;;,,,,boundary=------------------------7jqxUg1tWaweeuyRNJxBrB
选择;
作为分割符,可以这样:Content-Type: multipart/form-data;;;;;,,,,,,====,,;;;;;boundary=------------------------7jqxUg1tWaweeuyRNJxBrB
空白字符绕过
1 | org.apache.commons.fileupload.FileUploadBase#getBoundary |
org.apache.commons.fileupload.ParameterParser#getToken函数扫描Content-Type内容
1 | private String getToken(boolean quoted) { |
所以理论上multipar/formdata可以添加任意空白字符串,但由于大部分会破坏http数据包结构只能添加空格
最终payload:Content-Type: MulTIPaRt/form-data ;
multipart属性解析——form-data前缀绕过
org.apache.commons.fileupload.FileUploadBase#getFieldName(java.lang.String),同样支持大小写
multipart属性解析——MIME编码绕过
接下来就是解析header头参数,这部分key-value的解析逻辑和前面一致
1 | while(this.hasChar()) { |
不过由于这里是multipart属性的部分了,所以需要关注value的解析,这里的重点就是Java文件上传大杀器-绕waf(针对commons-fileupload组件)里写的MIME编码了,最终payload如下:
1 | Content-Disposition: form-data; name="file"; filename="=?utf-8?B?dGVzdC50eHQ=?=" |
总结
根据commons-fileupload的解析特性,总结可能可以用于绕过WAF的方式:
格式前缀绕过
Content-Type以
multipart/
开头,Content-Disposition以form-data
开头,后面可以添加额外的内容进行绕过,比如Content-Type: multipart/helloform-data;boundary=------------------------7jqxUg1tWaweeuyRNJxBrB
Content-Disposition:form-datahello; name="file"; filename="test.txt"
Key的大小写绕过以及空白字符
数据包中作为Key被解析的部分,比如
multipart/form-data
、boundary
、以及name、filename这些属性值本身,在解析时会统一转成小写,所以可以利用大小写混用绕过;以及扫描key、value过程中会跳过空白字符,比如:Content-Disposition: form-data; name="file"; FILENAME = "test.txt"
分割符绕过
key,value的解析是依据
;
、,
以及=
等符号,所以可以利用多个分割符进行绕过,需要注意的是,http header头中第一个和最后一个分割符必须统一,比如均为;
或者,
:Content-Type: multipart/form-data;;;====,,,,,===;;;;boundary=------------------------7jqxUg1tWaweeuyRNJxBrB
而multipart属性中分隔符只能用
;
:Content-Disposition:form-data;NAME="file";---;FILENAME="test.txt"
value中的MIME编码
- 例如:
Content-Disposition: form-data; name="file"; filename="=?utf-8?B?dGVzdC50eHQ=?="
- 例如:
思考
那次遇到的WAF是基于黑名单的校验,只要是multipart数据包,就禁止上传JSP相关的后缀;问题产生的原因在于WAF对multipart的识别和后端组件解析方式不一致导致的绕过;我不清楚WAF具体的解析方式,所以想到的成本比较低的修复方式是在进行Content-Type识别时,对这一行内容非boundary部分统一转成大写或者小写,然后去掉其中的空白字符,再识别是否为multipart/form-data
类型数据包;