0x01 背景说明
月初和southwind0师傅做代码审计时,发现了一个比较奇葩的问题。系统设置了全局的XSS过滤器 ,在其他功能点上生效了,但在一个公告发布功能没有被过滤。southwind0师傅通过对比数据包 发现公告发布数据包 是上传 包(也就是我们常见的上传 POST请求)。后来我经过编写测试代码,发现过滤器确实无法过滤上传数据包的参数值。
这让我不禁思考 *”上传包可绕过Java过滤器?”*,如果是真的,那么问题很严重呀,以后过滤器岂不是都可以这样绕过,那这样全局XSS,SQL注入防御过滤器岂不是形同虚设?查了下网上大多数提供XSS过滤器代码基本都存在这个问题,我意识到问题的严重性,打算深入Tomcat和Spring MVC的底层代码一探究竟。
0x02 测试代码
由于审计的代码属于敏感信息,我编写了一个和审计场景几乎一样的测试Demo用于本文的研究。测试Demo有get,post和upload页面用于测试Java过滤器对三种类型请求数据包的过滤情况。
2.1 后端处理代码
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44
package me.gv7.controller;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;@Controller @RequestMapping ("/test" )public class TestController { @RequestMapping (value = "get" ,method = RequestMethod.GET) public String doGet (Model model, String str,String bbb) { System.out.println("[+] " + str); model.addAttribute("res" ,str); return "get" ; } @RequestMapping (value = "post" ,method = RequestMethod.GET) public String post () { return "post" ; } @RequestMapping (value = "post" ,method = RequestMethod.POST) public String doPost (Model model,String str) { System.out.println("[+] " + str); model.addAttribute("res" ,str); return "post" ; } @RequestMapping (value = "upload" ,method = RequestMethod.GET) public String upload () { return "upload" ; } @RequestMapping (value = "upload" ,method = RequestMethod.POST) public String doUpload (Model model,String str) { System.out.println("[+] " + str); model.addAttribute("res" ,str); return "upload" ; } }
2.2 过滤wrapper代码
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
package me.gv7.filter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper (HttpServletRequest request) { super (request); } public String[] getParameterValues(String parameter) { String[] values = super .getParameterValues(parameter); if (values==null ) { return null ; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0 ; i encodedValues[i] = cleanXSS(values[i]); } return encodedValues; } public String getParameter (String parameter) { String value = super .getParameter(parameter); if (value == null ) { return null ; } return cleanXSS(value); } public String getHeader (String name) { String value = super .getHeader(name); if (value == null ) return null ; return cleanXSS(value); } private String cleanXSS (String value) { value = value.replaceAll(", "<" ).replaceAll(">" , ">" ); value = value.replaceAll("eval((.*))" , "" ); value = value.replaceAll("alert((.*?))" , "" ); value = value.replaceAll("confirm((.*?))" , "" ); value = value.replaceAll("["']展开 收缩
*javascript:(.*)["']" , """" ); value = value.replaceAll("(?i)script" , "" ); return value; } }
2.3 全局过滤器设置
1 2 3 4 5 6 7 8 9
filter > filter-name > XssFilterfilter-name > filter-class > me.gv7.filter.XssFilterfilter-class >filter >filter-mapping > filter-name > XssFilterfilter-name > url-pattern > /*url-pattern > dispatcher > REQUESTdispatcher >filter-mapping >
想获取完整代码,请到公众号后台回复”上传包绕Java过滤器测试代码”
0x03 原理分析
为了方便描述,我这里将请求分文三种,GET型请求,普通POST型请求和上传POST型请求。本文的普通型POST请求指的是除上传POST型请求之外的POST请求,而上传POST型请求就是我们上传包对应的请求。
3.1 Spring MVC如何获取到HTTP请求参数值?
为了更透彻的理解出现该问题的原因,我们需要搞清楚Spring MVC框架是如何获取到前端传来的HTTP请求的参数值。
前端提交的请求会先到达Tomcat服务器,其解析请求参数主要在Request.parseParameters()
中进行。
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 31 32 33 34 35 36 37
protected void parseParameters () { this .parametersParsed = true ; Parameters parameters = this .coyoteRequest.getParameters(); boolean success = false ; try { parameters.setLimit(this .getConnector().getMaxParameterCount()); ... parameters.handleQueryParameters(); if (this .usingInputStream || this .usingReader) { success = true ; } else if (!this .getConnector().isParseBodyMethod(this .getMethod())) { success = true ; } else { String contentType = this .getContentType(); ... if ("multipart/form-data" .equals(contentType)) { this .parseParts(); success = true ; return ; } else if (!"application/x-www-form-urlencoded" .equals(contentType)) { success = true ; return ; } else { int len = this .getContentLength(); ... parameters.processParameters(formData, 0 , len); ... success = true ; } } } }
Tomcat会根据ContentType
是否为multipart/form-data
判断是否问上传POST型请求,若是则会调用parseParts()
来解析,我们继续跟进。由于allowCasualMultipartParsing
配置项默认为false
,parseParts()
直接就返回了,也就是说Tomcat默认不会解析上传POST请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
private void parseParts () { if (this .parts == null && this .partsParseException == null ) { MultipartConfigElement mce = this .getWrapper().getMultipartConfigElement(); if (mce == null ) { if (!this .getContext().getAllowCasualMultipartParsing()) { this .parts = Collections.emptyList(); return ; } ... } ... } }
对针对GET行请求和普通POST,Tomcat会调用parameters.processParameters()
方法来解析。我们简单看下它的代码。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
private void processParameters (byte [] bytes, int start, int len, Charset charset) { ... int decodeFailCount = 0 ; int pos = start; int end = start + len; label172: while (pos int nameStart = pos; int nameEnd = -1 ; int valueStart = -1 ; int valueEnd = -1 ; boolean parsingName = true ; boolean decodeName = false ; boolean decodeValue = false ; boolean parameterComplete = false ; do { switch (bytes[pos]) { case 37 : case 43 : if (parsingName) { decodeName = true ; } else { decodeValue = true ; } ++pos; break ; case 38 : if (parsingName) { nameEnd = pos; } else { valueEnd = pos; } parameterComplete = true ; ++pos; break ; case 61 : if (parsingName) { nameEnd = pos; parsingName = false ; ++pos; valueStart = pos; } else { ++pos; } break ; default : ++pos; } } while (!parameterComplete && pos if (pos == end) { if (nameEnd == -1 ) { nameEnd = pos; } else if (valueStart > -1 && valueEnd == -1 ) { valueEnd = pos; } } ... }
至此,Tomcat层面对前端请求解析工作结束。接下来Spring MVC会收到Tomcat传来的HttpServletRequest
,此时若请求为上传POST型,Spring MVC会继续调用commons-fileuplad.jar
对Tomcat传来的原生Servlet请求类HttpServletRequest
的实例进行解析处理。
Spring MVC将原生的HttpServletRequest
对象传入CommonsMultipartResolver
类的parseRequest()
方法进行解析处理。
CommonsMultipartResolver.parseRequest()
方法主要分两步对上传请求进行解析。
第一步,调用commons-fileupload.jar
中的ServletFileUpload
类的parseRequest()
方法来解析出保存有上传表单各个元素的FileItem
列表。
第二步,调用CommonsFileUploadSupport.parseFileItem()
方法解析FileItem
列表为保存有表单字段名,字段值等信息MultipartParsingResult
类型的Map
。
下面我们来看下这两步的执行细节。首先第一步最终的处理方法为FileUploadBase.parseRequest()
FileUploadBase.parseRequest()
解析完会返回一个FileItem
实例列表。FileItem
就是存储着上传表单的各种元素(字段名,ContentType,是否是简单表单字段,文件名。)本例中我们提交的上传表单的FileItem
内容如下:
接着来到第二步,调用CommonsFileUploadSupport.parseFileItem()
对commons-fileupload.jar
处理的结果—FileItem列表,进行处理。
最后将上传表单解析的所有元素(multipartFiles,multipartParameters,multipartParameterContentTypes)封装为一个MultipartParsingResult
并返回。至此上传POST型请求的解析工作完成。
最后Spring MVC,会使用HandlerMethodInvoker.resolveRequestParam()
方法,将解析好的请求参数的值,绑定到不同的对象上,方便Controller层获取。具体我们在下面说。
3.2 上传包无法被过滤的原理
上面我们用较大边幅说明了Spring MVC是如何获取到前端发来的请求的参数值。下面我们就很好理解,问题的所在了。
经过跟踪发现,Spring MVC对各类型请求参数的解析并实现自动绑定,主要在HandlerMethodInvoker.resolveRequestParam()
方法。
继续跟进到获取参数值的那一步。
通过调式发现,这里如果是GET型和普通POST型请求的话,getRequest()
获取到的对象是我们编写的过滤类XssHttpServletRequestWrapper
的实例,故调用该对象getParameterValues()
来获取值,自然是被过滤了!
若是上传POST行请求的话,getRequest()
获取到的是CommonsMultipartResolver
类的对象。但实际上调用该对象的getParamterValues()
方法,会执行到DefaultMultipartHttpServletRequest
类的getParamterValues()
类获取值。这是调式发现的,我暂时也没有搞清楚为何,不过不影响我们解决本次研究的问题。
到这里我们基本明白了,上传包中的参数值没有被过滤,是因为Spring MVC在解析上传包获取其参数值时,没有使用我们编写的过滤类XssHttpServletRequestWrapper
中的getParamterValues()
方法,而是使用了DefaultMultipartHttpServletRequest
类getParamterValuses()
。
你可能有疑问,为何SpringMVC获取上传POST请求的参数值时,为啥不调用XssHttpServletRequestWrapper.getParamterValues()来获取呢?
答:因为这样获取不到。
借助以下相关类和接口的继承实现关系图,我们继续看看为何获取不到。
结合我们上面对Spring MVC和Tomcat如何解析到请求包的参数值的过程,知道GET型和普通POST型请求包是可以通过HttpServletRequest.getParameterValues()
直接获取到对应参数的值,而通过图中可知XssHttpServletRequestWrapper
实现了HttpServletRequest
,自然也是可以通过XssHttpServletRequestWrapper.getParameterValues()
获取到的。
但上传包Tomcat默认没有解析,根据继承关系XssHttpServletRequestWrapper
对象中保存的解析结果为Tomcat解析请求的结果,故通过该对象的getParameterValues()
方法获取到的参数值为null
。也是因此Spring MVC针对Tomcat解析的结果—原生HttpServletRequest
,使用common-fileupload.jar
来继续解析,得到MultipartHttpServletRequest
的实现对象。DefaultMultipartHttpServletRequest
类实现了MultipartHttpServletRequest
,故通过该类的getParameterValues()
方法即可获取到上传POST请求的参数值!
最后特别说明一点,其实上传POST请求数据是流经过过滤器的。没有被过滤,是由于获取参数值的时候,没有调用过滤器Wrapper对象的方法。所以最终我们看到了上传包可以“绕过”过滤器检查的现象。
0x05 最后的思考
在文章发布区,评论区,公告区….等功能点上常常需要上传图片或附件,这时表单往往会以上传包的形式提交数据。而这些功能点也是hack们最关注的XSS漏洞测试点,若不注意上传包可”绕过”过滤器的问题,会造成很严重的后果!
我从新翻开了之前审计的项目代码,发现很多Spring MVC项目都是使用过滤器对XSS和SQL注入进行全局防御。而过滤器的代码与本文例子的中过滤器代码相似,很明显都是从网上Copy过来的。这样编写代码是存在问题的,针对这种情况,我们该如何正确防御,我们下周文章详述!
文章来源于gv7.me:上传包可“绕过”Java过滤器的检查?
相关推荐: XSS中的JS转义和HTML转义
今天在给客户做渗透测试时,遇到了一处XSS。虽然很简单,但有点小意思。引发了我对js转义和html转义在XSS中的思考,故做个小笔记记录一下。两个例子都会以仿写现实场景的代码的说明问题。 0x01 一点知识贮备 1.1 关于转义 **;格式的字符串是html的…
评论