Struts2和Webwork远程命令执行漏洞分析

 

作者: 空虚浪子心,  出处:IT专家网

原文链接:http://security.ctocio.com.cn/100/11466600.shtml

  在漏洞发现者发布的POC中,并不能影响xwork 2.1.2之前的一些版本(这个版本之前的一些版本,下文会统称为老版本,之后的叫做新版),例如struts 2.0.14(就是struts修补了N个高危漏洞后的第一个版本,最常用的版本)其实是不能打的,本文会分析这个漏洞的起因,和结果,也会给出通杀 POC的思路。

  本文希望看懂文章的人,可以专注于分析思路,但是不希望大家拿着POC到处搞站,本文不提供任何黑客工具,所有的POC,都是已经公布过,无数人都知道的。

  xwork修补漏洞的悲剧(漏洞历史):

  xwork作为struts2和webwork的核心组件,曾经在已经修补过了“xwork参数拦截器允许ognl方法执行”漏洞,并给出了漏洞公告

  http://struts.apache.org/2.x/docs/s2-003.html

  S2-003 

  第一次修补

  但是这个修补,最终的结果是个悲剧。大家还记得我以前说过,在《Struts2框架安全缺陷》一文中,完全不懂web安全的开发人员,为了修补 XSS,竟然仅仅针对POC,过滤了这段字符“〈script〉”,一旦用户提交“〈script xxx〉”就绕过的傻X修补方式。

  他们保持一贯的风格,利用正则“{\\p{Graph}&&[^,#:=]}*”修补此漏洞。并且又仅仅针对攻击者给出的POC,做出修补,使用POC测试通过就发布了。

 

  解析下xwork开发人员使用POC的几个悲剧的测试用例。

  为了让ParametersInterceptor认为它是个合法ognl语句,变量中必须最终包含#。

  这行无法通过验证,java在做字符串运算时,#会被转义为#之后,才会做匹配,所以返回false。下图可以看到,#和#是完全相等的。

  所以修补漏洞的开发人员,自作聪明,直接用正则把#干掉了。

  攻击者发来的虽然是#,但是这段字符在内存中做字符串运算时,会先变成\#然后才做运算。java在处理用户提交的一段 string包含\时,为了保证数据完整性,会自动多加一个\用做转义,比如用户提交了数据“\n”,在内存中作字符串运算时,不会真的用换行做运算,而 是拿”\\n”这段字符做比较。那么用户提交的#被转为\#,就会绕过对#的检查。

  证据如图:

  这两个case没有通过代码验证原因同上。

  悲剧就在这里,这里虽然是\#,符合用户提交的场景,悲剧是这段字符里面因为有空格,就在+两边,所以无法通过,而攻击者如果去掉空格,就通过了。

  官方就这样发布了,其实发布的是个漏洞版。

  这是第一次修补,这次修补其实是大家都知道,因为发了公告出来,官方虽然公告说是高危,但是官方只知道攻击者可以通过ognl表达式修改 server端的session等信息。这时官方还没有意识到这其实是个远程代码执行漏洞,ognl不仅仅支持修改,还支持执行一些静态方法,比如 @Runtime@getRuntime().exec(“calc”)。

  第二次修补

  直到某天,可能是在版本xwork2.1.2时,官方偷偷修改安全配置,默认让SecurityMemberAccess(管理ognl权限的 类)的allowStaticMethodAccess为false,这导致静态方法不能执行,并且不知什么原因,偷偷修改正则,也同时放开了对参数名称 中空格字符的限制。

  漏洞被爆

  这次出了远程代码执行(Struts2/XWork < 2.2.0 Remote Command Execution Vulnerability),漏洞的发现者就是看到了#的限制其实无效,研究出了绕过默认安全配置的方法,并且利用ognl允许静态方法执 行,达到了远程代码执行的效果。

  原理简介:

  1、 用户提交了#被转义为\#,通过了对参数名称的验证后,最终ognl处理之前,又变成了#,也就是#,符合了ognl语法。

  2、 通过Ognl语句执行,可以在struts2和webwork运行起来时,把ognl上下文中的一些默认配置覆盖掉,漏洞发现者给出了不少可以覆盖的数值。

  3、 虽然默认配置是禁止静态方法执行的,但是xwork的配置,其实是可以覆盖的,一旦覆盖掉“用于禁止静态方法执行”的value,当然又可以执行了。

  4、 Runtime.getRuntime().exec()这段,其实可以当做静态方法调用,导致执行系统命令。Ognl语句调用静态方法:@Runtime@getRuntime().exec(“calc”)。

  漏洞发现者给出了shellcode,但是经过我的测试,shellcode并不能影响所有版本,而是仅仅针对xwork2.1.2及以上的版 本有效,xwork2.1.2及以上核心被应用在struts2和webwork某些版本中,所以他们间接受到了影响,但是修补代码,是xwork去做 的。

  漏洞发现者给出POC:

  山寨的修补方式带来的后果

  我看到很多人对这个漏洞简单分析了下,有一小撮不明真相的群众认为,这是因为参数名称\#带来的后果,官方自己上次修补的不完善,所以,可以过滤\#,搞定这件事情。

  如图是我做见过的“其中一个”漏洞分析者,自己搞的山寨补丁,原理就是禁止\#:

  摘自互联网某篇文章,不点名了,第二点解决方案居然不符合XML规则-_-!。

  这只是其中一种方案,很多人说只要禁止\#就可以了,不得不说,这个人应该分析了漏洞,并且了解了原理。他知道虽然官方的补丁出来 了,但是官方用的是白名单形式,对参数名称限制太严厉,很多特殊符号不能使用,可能会导致部分应用出问题,这是典型的开发人员思维。

  这是官方的补丁:

   http://svn.apache.org/viewvc/struts/struts2/trunk/xwork-core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java?view=markup&pathrev=956389

  为了优先保证业务,只要禁止了\#就可以修补漏洞,所以以上山寨方案貌似是可行的,并且经过测试POC打不了了。

  我也看到了这个方法,这真是个悲剧,拦截#能解决问题么?

  为什么漏洞发现者,通过#绕过了#限制?还有没有其他编码可以绕过?

  答案是,还有其他编码可以绕过,仅仅控制了#,是个悲剧。经过我实际测试,发现#号的8进制编码\43,也是在这里使用的,并且\043也是可以的。于是我笑了:

  这段新的POC,没有任何一个#,却一样可以执行calc,经过实际测试,绕过了所有仅仅过滤#的防御。让他们慢慢修补吧,我们不着急,等大家都打上了过滤#的补丁,再把这个新的POC放出来。

  我看到neeao就很谨慎,直接上官方补丁,这是他对这个漏洞的分析:

  http://neeao.com/archives/59/

  老版本的struts和webwork的POC不通用问题

  在推行修补方案时,开发人员总是从自己的角度和经验修补漏洞,他们采用过滤#的方案也罢了,最起码态度端正。不像有些互联网公司的开 发根本不去补,原因很简单,他用的是老版本的struts和webwork。对xwork2.1.2以下核心的struts2和webwork,POC打 下去没有任何效果,所以认为这个安全级别不高,还是等等官方公告吧(现在为止,官方没有发布任何公告,修补好代码提交SVN,也没有编译后发布版本)。

  出于好奇,决定仔细研究下。我之前也不熟悉xwork源码和ognl,以前仅仅研究过struts2的部分源码,盲目的debug了好几天,解决了N个问题,才搞定:

  空格问题:

  原POC中,会传三个参数,它们的作用,首先解析下。

  1、第一个参数

  在高版本的struts中,allowStaticMethodAccess(允许静态方法访问访问,做权限判断)默认是false的。但是低版本的本来就是true,所以传不传都一样,可以省了。

  2、第二个参数

  这句必须在,否则也不会调用静态方法。

  3、第三个参数

  这句是shellcode,不能没有。

  在老版本中,第二个参数,是不能运行的,把它弄的好看点:

  注意,new后面,有空格,在ParametersInterceptor的参数正则验证中,根本过不去。 既然老版本不允许参数中出现空格,那么如果你的shell里如果有空格,会通过么?嘿嘿。。。只要你的shell,无法通过这段验证,就不会执行:

  以上shellcode必须对空格和:符号,做16进制的转义,才能执行。

  PS:大家都悄悄的,不要告诉那些“只拿poc,不看技术文章的那些不明真相群众”。

  所以,要必须先解决的第一个问题是,改这个空格为 ,才能进入ognl表达式的流程。

  在比较新一点的版本的xwork中,允许空格,当然,也是允许 的,所以 替换空格,就通杀了第一个问题的新老版本struts正则验证。

  denyMethodExecution不能修改的问题:

  改完之后,发现竟然还是不能赋值,经过调试,在内存中,看到的 xwork.MethodAccessor.denyMethodExecution还是true,这说明这个表达式,没有执行成功。原因是这里做了 new对象操作。先定义了#foo变量为new java.lang.Boolean类型,默认为false,之后denyMethodExecution等于#foo。这是不允许的,原来的POC导致 空指针异常(原因后面说),后来解决了。

  总之用这个,可以通杀新老版本,也不会爆空指针:

  提交后再次查看内存中的context,发现这个值被修改为false。

  看看shellcode

  Shellcode的原理是,利用ognl支持静态方法执行,调用java的执行系统命令方法(其实完全可以调用任何java代码,比如写个文件等)。

  Shellcode是可以做new操作的。

  denyMethodExecution不能new操作,是因为这句执行时的上下文中, denyMethodExecution还是true,执行了这句,才是false,这时才可以new对象。

  所以shellcode的上下文,是可以做new对象操作的。

  Xwork的bug问题:

  解决了这两个问题,其实已经给shellcode创造了完备的环境,按照xwork的逻辑,应该直接让我们调用静态方法才是,但是在shellcode运行时,居然爆出了空指针,这个问题我研究了好久才搞明白,原来是xwork自己出了bug,到了新版本时,才修补。

  翻翻svn,看到在xwork2.1.2时,偷偷修改了一段代码。

  SecurityMemberAccess这个类原来有个这样的方法

  到了xwork2.1.2时,改为了:

  注意标红的,如果name==null,就返回true。

  为什么会有这行代码呢?

  它调用isExcluded(name),进入isExcluded方法后,做正则表达式的验证。

  如果传进来的是个paramName是null,并且excludeProperties是有值的,必然报错。Xwork的bug就是,所有的版本,调用静态方法时,都必然会传进来一个null,并且excludeProperties也是默认有值的。

  任何一个静态方法的调用,这里传进来的都是null。

  也就是说,要调用静态方法,就必须让excludeProperties这个家伙的值,是一个空的Set对象,否则对null对正则匹配,就会报错。

  excludeProperties是个Set,在shell执行的上下文中,它的值是这样来的:

  xwork处理用户发进来的ognl表达式时,会用xwork的SecurityMemberAccess做权限判断,以保证静态方法不会被人 随便调用。原理是传给shell执行的上下文中几个默认配置,其中一个是默认的allowStaticMethodAccess=true,也会给 excludeProperties这个Set对象会被添加一个value,结果就不是空的Set对象了。

  所以,在shellcode上下文中,执行静态方法必然会出错。

  让shellcode正常执行,解决方法的思路就是new一个新的HashSet。

  但是经过我测试,这里还是不能做new的操作,因为new的时候,会调用构造方法,这实质上还是在调用方法,调用方法就会出错。为了证明我的猜 测,debug调用到这一步的时候,手工修改它为new HashSet(),立刻就通过了。不能new,又怎么能得到一个空的Set呢?这是最后一道关卡。为了搞定它,我甚至查了struts- default.xml在内存中的位置,考虑是不是使用ognl修改掉。

  因为这个放弃了一段时间,很郁闷,当我再次拾起来,却突然看到了这个值的默认值。

  跟进emptySet();

  这是个有意思的知识点,我也是第一次了解到,可以使用EMPTY_SET,取到一个空的set。

  于是当我提交某些东西,让

  这一句覆盖了默认的那个有了一个值的map,重新变成了空的map,这才真正的达到了静态方法的终极调用条件。

  其实我都快吐血了,明明是xwork自己代码写的不严谨,直到他们新的版本才发现这个bug,并修补,居然导致我研究漏洞的同时还顺带帮他们处理bug。

  最后终于找到机会鄙视一下漏洞发现者的poc,他说老版本的struts2可以使用XXX的POC,经过我测试,不可行,原理也很简单,本文说了,POC中不能有空格。

  这句话即使在新版本中,也是没什么影响的,虽然会改变一个值,但是不受啥影响, shellcode中,有了这句话,就兼容新老版本,一切通杀了!

  最后凑出通杀0DAY在这里:

  被和谐了,没办法,毕竟在互联网公司里,要考虑到其他互联网公司同行的处境,虽然他们不一定都是漏洞公布的POC不能打的版本。

  POC,不要找我要了,本文仅仅说下技术研究。

  回顾下:

  适合新版本绕过双引号和#防御的poc:

  适合新版本绕过#的poc

  仅仅适用老版本(struts2.0.12)的POC

  仅仅适用新版本的也就是漏洞发现者放出来的poc

发表评论?

0 条评论。

发表评论