Structs2-005分析&复现
Ebounce
撰写于 2020年 11月 28 日

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没有回显:

但我们可以看到确实创建成功该文件。

漏洞分析

首先根据漏洞信息:

要点有以下两个

  1. #符号能够被绕过
  2. MethodAccessor.denyMethodExecution值能够被修改

‘#’如何被绕过

  1. 扯扯#到底有啥用

#ONGL表达式相关的漏洞中,通常用来访问非根对象(root)属性(调用root对象则不需要#注明),这个根对象rootvalueStack这个类,但valueStack中存在一个CompoundRoot类型的属性root,实际上root就是这个属性:

我们注意到该类中还包含了context属性,他是structs2为了方便开发者,而设置的,里面有一些自带的属性,以及运行存在的上下文。

这个context不是根对象(root),因此我们可以通过#进行调用,所以我们会发现一些payload中存在这一句:

#context[\'xwork.MethodAccessor.denyMethodExecution\']

这就是为什么能够通过#访问上下文的原因了,逻辑大概是这样#访问非根属性,context是非根属性,因此#能够访问到上下文的几乎所有变量,而上下文存在的变量中有安全配置,因此我们可以人为修改安全配置从而达到绕过的目的,当然#也可以用于简单的创建变量:

  1. 了解一下structs2的处理机制:

借用网图:

简单来说就是,Structs请求会先由各种各样的过滤器进行处理,然后Dispatcher类分发给代理类,代理类将请求分发给不同的Interceptor进行二次处理,拦截器处理完毕而,再交给用户编写的Action,最后在交给jsp等模板进行处理,然后才是返回结果。

  1. 双括号的ONGL表达式语法是个啥

根据手册提供的例子来看,(aaa)(bbb)语法等价于#aaa(bbb)他的作用类似于将aaa的值赋值为bbb而已,如果这个bbb是一个表达式的话,ONGL语法会尝试解析。而(aaa)(bbb)其实一种防止歧义的写法而已,这个语法与单纯的赋值语句不同,他具有表达式评估的功能,也就是动态解析

  1. #号究竟是在哪里过滤的

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编码了。

  1. 为什么能解析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.classreadChar函数,这个函数会判断首字符为\,然后判断下一位字符:

下一位字符是u,即判断是unicode格式:

拿到整体的Unicode编码:

最后得到对应编码所示字符:

从而能够解析unicode编码,也就导致对#的限制被绕过了。

POC构造原理

  1. 为什么要修改_memberAccess

    _memberAccess属性,本质上对应的是/lib/xwork-core-2.1.6.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.class类,当ONGLValueStack进行初始化时会调用setRoot属性:

这里对securityMemberAccess类的实例,进行了设置,表明allowStaticMethodAccessfalse,即不允许调用静态方法,这个类算是struts的全局安全配置类,最后会在OnglContext中与_memberAccess进行挂钩:

两者本质上是同一个类实例的不同名称,因此可以通过修改_memberAccess的属性来修改全局配置,从而修改allowStaticMethodAcess,达到调用静态方法的目的。

PS:

s2-003后加入了这一属性防止denyMethodExecution被修改。

  1. 理解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作用为:

  1. 先拿到context中的denyMethodExecution值,再找#vcc的值,此时vcc还没有值,遍历完左子树的右节点后,遍历右子树并返回根节点。
  2. 回到左子树的根节点解析赋值,让denyMethodExecution变成false.
  3. 再向上返回得到后面一个大括号整体为denyMethodExecution对应的对象,且值为false
  4. 最后将这个对象赋值给aaa

后面那句同理,这里就不多赘述了,通过上面两句,能够保证我们能够调用静态方法改变denyMethodExecution的值,从而允许我们使用类内部方法进行解析,从而保证后一句(asdf)(('#rt.exec("touch@/tmp/helloworld".split("@"))')(#rt=@java.lang.Runtime@getRuntime()))=1命令执行能够完成解析,造成真正的命令执行。

参考链接

Struts2漏洞分析与研究之S2-005漏洞分析

【Struts2-命令-代码执行漏洞分析系列】S2-003和S3-005

浅析 OGNL 的攻防史

Structs2-005分析&复现

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没有回显:

但我们可以看到确实创建成功该文件。

漏洞分析

首先根据漏洞信息:

要点有以下两个

  1. #符号能够被绕过
  2. MethodAccessor.denyMethodExecution值能够被修改

‘#’如何被绕过

  1. 扯扯#到底有啥用

#ONGL表达式相关的漏洞中,通常用来访问非根对象(root)属性(调用root对象则不需要#注明),这个根对象rootvalueStack这个类,但valueStack中存在一个CompoundRoot类型的属性root,实际上root就是这个属性:

我们注意到该类中还包含了context属性,他是structs2为了方便开发者,而设置的,里面有一些自带的属性,以及运行存在的上下文。

这个context不是根对象(root),因此我们可以通过#进行调用,所以我们会发现一些payload中存在这一句:

#context[\'xwork.MethodAccessor.denyMethodExecution\']

这就是为什么能够通过#访问上下文的原因了,逻辑大概是这样#访问非根属性,context是非根属性,因此#能够访问到上下文的几乎所有变量,而上下文存在的变量中有安全配置,因此我们可以人为修改安全配置从而达到绕过的目的,当然#也可以用于简单的创建变量:

  1. 了解一下structs2的处理机制:

借用网图:

简单来说就是,Structs请求会先由各种各样的过滤器进行处理,然后Dispatcher类分发给代理类,代理类将请求分发给不同的Interceptor进行二次处理,拦截器处理完毕而,再交给用户编写的Action,最后在交给jsp等模板进行处理,然后才是返回结果。

  1. 双括号的ONGL表达式语法是个啥

根据手册提供的例子来看,(aaa)(bbb)语法等价于#aaa(bbb)他的作用类似于将aaa的值赋值为bbb而已,如果这个bbb是一个表达式的话,ONGL语法会尝试解析。而(aaa)(bbb)其实一种防止歧义的写法而已,这个语法与单纯的赋值语句不同,他具有表达式评估的功能,也就是动态解析

  1. #号究竟是在哪里过滤的

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编码了。

  1. 为什么能解析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.classreadChar函数,这个函数会判断首字符为\,然后判断下一位字符:

下一位字符是u,即判断是unicode格式:

拿到整体的Unicode编码:

最后得到对应编码所示字符:

从而能够解析unicode编码,也就导致对#的限制被绕过了。

POC构造原理

  1. 为什么要修改_memberAccess

    _memberAccess属性,本质上对应的是/lib/xwork-core-2.1.6.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.class类,当ONGLValueStack进行初始化时会调用setRoot属性:

这里对securityMemberAccess类的实例,进行了设置,表明allowStaticMethodAccessfalse,即不允许调用静态方法,这个类算是struts的全局安全配置类,最后会在OnglContext中与_memberAccess进行挂钩:

两者本质上是同一个类实例的不同名称,因此可以通过修改_memberAccess的属性来修改全局配置,从而修改allowStaticMethodAcess,达到调用静态方法的目的。

PS:

s2-003后加入了这一属性防止denyMethodExecution被修改。

  1. 理解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作用为:

  1. 先拿到context中的denyMethodExecution值,再找#vcc的值,此时vcc还没有值,遍历完左子树的右节点后,遍历右子树并返回根节点。
  2. 回到左子树的根节点解析赋值,让denyMethodExecution变成false.
  3. 再向上返回得到后面一个大括号整体为denyMethodExecution对应的对象,且值为false
  4. 最后将这个对象赋值给aaa

后面那句同理,这里就不多赘述了,通过上面两句,能够保证我们能够调用静态方法改变denyMethodExecution的值,从而允许我们使用类内部方法进行解析,从而保证后一句(asdf)(('#rt.exec("touch@/tmp/helloworld".split("@"))')(#rt=@java.lang.Runtime@getRuntime()))=1命令执行能够完成解析,造成真正的命令执行。

参考链接

Struts2漏洞分析与研究之S2-005漏洞分析

【Struts2-命令-代码执行漏洞分析系列】S2-003和S3-005

浅析 OGNL 的攻防史

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论