CVE-2018-11776:如何通过Semmle QL找到Apache Struts的远程执行代码漏洞

媒介

2018年4月,一个新的Apache Struts远程代码履行破绽被申报。在Struts特定设置设置设备摆设摆设下,造访特制的URL可以触发该破绽。破绽编号为CVE-2018-11776(S2-057)。本文将先容发明破绽的历程。

映射进击面

许多破绽涉及从不受相信的源(例如,用户输入)流向某个特定位置的数据,这种环境下数据会以危险的要领被使用(SQL查询,反序列化以及一些其他解释说话等等)。对付特定项目,开始动手钻研此类问题的一种好措施是查看旧版本软件的已知破绽。这可以让你深入懂得所想要查找的各类源及接管点。

在本案例中,作者首先查看了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();//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); //

这里的proxy字段是ActionProxy类型,它是一个接口。在查看它的定义时,作者发明除了措施getMethod()(在上面的代码顶用于赋值的变量methodName)之外,还有各类其他的措施,如getActionName()和getNamespace()。这些措施貌似会从URL返复书息。以是作者开始假设所有这些措施都可能返回不受相信的输入。

现在可以应用QL开始对这些不受相信的滥觞进行建模:

class ActionProxyGetMethod extends Method {

ActionProxyGetMethod() {

getDeclaringType().getASupertype*().hasQualifiedName(“com.opensymphony.xwork2”, “ActionProxy”) and

(

hasName(“getMethod”) or

hasName(“getNamespace”) or

hasName(“getActionName”)

)

}

}

predicate isActionProxySource(DataFlow::Node source) {

source.asExpr().(MethodAccess).getMethod() instanceof ActionProxyGetMethod

}

识别OGNL接管点

现在已经识别并描述了一些不受相信的滥觞,下一步是为接管点做同样的工作。如前所述,许多Struts RCE涉及将远程输入解析为OGNL表达式。Struts中有许多函数终极将它们的参数作为OGNL表达式进行评估;对付在本文中开始的三个破绽,都应用了OgnlUtil::getValue(),然则在破绽S2-045(CVE-2017-5638)中,应用了TextParseUtil::translateVariables()。我们可以探求用于履行OGNL表达式的函数,而不是将每一个措施描述为QL中的零丁接管点。而OgnlUtil::compileAndExecute()和OgnlUtl::compileAndExecuteMethod()看起来像有盼望的接管点。

作者在一个QL predicate中描述了它们,如下所示:

predicate isOgnlSink(DataFlow::Node sink) {

exists(MethodAccess ma | ma.getMethod().hasName(“compileAndExecute”) or ma.getMethod().hasName(“compileAndExecuteMethod”) |

ma.getMethod().getDeclaringType().getName().matches(“OgnlUtil”) and

sink.asExpr() = ma.getArgument(0)

)

}

第一次考试测验污点跟踪

现在已经在QL中定义了源和接管点,以是可以在污点跟踪查询中应用这些定义。 这里作者应用DataFlow库来定义DataFlow设置设置设备摆设摆设:

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 sink

where cfg.hasFlow(source, sink)

select source, sink

这里作者应用了之前定义的isActionProxySource和isOgnlSink predicates。

必要留意的是,作者还覆盖了一个名为isAdditionalFlowStep的predicate。这个predicate容许包孕可以传播不法数据的额外步骤。例如,这容许将项目特定的信息合并到流设置设置设备摆设摆设中。再如,假如有经由过程某个收集层进行通信的组件,则可以在QL中描述那些各类收集端点的代码,容许DataFlow库跟踪不法数据。

对付此特定查询,作者添加了两个额外的流程步骤供DataFlow库应用。 第一个:

TaintTracking::localTaintStep(node1, node2)

[1] [2] [3]下一页

包孕标准QL TaintTracking库来跟踪标准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不法,由于字段被分配给`foo`中的不法值

}

如你所见,bar()中this.field的造访可能并不老是不法。例如,假如在bar()之前未调用foo()。那么不会在默认的DataFlow::Configuration中包孕此流程步骤,由于我们无法包管数据始终以这种要领流动。然则,对付找到破绽,将它们包孕在DataFlow::Configuration中就很有用。

初始结果和查询细化

在最新版本的源代码上运行查询,并开始反省结果,S2-032,S2-033和S2-037仍旧被查询标记。在查看其他结果之前,先查询造访为什么纵然代码已修复,这些特定的结果仍旧被标记。

虽然最初经由过程过滤输入来修复第一个破绽,然则在S2-037之后,Struts团队抉择经由过程调用OgnlUtil::getMalue()调换对OgnlUtil::getMalue()的调用来修复破绽。

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

措施callMethod()封装对compileAndExecuteMethod()的调用:

public Object callMethod(final String name, final MapString, Object> context, final Object root) throws OgnlException {

return compileAndExecuteMethod(name, context, new OgnlTaskObject>() {

public Object execute(Object tree) throws OgnlException {

return Ognl.getValue(tree, context, root);

}

});

}

并且compileAndExecuteMethod()在履行之前对表达式履行额外反省:

privateObject compileAndExecuteMethod(String expression, MapString, Object> context, OgnlTask task) throws OgnlException {

Object tree;

if (enableExpressionCache) {

tree = expressions.get(expression);

if (tree == null) {

tree = Ognl.parseExpression(expression);

checkSimpleMethod(tree, context); //额外反省

}

这意味着我们实际上可以从接管点中移除compileAndExecuteMethod()。

在从新运行查询后,高亮的getMethod()作为接管点的调用的结果消掉了。然则,仍旧有一些结果凸起显示了DefaultActionInvocation.java中的代码,这些代码被觉得是已经被修复的,例如对getActionName()的调用,并且这里的数据路径并不是很显着。

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

为了钻研为什么这个结果被标记,就必要看到DataFlow库用来孕育发生这个结果的每个流程步骤。QL容许编写特殊的路径问题查询,这些查询可天生可逐节点探索的可变长度路径,DataFlow库容许编写输出此数据的查询。

LGTM本身没有路径问题查询的路径探索UI,是以必要应用另一个Semmle利用法度榜样:QL for Eclipse。这是一个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()调用中的参数:

String chainedTo = actionName + nameSeparator + resultCode

actionName来自某个地方的`getActionName`

ActionConfig chainedToConfig = pkg.getActionConfigs().get(chainedTo)

//chainedTo包孕`actionName`并终极呈现在`get`措施中

单击下一步,就到了ValueStackShadowMap::get()措施,如下所示:

public Object get(Object key) {

Object value = super.get(key);//

if ((value == null) && key instanceof String) {

value = valueStack.findValue((String) key);//key`

}

return value;

}

事实证实,由于pkg.getActionConfigs()返回一个Map,而ValueStackShadowMap实现了Map接口,以是理论上pkg.getActionConfigs()返回的值可能是ValueStackShadowMap的一个实例。是以,QL DataFlow库显示了从变量chainedTo到类ValueStackShadowMap中的get()实现的潜在流程。实际上,ValueStackShadowMap类属于jasperreports插件,该类的实例仅在几个地方创建,并都不会被pkg.getActionConfigs()返回。在发明ValueStackShadowMap::get()不太可能被射中之后,作者经由过程在DataFlow::Configuration中添加一个过滤来删除依附它的结果:

override predicate isBarrier(DataFlow::Node node) {

exists(Method m | (m.hasName(“get”) or m.hasName(“containsKey”)) and

上一页[1] [2] [3]下一页

m.getDeclaringType().hasName(“ValueStackShadowMap”) and

node.getEnclosingCallable() = m

)

}

这个predicate意思是假如不法数据流入ValueStackShadowMap的get()或containsKey()措施,那么就不要继承跟踪它。

新破绽:CVE-2018-11776

只有10对源和接管点,就很轻易经由过程手工反省发明这些是否真正存在问题。经由过程一些路径,作者发明有些路径是无效的,由于它们在测试用例中,以是在查询中添加了一些过滤来过滤掉落这些路径。这就留下了一些异常有趣的结果。

以ServletActionRedirectResult.java中的源代码为例:

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

public void execute(ActionInvocation invocation) throws Exception {

actionName = conditionalParse(actionName, invocation);

if (namespace == null) {

namespace = invocation.getProxy().getNamespace();//源

} 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进入ActionMapping的构造函数

setLocation(tmpLocation);

继承履行这些步骤之后,可以看到getUriFromActionMapping()返回一个URL字符串,该字符串应用构造的ActionMapping中的namespace。然后经由过程变量tmpLocation流入setLocation()的参数,然后setLocation()在超类StrutsResultSupport中设置字段位置:

public void setLocation(String location) {

this.location = location;

}

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

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

setLocation(tmpLocation);

super.execute(invocation);

它将location字段通报给对conditionalParse()的调用:

public void execute(ActionInvocation invocation) throws Exception {

lastFinalLocation = conditionalParse(location, invocation);

doExecute(lastFinalLocation, invocation);

}

然后conditionalParse()将位置通报给translateVariables():

protected String conditionalParse(String param, ActionInvocation invocation) {

if (parse && param != null && invocation != null) {

return TextParseUtil.translateVariables(

param,

invocation.getStack(),

new EncodingParsedValueEvaluator());

} else {

return param;

}

}

以是当在ServletActionRedirectResult中没有设置namespace参数时,代码从ActionProxy获取namespace,然后将其作为OGNL表达式进行评估。为了测试这个,作者经由过程以下措施调换了showcase利用法度榜样中的一个设置设置设备摆设摆设文件(例如struts-actionchaining.xml)中的struts标记:

struts>

package name=”actionchaining” extends=”struts-default”>

action name=”actionChain1″ class=”org.apache.struts2.showcase.actionchaining.ActionChain1″>

result type=”redirectAction”>

param name = “actionName”>register2param>

result>

action>

package>

struts>

然后在本地运行showcase利用法度榜样,并造访了一个可以触发此破绽的URL并履行shell敕令以在谋略机上打开谋略器利用法度榜样。

不仅如斯,来自ActionChainResult,PostbackResult和ServletUrlRenderer的弗成信滥觞也同样有效!PortletActionRedirectResult中的那个可能也可以,但没有被测试。四个RCE足以证实问题的严重性。

总结

在这篇文章中,作者展示了经由过程应用已知(以前的)的破绽来赞助构建利用法度榜样的污点模型,只需将麻烦的事情留给QL DataFlow库就可以找到新的破绽。

鉴于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之间的相似性处于加倍语义的层面。它们由受污染的源连接,而不是源代码的位置,是以任何能够成功找到这样的变体的软件或对象都必要能够在全部代码库中履行这种语义阐发。

上一页[1] [2] [3]

赞(0) 打赏
分享到: 更多 (0)
免责申明:本站所有资料均来自于网络,版权归原创者所有!本站不提供任何保证,不保证真实性,并不承担任何法律责任

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

阿里云优惠网 更专业 更优惠

阿里云优惠券阿里云大礼包