CVE-2018-11776: 如何使用CodeQL发现5个 Apache Struts RCEs

admin 2022年1月11日11:22:35评论137 views字数 10296阅读34分19秒阅读模式

August22, 2018

这是一篇翻译,我觉得很好,就翻一下 分享给大家。

20184月,我向Struts安全团队和Apache Struts安全团队报告了一个新的远程代码执行漏洞。该漏洞已被分配为CVE-2018-11776(S2-057),在某些配置下运行Struts的服务器上暴露,通过访问巧尽心思构建的URL可以触发该漏洞。有关Struts的哪些版本和配置受到影响、缓解步骤和公开过程的详细信息,请参阅公告博客文章

这个发现是我对apache struts的持续安全性研究的一部分。在这篇文章中,我将介绍引导我找到漏洞的过程。我将解释如何使用以前的漏洞信息来了解Struts的内部工作原理,并创建封装Struts特定概念的查询。运行这些查询会产生突出显示问题代码的结果。这些查询托管在GitHub上,随着时间的推移,随着研究的继续,我们将向这个存储库添加更多的查询和库,以帮助Struts和其他项目的安全性研究。

绘制攻击面

许多安全漏洞涉及来自不可信源的数据流例如,用户输入到某个特定位置(sink),在该位置以危险的方式使用数据例如,SQL查询、反序列化、某些其他解释语言等...CodeQL使搜索此类漏洞变得容易。您只需描述各种源和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()的结果。

String methodName =proxy.getMethod();    //<--- untrusted source, but where from?LOG.debug("Executing action method = {}", methodName);String timerKey = "invokeAction: " + proxy.getActionName();try {    UtilTimerStack.push(timerKey);    Object methodResult;    try {        methodResult =ognlUtil.getValue(methodName + "()", getStack().getContext(), action); //<--- RCE


这里的proxy字段具有ActionProxy类型,它是一个接口。看看它的定义,我发现除了getMethod()方法(在上面的代码中用于指定受污染的变量methodName)之外,还有各种方法,比如getActionName()getNamespace()。这些方法看起来好像可以从URL返回信息。所以我开始假设所有这些方法都可能返回不可信的输入。(在后面的文章中,我将更深入地研究这些输入的来源。)

现在,我们可以使用CodeQL的语言QL:

class ActionProxyGetMethod extends Method {  ActionProxyGetMethod() {    getDeclaringType().getASupertype*().hasQualifiedName("com.opensymphony.xwork2", "ActionProxy") and    (      hasName("getMethod") or      hasName("getNamespace") or      hasName("getActionName")    )  }} predicateisActionProxySource(DataFlow::Nodesource) {   source.asExpr().(MethodAccess).getMethod() instanceofActionProxyGetMethod}


识别OGNL sink

既然我们已经识别并描述了一些不可信的源代码,下一步就是对sink执行同样的操作。如前所述,许多Struts rce都涉及将远程输入解析为OGNL表达式。Struts中有许多函数最终将参数作为OGNL表达式进行计算;对于我们在本文中开始的三个漏洞,使用了OgnlUtil::getValue(),但是在漏洞S2-045(CVE-2017-5638)中,使用了TextParseUtil::translateVariables()。我们不需要将这些方法中的每一个描述为单独的sink,而是寻找它们用来执行OGNL表达式的通用函数。我认为OgnlUtil::compileAndExecute()OgnlUtl::compileAndExecuteMethod()看起来像是很有前途的sink

我用一个QL谓词描述了它们,如下所示:

predicate isOgnlSink(DataFlow::Nodesink) {  exists(MethodAccessma | ma.getMethod().hasName("compileAndExecute") or ma.getMethod().hasName("compileAndExecuteMethod") |    ma.getMethod().getDeclaringType().getName().matches("OgnlUtil") and    sink.asExpr() = ma.getArgument(0)  )}


首次尝试跟踪污点

现在我们已经用CodeQL定义了源和汇,我们可以在污点跟踪查询中使用这些定义。通过定义数据流配置,我们使用数据流库来实现这一点:

class OgnlTaintTrackingCfg extends DataFlow::Configuration {  OgnlTaintTrackingCfg() {    this = "mapping"  }
override predicate isSource(DataFlow::Node source) { isActionProxySource(source) }
override predicate isSink(DataFlow::Node sink) { isOgnlSink(sink) }
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { TaintTracking::localTaintStep(node1, node2) or exists(Field f, RefType t | node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess() and node1.asExpr().getEnclosingCallable().getDeclaringType() = t and node2.asExpr().getEnclosingCallable().getDeclaringType() = t ) }}
from OgnlTaintTrackingCfg cfg, DataFlow::Node source, DataFlow::Node sinkwhere cfg.hasFlow(source, sink)select source, sink


这里我使用前面定义的isActionProxySourceisOgnlSink谓词。

请注意,我还重写了一个名为isAdditionalFlowStep的谓词。这个谓词允许我包含一些额外的步骤,在这些步骤中可以传播受污染的数据。例如,这允许我在流配置中合并特定于项目的信息。例如,如果我有通过某个网络层进行通信的组件,我可以用CodeQL描述这些不同网络端点的代码是什么样子的,从而允许数据流库通过该抽象跟踪被污染的数据。

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

TaintTracking::localTaintStep(node1,node2)


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

exists(Field f, RefType t |node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess()and  node1.asExpr().getEnclosingCallable().getDeclaringType()= t and  node2.asExpr().getEnclosingCallable().getDeclaringType()= t)


这意味着,如果一个字段被分配给某个受污染的值,那么对该字段的访问也将被视为受污染,只要这两个表达式都是由同一类型的方法调用的。粗略地说,这包括以下情况:

public void foo(String taint) {  this.field = taint;}
public void bar() { String x = this.field; //x is tainted because field is assigned to tainted value in `foo`}


如你所见这个bar()里的this.field 不一定总是被污染。例如,如果在bar()之前没有调用foo()。因此,我们没有在默认的DataFlow::Configuration中包含这个流步骤,因为我们不能保证数据总是以这种方式流动。但是,对于查找漏洞,我发现这个添加很有用,并且经常将它们包含在我的DataFlow::Configuration中。在后面的文章中,我将分享一些类似于这一步的其他流程步骤,这些步骤对于bug搜索很有用,但是由于类似的原因,默认情况下不包含这些步骤。

初始结果和查询优化

在对源代码的最新版本运行查询并开始查看结果后,我注意到问题S2-032S2-033S2-037的原因仍然被查询标记。在研究它发现的其他结果之前,我想研究为什么这些特定的结果仍然被标记,即使代码是固定的。

我们发现,虽然最初通过清理输入来修复第一个漏洞,但在S2-037之后,Struts团队决定通过将对OgnlUtil::getValue()的调用替换为对OgnlUtil::callMethod()的调用来修复它。

methodResult =ognlUtil.callMethod(methodName + "()", getStack().getContext(), action);


方法callMethod()包装对compileAndExecuteMethod()的调用:

public Object callMethod(final String name, final Map<String, Object> context, final Object root) throws OgnlException {  return compileAndExecuteMethod(name, context, new OgnlTask<Object>() {    public Object execute(Object tree) throws OgnlException {      return Ognl.getValue(tree, context, root);    }  });}


并且compileAndExecuteMethod()在执行表达式之前对其执行附加检查:

private <T> Object compileAndExecuteMethod(String expression, Map<String, Object> context,OgnlTask<T> task) throws OgnlException {  Object tree;  if (enableExpressionCache) {    tree = expressions.get(expression);    if (tree == null) {      tree = Ognl.parseExpression(expression);      checkSimpleMethod(tree, context); //<--- Additional check.    }


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

您可以看到,在我重新运行查询之后,突出显示对getMethod()的调用作为sink的结果消失了。然而,仍然有一些结果突出显示了Def中的代码java战术导航这应该已经修复了,比如对getActionName()的调用,而且从这里到compileAndExecute() sink的数据路径并不明显。

路径探索和进一步的查询优化

为了调查为什么标记此结果,我需要能够看到数据流库用于生成此结果的每个单独的流步骤。CodeQL允许您编写特殊的路径问题查询,这些查询生成可以逐个节点探索的可变长度路径,而DataFlow库允许您编写输出这些数据的查询。

在写这篇博文时,LGTM本身没有用于路径问题查询的路径探索UI,所以我需要使用CodeQL for Eclipse。这是一个Eclipse插件,它包含了一个可视化工具,它允许您完成污点跟踪中的各个步骤。您可以按照这里的说明免费下载并安装这个Eclipse插件。它不仅允许离线分析开源项目LGTM.com网站,同时也为您提供了一个更强大的开发环境。以下查询可以在Semmle security java目录下的Semmle/SecurityQueriesGit存储库中找到。您可以按照中的说明操作自述文件.mdEclipse插件中运行它们的文件。从这里开始,我将包括CodeQL for Eclipse的屏幕截图。

[编辑]:路径探索现在在LGTM上可用。您也可以使用我们的免费CodeQL扩展来实现visualstudio代码。参见安装说明https://securitylab.github.com/tools/codeql.

首先,在中运行查询首字母.ql。在CodeQL for Eclipse中,在从Def中选择结果时aultActionInvocation.java,您可以在路径资源管理器窗口中查看从源到sink的详细路径。

 

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

String chainedTo = actionName +nameSeparator + resultCode; //actionName comes from `getActionName`somewhereActionConfig chainedToConfig =pkg.getActionConfigs().get(chainedTo); //chainedTo contains `actionName` and ended up in the `get`method.


单击下一步key,将我带到ValueStackShadowMap::get()方法,它看起来像:

public Object get(Object key) {  Object value = super.get(key);  //<---key gets tainted?   if ((value == null) && key instanceof String) {    value = valueStack.findValue((String)key);  //<--- findValue ended up evaluating `key`  }   return value;}


原来是因为pkg.getActionConfigs()返回一个mapvalueStackShadowMap实现map接口,理论上,返回的值可能pkg.getActionConfigs()ValueStackShadowMap的实例。因此,CodeQL数据流库显示了从变量chainedTo到类ValueStackShadowMapget()实现的潜在流。实际上,类ValueStackShadowMap属于jasperreports插件,该类的实例只在几个地方创建,没有一个由pkg.getActionConfigs(). 在研究了这个问题并说服自己ValueStackShadowMap::get()不太可能被命中之后,我通过在DataFlow::Configuration中添加一个屏障来删除依赖它的结果:

override predicate isBarrier(DataFlow::Nodenode) {  exists(Methodm | (m.hasName("get") or m.hasName("containsKey")) and    m.getDeclaringType().hasName("ValueStackShadowMap") and    node.getEnclosingCallable() = m  )}


这个谓词表示,如果污染数据流入ValueStackShadowMapget()containsKey()方法,则不要继续跟踪它。(我在这里添加了containsKey()方法,因为它遇到了相同的问题。)

在为ActionMapping::toString()添加了一个进一步的屏障(每当对任意对象调用toString()时都会导致问题),我重新运行查询,结果只剩下少数几个结果。您也可以尝试使用Eclipse插件来可视化污染路径。

新的漏洞

由于只有10对源和汇,很容易手工检查并检查这些是否是真正的问题。通过一些路径,我发现有些路径是无效的,因为它们在测试用例中等等,所以我在查询中添加了一些屏障来过滤掉这些路径。这留下了一些非常有趣的结果。

获取Servlet中的源代码ActionRedirectResult.java例如:

 

在第一步中,调用getNamespace()的源通过变量namespace流入ActionMapping构造函数的参数:

public void execute(ActionInvocation invocation) throws Exception {  actionName = conditionalParse(actionName,invocation);  if (namespace == null) {    namespace = invocation.getProxy().getNamespace();  //<---source  } else {    namespace = conditionalParse(namespace,invocation);  }  if (method == null) {    method = "";  } else {    method = conditionalParse(method,invocation);  }   String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace,method, null)); //<--- namespace goes intoconstructor of ActionMapping   setLocation(tmpLocation);


 

按照进一步的步骤,我看到getUriFromActionMapping()返回一个URL字符串,该字符串使用构造的ActionMapping中的命名空间。然后,它通过变量tmpLocation流入setLocation()的参数:

setStrutsSupport类中设置位置:

public voidset Location(String location) {    this.location = location;}


然后,代码对ServletActionResult调用execute()

String tmpLocation =actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null)); setLocation(tmpLocation); super.execute(invocation);


它将位置字段传递给conditionalParse()的调用:

public void execute(ActionInvocation invocation) throws Exception {    lastFinalLocation =conditionalParse(location, invocation);    doExecute(lastFinalLocation, invocation);}


conditionalParse()然后将位置传递给translateVariables(),后者在幕后将paramas计算为一个OGNL表达式:

protected String conditionalParse(String param, ActionInvocation invocation) {    if (parse && param != null && invocation != null) {        returnTextParseUtil.translateVariables(            param,            invocation.getStack(),            newEncodingParsedValueEvaluator());    } else {        return param;    }}


因此,当ServletActionRedirectResult中没有设置namespace参数时,代码从ActionProxy获取名称空间,然后将其作为OGNL表达式求值。为了测试这一点,我在一个配置文件(struts)中替换了struts- actionchaining.xml例如)showcase应用程序中执行以下操作:

<struts>    <packagename="actionchaining"extends="struts-default">        <actionname="actionChain1"class="org.apache.struts2.showcase.actionchaining.ActionChain1">           <resulttype="redirectAction">             <paramname = "actionName">register2</param>           </result>        </action>    </package></struts> 

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

它成功了(在花了一段时间绕过OGNL沙盒之后)。现阶段,我暂缓透露更多细节,但我会在适当的时候公布。

不仅如此,来自ActionChainResultPostbackResultServletUrlRenderer的不受信任的源代码也可以工作!PortletActionRedirectResult中的那个可能也可以,但我没有测试它。四个RCE可能足以证明问题的严重性。

结论

在这篇文章中,我展示了通过使用已知的(过去的)漏洞来帮助构建应用程序的污点模型,只需将繁重的工作留给CodeQL数据流库,就可以发现新的漏洞。特别是,通过研究Struts中之前的三个rce,我们最终发现了另外四个(可能是五个)

考虑到S2-032S2-033S2-037都是在短时间内被发现和修复的,安全研究人员明确研究了S2-032以寻找类似的问题,并发现了S2-033S2-037。所以这里最大的问题是:鉴于我在这里发现的漏洞(S2-057)也来自一个类似的污染源,安全研究人员和供应商怎么会错过这个漏洞,而在两年后才发现呢?在我看来,这是因为S2-032S2-033S2-037之间的相似性在某种意义上是局部的,因为它们都出现在源代码中相似的位置(都在Rest插件中)S2-057S2-032之间的相似性在语义层面上要大得多。它们是由受污染的源代码链接的,而不是源代码的位置,因此任何能够成功找到类似变体的软件或工具都需要能够在整个代码库中执行这种语义分析,正如我所演示的,现在可以用CodeQL来完成。

如果你认为这一切更像是侥幸或是哥伦布的蛋,因为我假设ActionProxy中的名称空间字段是突然被污染的,那么请继续关注下一篇文章,我将从第一原则开始,从传入的HttpRequestServlet本身开始,做一些污点跟踪。我还将分享我的“bug搜寻工具箱中的一些工具,以及一些改进查询的一般技巧。在此过程中,我们还将看到如何使用CodeQL来捕获S2-045Equifax所遭受的漏洞!

 


本文始发于微信公众号(xsser的博客):CVE-2018-11776: 如何使用CodeQL发现5个 Apache Struts RCEs

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月11日11:22:35
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2018-11776: 如何使用CodeQL发现5个 Apache Struts RCEshttps://cn-sec.com/archives/481866.html

发表评论

匿名网友 填写信息