尝试入门 Java 安全,下一步的计划是结合 CodeQL 以及静态分析、污点分析这方面的技术积累构建自己的技术栈。

环境搭建

参考Java Web安全入门——S2漏洞测试环境搭建,搭建的本地环境,顺带试了一下 exp,效果还不错

Show

漏洞分析

漏洞成因

关于漏洞的成因,参考 https://cwiki.apache.org/confluence/display/WW/S2-001 的官方描述:

Problem

The ‘altSyntax’ feature of WebWork 2.1+ and Struts 2 allows OGNL expressions to be inserted into text strings and is processed recursively. This allows a malicious user to submit a string, usually through an HTML text field, containing an OGNL expression that will then be executed by the server if the form validation has failed. For example, say we had this form that required the ‘phoneNumber’ field to not be blank:

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

The user could leave the ‘phoneNumber’ field blank to trigger the validation error, then populate the ‘name’ field with %{1+1}. When the form is re-displayed to the user, the value of the ‘name’ field will be ‘2’. The reason is the value field is, by default, processed as %{name}, and since OGNL expressions are evaluated recursively, it is evaluated as if the expression was %{%{1+1}}.

The OGNL parsing code is actually in XWork and not in WebWork 2 or Struts 2.

可以看出原因主要在于 XWork 允许 OGNL 表达式递归地执行,而当用户提交表单而且表单的验证失败时,会对字段进行解析,此时如果字段是 OGNL 表达式的话,则该字段会被递归执行。

OGNL 是一种功能非常强大的表达式语言,但也存在着非常多的漏洞,关于 OGNL 的安全问题可以参考乌云上的这篇文章 OGNL设计及使用不当造成的远程代码执行漏洞

漏洞定位

存在漏洞的函数在 xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.java

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

  while(true) {
    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);
    Object o = stack.findValue(var, asType);
    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;
    }
  }
}

通过审计代码可以发现,当 while 循环生成的 expression 变量一直符合 %{.*} 的形式时,该循环会一直持续执行。当传入的参数是 %{username},经过 stack.findValue(var, asType); 的解析,expression 变量会变成username 表单项内具体的值。也就是说如果我们在表单的 username 项填入 %{1+1} 的话,循环会继续。

Loop 1

下一次循环,经过对 %{1+1} 的解析,得到结果 2,此时由于不满足格式的条件要求,循环停止,函数返回。

Loop 2

最终页面将 username 项内的值渲染为 2:

Loop 2

P.S. 关于调试的问题,可以参考 Dlive 师傅说的,以 OGNL 原生 API 处下断点,因为最终框架还是会调用原生 API 来实现功能。

我在看《Struts2技术内幕 - 深入解析Struts2架构设计与实现原理》这本书的时候

发现了一个调试Struts2 OGNL表达式导致的问题的一个好方法

既然是OGNL导致的RCE,那就是说Payload里的OGNL表达式会被执行

那么我们直接在OGNL的原生API getValue处下断点即可,这个方法用于解析OGNL表达式并返回表达式的值

OGNL最低层的数据访问API存在于ognl jar包中的OGNL.java

漏洞修复

通过加入变量 loopCount 限制了递归解析 OGNL 表达式的次数,进而完成了对漏洞的修复。

Fix

总结

还在想为啥这 bug 用的库都这么老,一看 CVE 号是 CVE-2007-4556,怪不得……

参考链接