在这篇文章中,我将介绍如何自定义一个标准lgtm查询来查找Apache Struts中的远程代码执行漏洞。如果你想了解这个漏洞的更多的公告信息,可以参考这篇博客。这个漏洞已经分配了编号CVE-2017-9805,漏洞的安全公告可以在Struts官网找到以及受影响的Apache Struts2.5.13的漏洞解决详细信息。由于这个漏洞太严重了,所以本文中删除了一些细节(包括exp),这些信息几周后会补充。
我们强烈建议Struts的用户升级到最新版本,以减轻此安全风险。
我发现的这个漏洞是Java在反序列化上处理不安全导致的。继克里斯·弗洛霍夫(Chris Frohoff)和加布里埃尔·劳伦斯(Gabriel Lawrence)在Apache Commons Collections中发现了导致任意代码执行的反序列化的漏洞之后,近年来又出现了许多类似的漏洞,许多Java应用程序受到这种漏洞的影响。如果你想更多地了解这种类型的漏洞,那么lgtm文档页面的这个主题是一个开始的好地方。
lgtm使用专门设计的语言(QL)编写的查询来识别代码中的风险。这些为Java设计的查询之一是检测潜在的不安全的反序列化,这些反序列化通常用于处理用户可控的数据。这种查询识别的场景通常是未被净化的数据却被反序列化成了java对象,用户可控数据来源可能是http请求或者是任何一个socket连接。
除此之外,这种查询还会检查用户控制的数据流向反序列化方法的普通函数。然而,有些项目在接受远程用户输入上所采用的方法稍微有些不同。比如,Apache Struts使用ContentTypeHandler接口。这会将数据转为Java对象。由于这个接口的实现都是将收到的数据反序列化,所以实现这个接口的每个类都值得关注(潜在风险)。用于检测不安全反序列化标准QL查询可以轻松地适应于识别用于处理用户输入的方法。这是通过自定义数据源来完成的。
在这个例子中,我们对来自toObject方法的数据流感兴趣,因为这个方法是在ContentTypeHandler接口定义的:
void toObject(Reader in, Object target);
toObject的第一个参数收到的数据应该被认为是肮脏的:因为该数据是远程用户控制的所以不值得相信任。我们的目标是发现这么一个点,某个反序列化方法未经验证或净化就调用了肮脏的数据。
QL DataFlow函数库提供了这么个功能,它能够在源码中跟踪各个步骤的肮脏数据。这就是著名的污点追踪技术。例如,数据被跟踪是因为调用了不同的方法。
IOUtils.copy(remoteUserInput, output); // 输出也是安脏的,因为该函数单纯的复制保存了。数据
为了充分利用DataFlow库里的污点追踪功能,我们来定义ContentTypeHandler.toObject(...)的参数作为肮脏源。
首先,我们得定义查询是如何识别ContentTypeHandler接口和toObject方法的。
/** The ContentTypeHandler Java class in Struts **/
class ContentTypeHandler extends Interface {
ContentTypeHandler() {
this.hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler")
}
}
/** The method `toObject` */
class ToObjectDeserializer extends Method {
ToObjectDeserializer() {
this.getDeclaringType().getASupertype*() instanceof ContentTypeHandler and
this.getSignature = "toObject(java.io.Reader,java.lang.Object)"
}
}
这里我们使用getASupertype*()去限制匹配了任意将ContentTypeHandler作为超类的类。
接下来我们想将toObject的第一个参数标记为不安全的数据源,并跟踪它在代码之间的流向。为了达到这个目的,我们扩展了QL dataflow库中FlowSource类。
/** Mark the first argument of `toObject` as a dataflow source **/
class ContentTypeHandlerInput extends FlowSource {
ContentTypeHandlerInput() {
exists(ToObjectDeserializer des |
des.getParameter(0).getAnAccess() = this
)
}
}
直观的说,上面这个类的定义表示了传递toObject的第一个实参都会被ToObjectDeserializer捕捉,所以这就是数据流动源头。请注意,由于技术原因,流源必须是表达式。因此,我们可以确定那个参数收到的实际值(是表达式)作为源,而不是参数本身。
现在我们知道了数据源在哪,那我们就可以找到是哪个不安全的反序列化方法调用了肮脏的数据。我们不必完整了解这个方法,因为它已经出现在了用户控制数据查询的反序列化过程中(64行:UnsafeDeserializationSink)我们只需要将其定义复制到查询控制台即可。通过这个,我们最终查询变成了:
from ContentTypeHandlerInput source, UnsafeDeserializationSink sink
where source.flowsTo(sink)
select source, sink
在FlowSource中我们使用了.flowTo来预测追踪情况,以便我们识别在ContentTypeHandlerInput执行的不安全反序列化。
当我在Struts上进行自定义查询时,产生了一个精准结果。(现在运行它将不会产生结果,因为已经修复了)。我证明了这是一个真正的远程代码执行漏洞,然后将其报告给Struts安全团队。他们的响应非常快,马上就拿出了解决方案尽快这是一个非常重要的任务,API必须改变。由于发现问题的严重性,现阶段我不会披露更多的细节。相反,我将在几个星期后更新本文,并提供更多的信息。