Structs2-005分析&复现
环境搭建
调试环境:Manjaro Linux
,jdk1.8_25
,tomcat6.0.51
工具:IDEA 2020.2.3
由于已经有了一套完善的搭建环境了(vulhub),所以我们这里借用P师傅的环境进行本地调试,如果你的idea
是最新版有可能出现无法添加Struts2
框架支持,这时候需要先在插件里下载好,启用即可:
<!--more-->
然后我们为了本地调试需要将vulhub
中的war
包解压,首先创建一个新项目,因为这样可以让idea
帮我们把结构组织好,可以使用如下图的方式创建项目:
在此之前如果你没有下载tomcat
或者其他中间件,请移步tomcat6,由于这个洞很老了,算是互联网考古了,因此我下载的是tomcat6
,实验了一下tomcat7
发现会直接400
,似乎是一些特殊字符不允许使用了,为了方便起见直接用tomcat6
吧
最后添加到idea
环境中:
将他们的vulhub/s2-005
下的war
包解压,移到项目下即可,整体结构如下图:
然后就可以正常启动和调试了。
漏洞复现
struts2-2.1.8.1
使用下面的POC即可复现成功:
?(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)(('%5cu0023rt.exec(%22touch@/tmp/helloworld%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))=1
此POC没有回显:
但我们可以看到确实创建成功该文件。
漏洞分析
首先根据漏洞信息:
要点有以下两个
#
符号能够被绕过MethodAccessor.denyMethodExecution
值能够被修改
‘#’如何被绕过
- 扯扯
#
到底有啥用
#
在ONGL
表达式相关的漏洞中,通常用来访问非根对象(root)属性(调用root对象则不需要#
注明),这个根对象root
是valueStack
这个类,但valueStack
中存在一个CompoundRoot
类型的属性root
,实际上root
就是这个属性:
我们注意到该类中还包含了context
属性,他是structs2
为了方便开发者,而设置的,里面有一些自带的属性,以及运行存在的上下文。
这个context
不是根对象(root),因此我们可以通过#
进行调用,所以我们会发现一些payload
中存在这一句:
#context[\'xwork.MethodAccessor.denyMethodExecution\']
这就是为什么能够通过#
访问上下文的原因了,逻辑大概是这样#
访问非根属性,context
是非根属性,因此#
能够访问到上下文的几乎所有变量,而上下文存在的变量中有安全配置,因此我们可以人为修改安全配置从而达到绕过的目的,当然#
也可以用于简单的创建变量:
- 了解一下
structs2
的处理机制:
借用网图:
简单来说就是,Structs
请求会先由各种各样的过滤器进行处理,然后Dispatcher
类分发给代理类,代理类将请求分发给不同的Interceptor
进行二次处理,拦截器处理完毕而,再交给用户编写的Action
,最后在交给jsp
等模板进行处理,然后才是返回结果。
- 双括号的
ONGL
表达式语法是个啥
根据手册提供的例子来看,(aaa)(bbb)
语法等价于#aaa(bbb)
他的作用类似于将aaa的值赋值为bbb
而已,如果这个bbb
是一个表达式的话,ONGL
语法会尝试解析。而(aaa)(bbb)
其实一种防止歧义的写法而已,这个语法与单纯的赋值语句不同,他具有表达式评估
的功能,也就是动态解析。
#
号究竟是在哪里过滤的
Structs2
中获得参数是由ParametersInterceptor
类完成的,此类完整路径为xwork-core-2.1.6.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
,我们可以看到,在此处获得了URL
参数:
需要的是如果我们直接使用#
进行变量的定义会被过滤掉:
随便给一个带#
号的payload
:
('#hello')(vaaa)=true
我们发现实际上在获得参数之前,我们输入的恶意内容就已经经历过一次过滤了,那么在这之前又发生了什么,首先在获得值栈
之前,经过了一轮初始化操作:
这里将我们需要使用恶意payload
的一个条件DenyMethodExecution
设置为了true
,后面再使用setParmaters
进行参数的解析,我逐步向上溯源,发现在Filter
得到参数以前,#
就已经被过滤了:
因此我们继续向上查找,结合网上资料,推测在更高版本下的struts
在得到#
字符后便停止了对后面所有字符的解析,并且这层操作是在filter
之前完成的,当然也有可能和tomcat
版本有关,随后我换成了更老一点的tomcat6.0.8
版本发现仍然是同样的结果:
得出结论,在更高版本下的struts
会停止对#
后面任何字符的解析,并且这层处理先于过滤器和拦截器的处理,看来我们测试只能使用Unicode
编码了。
- 为什么能解析
unicode
编码
具体解析参数的地方在/home/ebounce/s2/s2-005/web/WEB-INF/lib/xwork-core-2.1.6.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class#setParameters
处:
继续跟进,在这里调用了ONGL
解析类:
出现了分析语法函数:
这个函数最后会调用lib/ognl-2.7.3.jar!/ognl/OgnlParser.class
中的topLebelExpression
函数,后面继续跟进后会对表达式进行AST
解析了,这里使用44
来标记(
:
随后会进行到/lib/ognl-2.7.3.jar!/ognl/JavaCharStream.class
的readChar
函数,这个函数会判断首字符为\
,然后判断下一位字符:
下一位字符是u
,即判断是unicode
格式:
拿到整体的Unicode
编码:
最后得到对应编码所示字符:
从而能够解析unicode
编码,也就导致对#
的限制被绕过了。
POC构造原理
为什么要修改
_memberAccess
_memberAccess
属性,本质上对应的是/lib/xwork-core-2.1.6.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.class
类,当ONGLValueStack
进行初始化时会调用setRoot
属性:
这里对securityMemberAccess
类的实例,进行了设置,表明allowStaticMethodAccess
为false
,即不允许调用静态方法,这个类算是struts
的全局安全配置类,最后会在OnglContext
中与_memberAccess
进行挂钩:
两者本质上是同一个类实例的不同名称,因此可以通过修改_memberAccess
的属性来修改全局配置,从而修改allowStaticMethodAcess
,达到调用静态方法的目的。
PS:
s2-003后加入了这一属性防止denyMethodExecution
被修改。
- 理解POC的构造
这一句:
('#_memberAccess['allowStaticMethodAccess']')(vaaa)=true
是将_memberAccess['allowStaticMethodAccess']
的值设置为true。
但可能看了上面双括号语法之后还是很难理解为什么,需要这样,实际上这和AST语法解析有关,ONGL以及绝大多数的编程语言都采用的AST解析算法,假如我们在ONGL中写入表达式(aaa)(bbb)=true
,我们假设()()
之间链接的运算符叫做解析赋值,=
叫做赋值,则根据AST生成,若两个运算符不存在优先级关系,则二叉树长这样:
我们使用后序遍历算法遍历二叉树(因为需要最后解析根节点,毕竟我们的表达式是二元运算符),解析结果如下:
由于最后使用解析赋值运算符,因此(bbb=true)
作为表达式将被解析赋值aaa=bbb=true
,从而成功解析出该表达式,所以我们会发现POC中存在很多很多奇奇怪怪的括号。
下一句的解析也是同样的道理,我们同样将这句POC表达式的二叉树形式表示出来:
(aaaa)(('#context[\'xwork.MethodAccessor.denyMethodExecution\']=#vccc')(#vccc=new java.lang.Boolean("false")))
遍历顺序大概是这样:
均遵循先是左子树,后右子树,最后根节点的顺序,从遍历顺序我们可以看出,这句POC作用为:
- 先拿到
context
中的denyMethodExecution
值,再找#vcc
的值,此时vcc
还没有值,遍历完左子树的右节点后,遍历右子树并返回根节点。 - 回到左子树的根节点解析赋值,让
denyMethodExecution
变成false
. - 再向上返回得到后面一个大括号整体为
denyMethodExecution
对应的对象,且值为false
- 最后将这个对象赋值给
aaa
后面那句同理,这里就不多赘述了,通过上面两句,能够保证我们能够调用静态方法改变denyMethodExecution
的值,从而允许我们使用类内部方法进行解析,从而保证后一句(asdf)(('#rt.exec("touch@/tmp/helloworld".split("@"))')(#rt=@java.lang.Runtime@getRuntime()))=1
命令执行能够完成解析,造成真正的命令执行。