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足以证明问题的严重性。

 

结论

在这篇文章中,我已经展示了通过使用已知(过去)的漏洞来帮助构建应用程序的污点模型,然后由 QL DataFlow库找新的漏洞。特别是通过研究Struts中之前的三个RCE,最终找到了四个(也可能是五个)!

鉴于S2-032,S2-033和S2-037都是在短时间内被发现和修复的,安全研究人员清楚地研究了S2-032用以寻找类似问题并发现S2-033和S2-037。这里就有问题了:我发现的漏洞(S2-057)也来自类似的污染源,为什么安全研究人员和供应商之前没发现?在我看来,这是因为S2-032,S2-033和S2-037之间的相似性在某种意义上是局部的,因为它们都出现在源代码中的相似位置(全部在Rest插件中)。 S2-057和S2-032之间的相似性处于更加语义的层面。它们由受污染的源链接,而不是源代码的位置,因此任何能够成功找到这样的变体的软件或工具都需要能够在整个代码库中执行这种语义分析,就像我现在可演示的QL。

如果你认为我的这些发现只是运气好,因为我假设ActionProxy中的命名空间字段已经被污染了,那么请继续关注下一篇文章,我会展示更多的细节问题,并从传入的HttpRequestServlet本身开始,从“第一原则”开始进行一些污点跟踪。我还将从我的“漏洞狩猎工具箱”中分享一些工具,以及一些改进查询的一般提示。在这样做的过程中,QL还捕获了漏洞S2-045!