漏洞公告

首先看Struts2 官方给出的公告信息可得知:

Apache Struts 2.3.x版本中启用了Struts 2 Struts 1 plugin 可能导致任意代码执行漏洞

关于Struts 1 plugin:

“The Struts 1 plugin allows you to use existing Struts 1 Actions and ActionForms in Struts 2 applications.

This plugin provides a generic Struts 2 Action class to wrap an existing Struts 1 Action,org.apache.struts2.s1.Struts1Action.”

以上内容可参考Struts 1 Plugin,简单的说就是org.apache.struts2.s1.Struts1Action 类为一个Wrapper类,可以将 Struts1时代的Action封装成为Struts2中的Action,以便让其可以继续在struts2应用中工作。

漏洞DEMO分析

网上已经有几篇分析很棒的文章可以参考,通过几篇分析文章可以了解到官方提供的demo程序Showcase中的Struts1 Integration就存在该漏洞,这里以struts-2.3.24-all.zip中的demo为例,详细看下代码:

SaveGangster.Action的实现类为Struts1Action,而在Struts1Action的 execute 方法中,会调用对应的Action 的 execute 方法,如下:

public String execute() throws Exception {
        ......
        ......
        Action action = null;

        try {
            //获取Action,这里this.className为SaveGangsterAction
            action = (Action)this.objectFactory.buildBean(this.className, (Map)null);
        } catch (Exception var12) {
            throw new StrutsException("Unable to create the legacy Struts Action", var12, actionConfig);
        }

        Struts1Factory strutsFactory = new Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
        ActionMapping mapping = strutsFactory.createActionMapping(actionConfig);
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpServletResponse response = ServletActionContext.getResponse();
        //调用SaveGangsterAction的execute方法
        ActionForward forward = action.execute(mapping, this.actionForm, request, response);
        //获取request中ActionMessage
        ActionMessages messages = (ActionMessages)request.getAttribute("org.apache.struts.action.ACTION_MESSAGE");
        //检查ActionMessage是否为null,如果存在则继续
        if(messages != null) {
            Iterator i = messages.get();

            label36:
            while(true) {
                while(true) {
                    if(!i.hasNext()) {
                        break label36;
                    }

                    ActionMessage msg = (ActionMessage)i.next();
                    if(msg.getValues() != null && msg.getValues().length > 0) {
                        this.addActionMessage(this.getText(msg.getKey(), Arrays.asList(msg.getValues())));
                    } else {
                        //这里msg的values为null,key为Gangster ${1+3} added successfully,这里进入getText函数
                        this.addActionMessage(this.getText(msg.getKey()));
                    }
                }
            }
        }
        ......

通过(Action)this.objectFactory.buildBean(this.className, (Map)null);获取当前action,这里className为

<param name="className">org.apache.struts2.showcase.integration.SaveGangsterAction</param>

而该demo中SaveGangsterAction类继承了Action并重写了execute方法:

public class SaveGangsterAction extends Action {
    @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");
    }
}

这里同时将gforn.getName()放到了ActionMessage结构中,并添加到request,属性名为org.apache.struts.action.ACTION_MESSAGE。动态调试可以知道这里name的值为:

${1+3}

继续往下看代码的执行流程。在调用SaveGangsterAction的execute方法后,接着检查了request中ActionMessage是否为空,不为空则对ActionMessage进行处理并回显给客户端。这里调用了getText函数。

this.addActionMessage(this.getText(msg.getKey()));

getText函数的存在是因为Struts2要走向世界,帮助用户解决前端国际化问题。它会根据不同的Locale(本例中为zh_CN)去对应的资源文件里面获取相关文字信息并展现。这样如果你要开发国际化的应用就不需要每种语言都整一个模版了。。这个在n1nty的分析中也有提到。

继续跟进,最后到了LocalizedTextUtil类的findText方法,这个方法分析过Struts2漏洞的都熟悉,如今年的S2-045。

public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) {
        ValueStack valueStack = ActionContext.getContext().getValueStack();
        return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
    }

这里aTextName、defaultMessage均为 “Gangster ${1+3} added successfully”

查看LocalizedTextUtil.findText函数的介绍

“If a message is found, it will also be interpolated. Anything within ${...} will be treated as an OGNL expression and evaluated as such.”

message中在${…}中的任何值都将被视为OGNL表达式被解析执行,从而导致RCE。如图

关于POC

这个漏洞并不具有通用性,且利用方式和之前的漏洞几无差别。在源代码审计的时候或许可以根据具体参数构造poc验证。可参考jas502n提供的测试POC

安全建议

S2-048漏洞原因是将用户可控的值添加到ActionMessage并在客户前端展示,导致其进入getText函数,最后message被当作ognl表达式执行。

所以开发者通过使用resource keys替代将原始消息直接传递给ActionMessage。 不要使用如下的方式

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

参考

[1] http://bobao.360.cn/learning/detail/4078.html [2] https://github.com/jas502n/st2-048 [3] http://xxlegend.com/2017/07/08/S2-048%20%E5%8A%A8%E6%80%81%E5%88%86%E6%9E%90/ [4] http://blog.topsec.com.cn/ad_lab/strutss2-048%E8%BF%9C%E7%A8%8B%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

源链接

Hacking more

...