S2-057漏洞原作者自述:如何利用自动化工具发现5个RCE

Track-SSG   ·   发表于 2018-08-27 12:18:38   ·   漏洞文章
译文声明

本文是翻译文章,文章原作者,文章来源:lgtm.com                                
原文地址:https://lgtm.com/blog/apache_struts_CVE-2018-11776
                           


译文仅供参考,具体内容表达以及含义原文为准

2018年4月,我向Apache Struts和Struts安全团队中报告了一个新的远程执行代码漏洞——CVE-2018-11776(S2-057),在做了某些配置的服务器上运行Struts,可以通过访问精心构造的URL来触发漏洞。
这一发现是我对Apache
Struts的持续安全性研究的一部分。在这篇文章中,我将介绍我发现漏洞的过程以及如何利用以前的漏洞信息来获取Struts内部工作的原理,创建封装Struts特定概念的QL查询。运行这些查询会高亮显示有问题代码的结果。这些工程都托管在GitHub上,后面我们也会向此存储库添加更多查询语句和库,以帮助Struts和其他项目的安全性研究。

 

映射攻击面

许多安全漏洞都涉及了从不受信任的源(例如,用户输入)流向某个特定位置(sink)的数据,并且数据采用了不安全的处理方式——例如,SQL查询,反序列化,还有一些其他解释型语言等等,QL可以轻松搜索此类漏洞。你只需要描述各种source和sink,然后让DataFlow库完成这些事情。对于特定项目,开始调查此类问题的一种好方法是查看旧版本软件的已知漏洞。
这可以深入了解你想要查找的source和sink点。

这次漏洞发现过程中,我首先查看了RCE漏洞S2-032(CVE-2016-3081),S2-033(CVE-2016-3687)和S2-037(CVE-2016-4438)。
与Struts中的许多其他RCE一样,这些RCE涉及不受信任的输入被转为OGNL表达式,允许攻击者在服务器上运行任意代码。
这三个漏洞特别有意思,它们不仅让我们对Struts的内部工作机制有了一些了解,而且这三个漏洞实际上是一样的,还修复了三回!

这三个问题都是远程输入通过变量methodName作为方法的参数传递的造成的
OgnlUtil::getValue().
![](https://p4.ssl.qhimg.com/t01ba64d4377a0d041f.png)

这里proxy有ActionProxy的类型,它是一个接口。
注意它的定义,除了方法getMethod()(在上面的代码中用于赋值的变量methodName)之外,还有各种方法,如getActionName()和getNamespace()。
这些方法看起来像是会从URL返回信息,所以我就假设所有这些方法都可能返回不受信任的输入。
(后面的文章中,我将深入研究我对这些输入来自何处的调查。)

现在使用QL开始对这些不受信任的源进行建模:

 

识别OGNL的 sink点

现在我们已经识别并描述了一些不受信任的来源,下一步是为sink点做同样的事情。 如前所述,许多Struts
RCE涉及将远程输入解析为OGNL表达式。 Struts中有许多函数最终将它们的参数作为OGNL表达式;
对于我们在本文中开始的三个漏洞,使用了OgnlUtil ::
getValue(),但是在漏洞S2-045(CVE-2017-5638)中,使用了TextParseUtil ::
translateVariables()。 我们可以寻找用于执行OGNL表达式的常用函数,我感觉OgnlUtil ::
compileAndExecute()和OgnlUtl :: compileAndExecuteMethod()看起来更有戏。

我的描述:

 

第一次尝试

现在我们已经在QL中定义了source和sink,我们可以在污点跟踪查询中使用这些定义。 通过定义DataFlow配置来使用DataFlow库:

这里是我使用之前定义的isActionProxySource和isOgnlSink。

注意一下,我这里重载了isAdditionalFlowStep,这样它可以允许我包含污染数据被传播的额外步骤。
比如允许我将特定于项目的信息合并到流配置中。
例如,如果我有通过某个网络层进行通信的组件,我可以在QL中描述那些各种网络端的代码是什么样的,允许DataFlow库跟踪被污染的数据。

对于此特定查询,我添加了两个额外的流程步骤供DataFlow库使用。 第一个:


它包括跟踪标准Java库调用,字符串操作等的标准QL TaintTracking库步骤。第二个添加是一个近似值,允许我通过字段访问跟踪污点数据:

也就是说如果将字段赋了某个受污染的值,那么只要两个表达式都由相同类型的方法调用,对该字段的访问也将被视为污染。看下面的例子:
从上面看出,bar()中this.field的访问可能并不总是受到污染。
例如,如果在bar()之前未调用foo()。 因此,我们不会在默认的DataFlow ::
Configuration中包含这个步骤,因为无法保证数据始终以这种方式流动,但是,对于挖漏洞,我觉得加上这个很有用。在后面的帖子中,我将分享一些类似于这个的其他流程步骤,这些步骤对于找bug很有帮助,但由于类似的原因,默认情况下是不包含这些步骤的。

 

初始结果和细化查询

我在最新版本的源代码上跑了一下QL,发现因S2-032,S2-033和S2-037仍然被标记了。 这些漏洞明明已经被修复了,为什么还是会报问题呢?

经过分析,我们觉得应该是虽然最初通过过滤输入来修复漏洞,但是在S2-037之后,Struts团队决定通过调用OgnlUtil :: getMalue()替换对OgnlUtil :: getMalue()的调用来修复它。


callMethod()封装了compileAndExecuteMethod():

compileAndExecuteMethod()在执行之前对表达式执行额外检查:

这意味着我们实际上可以从我们的sink点中删除compileAndExecuteMethod()。

在重新运行查询后,高亮显示对getMethod()作为sink的调用的结果消失了。
但是,仍然有一些结果高亮显示了DefaultActionInvocation.java中的代码,这些代码被认为是固定的,例如对getActionName()的调用,并且数据路径从此处到compileAndExecute()并不是很明显。

 

路径探索和进一步查询细化

为了搞清楚为什么这个结果被标记,需要能够看到DataFlow库用来产生这个结果的每个步骤。 QL允许编写特殊的路径问题查询,这些查询可生成可逐节点探索的可变长度路径,DataFlow库允许编写输出此数据的查询。

在撰写这篇博客的时候,LGTM本身没有关于路径问题查询的路径探索UI,因此用了另一个Semmle应用程序:QL for
Eclipse。这是一个Eclipse插件,刚好可以满足我们这里的需求,允许完成污点跟踪中的各个步骤。它不仅可以在LGTM.com上对开源项目进行离线分析,还可以为提供更强大的开发环境。可以在semmle-security-java目录下的Semmle
/ SecurityQueries
Git存储库中找到以下查询。按照README.md文件中的说明在Eclipse插件中运行。下文将贴出部分运行的截图。

首先,在initial.ql中运行查询。在QL for Eclipse中,从DefaultActionInvocation.java中选择结果后,您可以在Path Explorer窗口中看到从源到接收器的详细路径。

在上图中可以看出,经过几个步骤后,调用getActionName()返回的值会流入到pkg.getActionConfigs()返回的对象的get()方法的参数中:

点击下一步,key到了ValueStackShadowMap :: get()方法:

事实证明,因为pkg.getActionConfigs()返回一个Map,而ValueStackShadowMap实现了Map接口,所以理论上pkg.getActionConfigs()返回的值可能是ValueStackShadowMap的一个实例。
因此,QL DataFlow库显示了从变量chainedTo到类ValueStackShadowMap中的get()实现的潜在流程。
实际上,ValueStackShadowMap类属于jasperreports插件,该类的实例仅在几个地方创建。因此我觉得问题应该不在ValueStackShadowMap
:: get(),我通过在DataFlow :: Configuration中添加一个barrier来排除这种结果:

这里的意思是如果污染数据流入ValueStackShadowMap的get()或containsKey()方法,那么就不要继续跟踪它。 (我在这里添加了containsKey()方法,因为它也有同样的问题。)

又为ActionMapping :: toString()添加了barrier之后(因为在任意对象上调用toString()时出问题),重新运行查询,只留下了部分结果。 当然你也可以尝试使用Eclipse插件来显示污点路径。

 

发现漏洞

只有10对source和sink,很容易通过手工检查这些是否是真正的问题。 通过一些路径,看出有些路径是无效的,所以我又在查询中添加了一些barrier来过滤掉这些路径。 最后的结果比较有意思。
以ServletActionRedirectResult.java中的源代码为例:


在第一步中,调用getNamespace()的source通过变量名称空间流入ActionMapping构造函数的参数中:

继续跟这些步骤,看到getUriFromActionMapping()返回一个URL字符串,该字符串使用构造的ActionMapping中的命名空间。 然后通过变量tmpLocation流入setLocation()的参数:
然后setLocation()在超类StrutsResultSupport中设置location:

然后代码在ServletActionResult上调用execute():

将location字段传递给对conditionalParse():

conditionalParse()然后将location传递给translateVariables(),它将param转化为引擎盖下的OGNL表达式:

所以看起来当在ServletActionRedirectResult中没有设置namespace参数时,代码从ActionProxy获取命名空间,然后将其作为OGNL表达式。

为了验证这个想法,我通过以下方法替换了showcase应用程序中的一个配置文件(例如struts-actionchaining.xml)中的struts标记:

然后我在本地运行showcase应用程序,访问了一个旨在触发此漏洞的URL并执行shell命令以在我的计算机上打开计算器应用程序。

弹出计算器了(中间还花了一些时间绕过OGNL沙箱)。现在我暂时不提供进一步的细节,后面会发出来。

不单单这一处,来自ActionChainResult,PostbackResult和ServletUrlRenderer的不可信来源都能弹出计算器!
PortletActionRedirectResult中的那个可能也可以,但我没有测试。 四个RCE足以证明问题的严重性。

 



打赏我,让我更有动力~

0 条回复   |  直到 2018-8-27 | 1198 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.