1、概述
S2-005是由于官方在修补S2-003不全面导致绕过补丁造成的。我们都知道访问Ognl的上下文对象必须要使用#符号,S2-003对#号进行过滤,但是没有考虑到unicode编码情况,导致\u0023或者8进制\43绕过。
S2-005则是绕过官方的安全配置(禁止静态方法调用和类方法执行),再次造成漏洞。
Payload如下:
http://www.xxxx.com/aaa.action?(‘\u0023_memberAccess[\‘allowStaticMethodAccess\‘]‘)(meh)=true&(aaa)((‘\u0023context[\‘xwork.MethodAccessor.denyMethodExecution\‘]\u003d\u0023foo‘)(\u0023foo\u003dnew%20java.lang.Boolean("false")))&(asdf)((‘\u0023rt.exit(1)‘)(\u0023rt\u003d@java.lang.Runtime@getRuntime()))=1
提交上述URL,就会导致服务器down掉。把编码转换过来为下面三个步骤:
(1)?(‘#_memberAccess[‘allowStaticMethodAccess‘]‘)(meh)=true
(2)&(aaa)((‘#context[‘xwork.MethodAccessor.denyMethodExecution‘]=#foo‘)(#foo=new%20java.lang.Boolean("false")))
第一步将_memberAccess变量中的allowStaticMethod设置为true,这里payload还要加括号,并且还带个"(meh)"呢?其实是为了遵守Ognl语法树的规则,这个后面再说。第一步完成后,就可以执行静态方法了。
第二步将上下文中的xwork.MethodAccessor.denyMethodExecution 设置为false,即允许方法的执行,这里的MehodAccessor是Struts2中规定方法/属性访问策略的类,也存在与Ognl的上下文中。同样遵守Ognl语法树规则。
第三步就是真正的攻击代码,前两步就是要保证第三步成功执行,第三步就是执行了关闭服务器的代码。但是要过调用Runtime类的静态方法获取一个Runtime对象。
2、深入探讨
那么为什么攻击者只要提交这个攻击URL就能使服务器down掉呢?仅仅看这三句不要以为就懂了,要理解其中的奥妙,必须要阅读源码。
中间件处理一个用户request顺序是这样的:
首先服务器接收到请求后会读取web.xml文件,这个是网站配置文件,里面有个过滤器,叫:org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
然后这个过滤器执行完之后,就可以执行我们的Action了。
这个类主要是做一些配置文件读取,以及默认拦截器栈的执行。
具体是这样的,查看struts-default.xml配置文件,这里有全部的类信息。
这里定义了一个默认的package叫struts-default,里面定义了结果集的类型以及默认的拦截器(interceptors),还有默认加载的类ActionSupport。
以及,最重要的是默认执行的拦截器栈(default-interceptor-ref),名叫defaultStack。
这个拦截器栈的结构如下:
是个栈结构,在执行我们自己的Action,这里面的拦截器会依次执行,对于我们分析来说,比较重要的一个拦截器是params,它的作用主要是用来处理URL中的参数的,类全名为:org.apache.struts2.interceptor.ActionMappingParametersInteceptor。
拦截器中的执行方法是doIntecept(),这里做了如下处理,在方法addParametersToContext:
可以看到这里获取一个值栈,并将参数(parameters)放入进去。
在setParameters方法中,其实是将请求做成了键值对,加入了新的值栈(Map结构)中。
在Ognl中,值栈(ValueStack)的实现类是OgnlValueStack,看看里面的setValue方法,发现又调用了OgnlUtil的setValue,好一个装饰模式。。。
跟进去OgnlUtil类,发现又包装了一遍,最终是调用了Ognl中的setValue方法。
注意,这里有个compile方法,用来将传入的字符串进行解析,执行完之后返回一个语法树。
执行完compile方法,再执行setValue方法,参数为四个:语法树、上下文、根对象以及传入的value值。
要解释payload中代码为啥都放到一个一个括号里,这就牵扯到Ognl的语法树的原理。由于我现在还没学过编译原理,只能看个大概。
在Ognl中,有几个类型的语法树,ASTEval、ASTAssign、ASTStaticMethod、ASTConst、ASTProperty、ASTChain等,这些在构造树的时候会应用于不同的语法格式,虽然传入的ognl仅仅是字符串。比如传入user.name就是生成ASTChain,因为这里采用了链式结构来访问user对象中的name属性。
Payload中的攻击代码有两个形式:
(1)?(‘#_memberAccess[‘allowStaticMethodAccess‘]‘)(meh)=true
(2)&(aaa)((‘#context[‘xwork.MethodAccessor.denyMethodExecution‘]=#foo‘)(#foo=new%20java.lang.Boolean("false")))
一种是(expression)(constant) = value,一种是(constant)((expression1)(expression2))。
Ognl解析引擎是这样处理的,每个括号对应语法树上的一个分支,并且从最右边的叶子节点开始解析执行。
比如最后一个攻击代码,解析成语法树如下:
从右枝开始执行,依次分解,并按照不同类型的子树上面的方法来执行setValueBody或者setValueBody方法,并且对每条Ognl进行执行。
总结一下,对于:
(1)(expression)(constant)= value会执行expression=value。
(2)(constant)((expression1)(expression2))会先执行expression2,然后再执行expression1。
这就是为什么payload中的攻击代码会以那么奇怪的形式用括号包裹起来。
3、总结
到这里整个漏洞机理就比较清楚了。由于参数进入了params拦截器中,这个拦截器将url参数中的Ognl表达式以键值对的方式放入值栈,Ognl解析执行引擎就会按照规定的语法规则解析字符串并执行Ognl表达式。由于拦截器中没有对特定的符号进行完全的过滤(没有过滤八进制和十六进制的unicode码),所以导致任意代码执行,任意代码执行可以的话,就是花式攻击的节奏了。
最后一句,由于本人不才,对于语法树的分析不是很到位,期待牛人对这一块的讲解与分享。