转载

上传包为何可"绕过"Java过滤器?

月初和southwind0师傅做代码审计时,发现了一个比较奇葩的问题。系统设置了全局的XSS过滤器,在其他功能点上生效了,但在一个公告发布功能没有过滤到。southwind0师傅通过对比数据包发现公告发布数据包是Multipart数据包(也就是我们常见的文件上传数据包)。后来我经过编写测试代码发现上传数据包确实无法过滤。

这让我不禁思考 “上传包可绕过Java过滤器?” ,如果是真的,那么问题很严重呀,以后过滤器岂不是都可以这样绕过,那这样全局XSS,SQL注入防御过滤器岂不是形同虚设?查了下网上大多数提供XSS过滤器代码基本都存在这个问题,我意识到问题的严重性,打算深入Tomcat和Spring MVC的底层代码一探究竟。

0x02 测试代码

由于审计的代码属于敏感信息,我编写了一个和审计场景几乎一样的测试Demo用于本文的研究。测试Demo有get,post和upload页面用于测试Java过滤器对三种类型请求数据包的过滤情况。

上传包为何可"绕过"Java过滤器?

2.1 后端处理代码

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){/*@RequestParam("str") */
        System.out.println("[+] " + str);
        model.addAttribute("res",str);
        return "upload";
    }
}

2.2 过滤wrapper代码

package me.gv7.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class XssHttpServletRequestWrapperextends 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 < count; 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("//(", " ").replaceAll("//)", " ");
        value = value.replaceAll("eval//((.*)//)", "");
        value = value.replaceAll("alert//((.*?)//)", "");
        value = value.replaceAll("confirm//((.*?)//)", "");
        value = value.replaceAll("[///"///'][//s]*javascript:(.*)[///"///']", "/"/"");
        value = value.replaceAll("(?i)script", "");
        return value;
    }
}

2.3 全局过滤器设置

<filter>
    <filter-name>XssFilter</filter-name>
    <filter-class>me.gv7.filter.XssFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>XssFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

想获取完整代码,请到公众号后台回复”上传包绕Java过滤器测试代码”

0x03 原理分析

为了方便描述,我这里将请求分文三种,GET型请求,普通POST型请求和上传POST型请求。本文的普通型POST请求指的是处理上传POST型请求,而上传POST型请求就是我们上传包对应的请求。

3.1 Spring MVC如何获取到HTTP请求参数值?

为了更透彻的理解出现该问题的原因,我们需要搞清楚Spring MVC框架是如何获取到前端传来的HTTP请求的参数值。

前端提交的请求会先到达Tomcat服务器,其解析请求参数主要在 Request.parseParameters() 中进行。

//org.apache.catalina.connector.Request
   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 {
                // 获取请求包ContentType头
                String contentType = this.getContentType();
                ...
                // 如果请求ContentType为multipart/form-data,也就是上传POST
                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请求。

private void parseParts(){
        if (this.parts == null && this.partsParseException == null) {
            MultipartConfigElement mce = this.getWrapper().getMultipartConfigElement();
            if (mce == null) {
                /*
                 Tomcat7.0+ 已经内置了multipart支持,但是必须显示激活,默认关闭。在全局tomcat配置文件context.xml,或者为war的本地context.xml添加<Context allowCasualMultipartParsing="true">开启。
                */
                if (!this.getContext().getAllowCasualMultipartParsing()) {
                    this.parts = Collections.emptyList();
                    return;
                }
                ...
            }
			...
        }
    }

对针对GET行请求和普通POST,Tomcat会调用 parameters.processParameters() 方法来解析。我们简单看下它的代码。

  private void processParameters(byte[] bytes,int start, int len, Charset charset){
...
      int decodeFailCount = 0;
      int pos = start;
      int end = start + len;

      label172:
      while(pos < end) {
          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]) {
              /*如果遇到%(37)和+(43),会对值进行进行URL解码*/
              case 37:
              case 43:
                  if (parsingName) {
                      decodeName = true;
                  } else {
                      decodeValue = true;
                  }

                  ++pos;
                  break;
              /*如果遇到的&(38),标记该处为参数名和参数值结尾*/
              case 38:
                  if (parsingName) {
                      nameEnd = pos;
                  } else {
                      valueEnd = pos;
                  }

                  parameterComplete = true;
                  ++pos;
                  break;
              /*如果遇到=(61),标记该处为参数名的结尾,参数值的开始处*/
              case 61:
                  if (parsingName) {
                      nameEnd = pos;
                      parsingName = false;
                      ++pos;
                      valueStart = pos;
                  } else {
                      ++pos;
                  }
                  break;
              default:
                  ++pos;
              }
          } while(!parameterComplete && pos < end);

          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() 方法进行解析处理。

上传包为何可&quot;绕过&quot;Java过滤器?

CommonsMultipartResolver.parseRequest() 方法主要分两步对上传请求进行解析。

  • 第一步,调用 commons-fileupload.jar 中的 ServletFileUploadparseRequest 来解析出保存有上传表单各个元素的 FileItem 列表。
  • 第二步,调用 CommonsFileUploadSupport.parseFileItem() 方法解析 FileItem 列表为保存有表单字段名,字段值等信息 MultipartParsingResult 类型的 Map

下面我们来看下这两步的执行细节。首先第一步最终的处理方法为 FileUploadBase.parseRequest()

上传包为何可&quot;绕过&quot;Java过滤器?

FileUploadBase.parseRequest() 解析完会返回一个 FileItem 实例列表。 FileItem 就是存储着上传表单的各种元素(字段名,ContentType,是否是简单表单字段,文件名。)本例中我们提交的上传表单的 FileItem 内容如下:

上传包为何可&quot;绕过&quot;Java过滤器?

接着来到第二步,调用 CommonsFileUploadSupport.parseFileItem()commons-fileupload.jar 处理的结果—FileItem列表,进行处理。

上传包为何可&quot;绕过&quot;Java过滤器?

最后将上传表单解析的所有元素(multipartFiles,multipartParameters,multipartParameterContentTypes)封装为一个 MultipartParsingResult 并返回。至此上传POST型请求的解析工作完成。

最后Spring MVC,会使用 HandlerMethodInvoker.resolveRequestParam() 方法,将解析好的请求参数的值,绑定到不同的对象上,方便Controller层获取。具体我们在下面说。

3.2 上传包无法被过滤的原理

上面我们用较大边幅说明了Spring MVC是如何获取到前端发来的请求的参数值。下面我们就很好理解,问题的所在了。

经过跟踪发现,Spring MVC对各类型请求参数的解析并实现自动绑定,主要在 HandlerMethodInvoker.resolveRequestParam() 方法。

上传包为何可&quot;绕过&quot;Java过滤器?

继续跟进到获取参数值的那一步。

上传包为何可&quot;绕过&quot;Java过滤器?

通过调式发现,这里如果是GET型和普通POST型请求的话, getRequest() 获取到的对象是我们编写的过滤类 XssHttpServletRequestWrapper 的实例,故调用该对象 getParameterValues() 来获取值,自然是被过滤了!

若是上传POST行请求的话, getRequest() 获取到的是 CommonsMultipartResolver 类的对象。但实际上调用该对象的 getParamterValues() 方法,会执行到 DefaultMultipartHttpServletRequest 类的 getParamterValues() 类获取值。这是调式发现的,我暂时也没有搞清楚为何,不过不影响我们解决本次研究的问题。

上传包为何可&quot;绕过&quot;Java过滤器?

到这里我们基本明白了,上传包中的参数值没有被过滤,是因为Spring MVC在解析上传包获取其参数值时,没有使用我们编写的过滤类 XssHttpServletRequestWrapper 中的 getParamterValues() 方法,而是使用了 DefaultMultipartHttpServletRequestgetParamterValuses()

你可能有疑问,为何SpringMVC获取上传POST请求的参数值时,为啥不调用XssHttpServletRequestWrapper.getParamterValues()来获取呢?

答:因为这样获取不到。

借助以下继承实现关系图,我们继续看看为何获取不到。

上传包为何可&quot;绕过&quot;Java过滤器?

结合我们上面对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请求的参数值!

0x05 最后的思考

在文章发布区,评论区,公告区….等功能点上常常需要上传图片或附件,这时表单往往会以上传包的形式提交数据。而这些功能点也是hack们最关注的XSS漏洞测试点,若不注意上传可”绕过”过滤器的问题,会造成很严重的后果!

我从新翻开了之前审计的项目代码,发现很多Spring MVC项目都是使用过滤器对XSS和SQL注入进行全局防御。而过滤器的代码与本文例子的中过滤器代码相似,很明显都是从网上Copy过来的。这样编写代码是存在问题的,针对这种情况,我们该如何正确防御,我们下周文章详述!

原文  http://gv7.me/articles/2019/why-can-multipart-post-bypass-java-filter/
正文到此结束
Loading...