翻译自:https://lgtm.com/blog/spring_data_rest_CVE-2017-8046_ql
翻译:聂心明
在这篇文章中,我将介绍Spring Data REST远程命令执行漏洞并且我将展示如何使用ql帮助保证漏洞得到正确的修复。
这个漏洞( CVE-2017-8046 https://pivotal.io/security/cve-2017-8046 )它通过发送PATCH请求,并且数据包里面带有特意构造的恶意json数据,这样就可以在服务器上运行任意代码,而且不幸的是,这个漏洞非常好利用。它影响了Spring Data REST组件,这个组件分布在各种Spring项目中,包括,Spring Boot framework。想要看更详细的受影响Spring组件,可以访问 https://lgtm.com/blog/spring_data_rest_CVE-2017-8046
我在2017年9月发现这个漏洞,然后我快速的把这个漏洞报给了官方,通过我们的帮助,官方在2017年十月发布给受影响的组件发布了补丁,在Pivotal的要求下,并且由于这个漏洞的严重性,我们在2018年3月公开了这个漏洞的。在文章底部有漏洞披露的完整时间线。
Spring表达式(SpEL )是非常强大的spring框架表达式语言。典型地,使用SpelExpressionParser (https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/expression/spel/standard/SpelExpressionParser.html ) 把字符串解析成表达式 ( https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/expression/Expression.html ),当其中一个方法获取到值之后,表达式就会被执行,getValueType 或者 setValue会被调用:
Expression expr = expressionParser.parseExpression(stringScript);
expr.getValue(); //Execute the code in stringScript
通常,SpEL仅限于内部使用并且stringScript会被程序完全控制。无论怎么样,SpEL是非常强大的语言,如果输入的表达式来自于不受信任的源,那么攻击者就可以在有漏洞的服务器上执行。例如,如果stringScript是下面这样的形式的话:
String stringScript = "T(java.lang.Runtime).getRuntime().exec(" + cmd + ").x";
之后cmd就被执行。
这就是这个Spring Data REST漏洞的基本原理,Spring Data REST把远程输入的数据解析成SpEL表达式,并且将其解析。为了使用QL发现漏洞,我首先使用表达式解析器建模类和函数的访问:
/** The class `ExpressionParser` in spring. */
class ExpressionParser extends RefType {
ExpressionParser() {
this.hasQualifiedName("org.springframework.expression", "ExpressionParser")
}
}
/** Methods to parse an expression. */
class ParseExpression extends MethodAccess {
ParseExpression() {
exists (Method m |
(m.getName().matches("parse%") or m.hasName("doParseExpression"))
and
this.getMethod() = m
)
}
}
在解析之前,它能辨别出多种函数。一些函数被定义在ExpressionParser中,然而一些其他函数只在SpelExpressionParser定义,我不会在这里指定方法的具体的声明类型,反而在查询结果中限制查询结果。
为了获取更多有价值的结果,我想寻找一段可以被输入控制且能被解析的表达式。以前的一种方法是使用数据流库追踪远程的输入。可是,我对Spring Data REST的内部工作原理不是很了解,所以没有对远程输入建模,我决定探索一下,并且在javadoc中寻找一些线索。通常在web应用程序中进行远程输入的类中,一个路径或者URL会在Javadoc或者注释中具体说明。在QL中,我能通过Javadoc类访问Javadoc,所以让我们在Javadoc中寻找包含路径和URL的内容吧。下面的类是javadoc中的一个类,这个类做一些基本的字符串分析,目的是检查表达式的内容中是否包含文件路径或者URL。
/** Look for hints in the javadoc that indicates this method is use for processing path. */
class DocHasPath extends Javadoc {
DocHasPath() {
exists(string s |
//Look for something that indicates the functionality is to do with a path.
s = this.toString().regexpMatch("(.* )?/[^ ]*/.*|.*path.*")
)
}
}
我使用下面的方法去发现在Javadoc类中包含路径的函数
/** Look for methods that may be used for processing paths, indicated by javadoc. */
class CallHasPath extends Callable {
CallHasPath() {
//exclude test classes
not this.getDeclaringType() instanceof TestClass and
(
this.getDoc().getJavadoc() instanceof DocHasPath or
this.getDeclaringType().getDoc().getJavadoc() instanceof DocHasPath
)
}
}
特别是,我对这个把字符串解析成SpEL表达式的函数非常感兴趣,所以把他们放在一起,我发现两个成果 ( https://lgtm.com/query/2141670445/ )。这个成果在同一个类之中,并且javadoc建议这个类用于将补丁路径转换为SpEL表达式。更进一步的调查显示,在类中解析路径会调用PatchOperation ( https://github.com/spring-projects/spring-data-rest/blob/8f269e28fe8038a6c60f31a1c36cfda04795ab45/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchOperation.java ) 。PatchOperation的基类会被用于处理PATCH请求。这个类接收PATCH请求,它们作为字段成员被存储并被解析成SpEl Expression ( https://github.com/spring-projects/spring-data-rest/blob/8f269e28fe8038a6c60f31a1c36cfda04795ab45/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/PatchOperation.java#L73 )。
经过测试,这个是可以被利用的,我把这个问题报告给了Pivotal,Pivotal团队很快的在PatchOperation中添加了verifyPath ( https://github.com/spring-projects/spring-data-rest/commit/8f269e28fe8038a6c60f31a1c36cfda04795ab45 ) 函数去过滤输入。这个想法是在解析表达式之前调用verifyPath,目的是为了过滤远程输入:
verifyPath(entityType); //verify the `path` field that was parsed into a SpEL expression (`spelExpression`)
return evaluate(spelExpression.getValueType(targetObject)); //OK to call `getValueType` for evaluation.
由于原来的问题没有被修复完全,因此看到相关的安全问题并不罕见。比如在Spring-Webflow中,就有 CVE-2017-4971 和 CVE-2017-8039两个类似的漏洞,这两个漏洞是相同的问题,但是用了两个不同的代码路径。如果安全漏洞没有被完全修复,就会导致很多严重的安全问题,有经验的黑客会根据安全通告找到漏洞的位置。所以在发安全通告之前,要确定补丁能够全面覆盖。为了查看漏洞是否被完全修复,就要检查是否所有调用了getValue,getValueType或者setValue代码之前都调用了verifyPath。首先,让我们定义一个包含verifyPath函数的模块:
/** The sanitization method `verifyPath` */
class VerifyPath extends Method {
VerifyPath() {
this.hasName("verifyPath") and
this.getDeclaringType().hasQualifiedName("org.springframework.data.rest.webmvc.json.patch", "PatchOperation")
}
}
为了确定在调用verifyPath之后,SpEL表达式会被有效的验证,我利用了QL中的ControlFlow库。首先我定义一个ActionConfiguration,这个模块调用了verifyPath。我用isAction查看通过调用verifyPath的执行路径。
/** A control flow node that represents a call to `verifyPath`. */
class VerifyPathActionConf extends ActionConfiguration {
VerifyPathActionConf() { this = "VerifyPathActionConf" }
override predicate isAction(ControlFlowNode node) {
node.(MethodAccess).getMethod() instanceof VerifyPath
}
}
ActionConfiguration包含一个名字叫callAlwaysPerformsAction的断言,它会检查是否是因为输入导致(调用)指定的操作(在这个场景中,调用一个函数导致调用verifyPath),这个操作在每个代码路径上都会被执行。
from MethodAccess ma, VerifyPathActionConf conf
where conf.callAlwaysPerformsAction(ma)
select ma
这标识了一个总是调用verifyPath的函数,比如:
public void callsVerify() {
verifyPath();
}
并且遗漏了一些可能在代码路径执行路径上可能不会调用verifyPath的函数,比如:
public void notAlwaysCallsVerify(boolean call) {
if (call) {
verifyPath();
}
}
用VerifyPathActionConf,我现在就能寻找到那些直接执行SpEL表达式而没有调用verifyPath的地方。更特别的是,我想发现PATCH请求中仍然存在的漏洞。当发出PATCH请求能触发PatchOperation类的perform函数的时候,这标志着这个操作可能会有安全问题。比如
public void perform(Object target, Class<T> type) {
spelExpression.getValue()
}
在不先调用verifyPath的情况下直接评估spelExpression,我还需要确定可传递地评估spelExpression的情况:
public void perform(Object target, Class<T> type) {
evaluateWithoutVerify(); //没有调用verifyPath的spelExpression
}
还漏掉一种安全的情况
public void perform(Object target, Class<T> type) {
verifyPath(type);
evaluateWithoutVerify();
}
或者
public void perform(Object target, Class<T> type) {
evaluateWithVerify(); //calls verifyPath before evaluation.
}
与局部控制流分析不同,QL会找到更多的可能性和他们之间的传递关系,所以,QL处理这些场景会更加轻松一些。总之,不安全的调用应该满足下面的条件:
我们看到一些不安全的调用被递归的定义,所以为了构造一个QL类,我们使用递归进行建模:
/** A method that evaluates the expression before calling `verifyPath`.*/
class UnsafeEvaluateCall extends MethodAccess {
UnsafeEvaluateCall() {
(
//Base of the recursion.
this.getMethod() instanceof Evaluate
or
//recursive definition: This calls another `UnsafeEvaulateCall`
exists(UnsafeEvaluateCall unsafe |
this.getMethod() = unsafe.getEnclosingCallable()
)
)
and
//Does not always call verify before this.
not exists(VerifyPathCallerAccess verify |
dominates(verify, this)
)
}
}
使用这段代码查询( https://lgtm.com/query/1505746236024/ )。你会发现copy,move和remove操作依然有漏洞。经过测试发现,这些操作依然可以被利用,之后我把这些问题报告给了Pivotal。Pivotal 很快的给我回信说,他们已经确认问题,并且用verifyPath保护了所有的操作。并且还添加了一些单元测试 ( https://github.com/spring-projects/spring-data-rest/blob/973f932a390ec1dd6e4b8e78f75b355238db12a8/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/PatchOperationUnitTests.java )保证漏洞已经被修复
在所有的操作前面加上verifyPath 函数似乎已经完全修复了所有的漏洞,但是我决定去检查一下verifyPath 本身是否实现的正确。当检测具体的算法实现的时,最好把这个工作留给人去做。但我们已经看出来,在识别逻辑错误的这方面,QL非常有用。尤其,程序员和我假设,PatchOperation中的路径这个变量是唯一被spelExpression解析的,所以我想知道这个是不是真的。我写了另一个查询,目的是寻找流入parse 函数的参数的所有表达式。
from FlowSource source, Expr sink
where source.flowsTo(sink)
and exists(ParseExpression m | sink = m.getAnArgument() and
m.getQualifier().getType().(RefType).getASupertype*() instanceof ExpressionParser and
m.getEnclosingCallable().getDeclaringType() instanceof PathToSpEL)
//Exclude the field `path` that have been checked by `verifyPath` already.
and source.getEnclosingCallable().getDeclaringType().getASupertype*() instanceof PatchOperation
and not exists(PathField path, FlowSource commonSource |
commonSource.flowsTo(path.getAnAccess()) and commonSource.flowsTo(source)
)
select source, sink
这个查询 ( https://lgtm.com/query/1505752096043/ )给出了三个结果。所以我看到了copy和move 操作,另外一个被调用的变量from ( https://lgtm.com/projects/g/spring-projects/spring-data-rest/snapshot/dfc2adf373ac6c110377e6a7ea2745205342629d/files/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/FromOperation.java?sort=name&dir=ASC&mode=heatmap&excluded=false#L42 ) 也被SpEL表达式解析( https://lgtm.com/projects/g/spring-projects/spring-data-rest/snapshot/dfc2adf373ac6c110377e6a7ea2745205342629d/files/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/CopyOperation.java?sort=name&dir=ASC&mode=heatmap&excluded=false#L60 ) 。这个发现是非常有意义的,因为要给copy和move提供两个路径,一个是源地址另一个是目的地址。无论怎样,当我们观察 PatchOperation函数的时候,很容易想到只提供一个路径的操作,但是会忽略掉另一种情况。尤其过一些时间之后,当不同的人添加代码实现新的操作的时候。通过QL,而不是通过猜测,几分钟之后我完成了搜索并且得到了我想要的结果。经过测试我发现这些操作依然可以被利用,我把这些发现又报告给了Pivotal 。这次他们也确认了,并且添加了新的单元测试目的是捕捉所有的问题,并且花了稍微长的时间才做出回应。观察这次测试( https://github.com/spring-projects/spring-data-rest/blob/973f932a390ec1dd6e4b8e78f75b355238db12a8/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/patch/PatchOperationUnitTests.java#L34 ),很明显的是from这个参数没有被测试。即使这样,这个漏洞也需要path这个变量是一个合法的路径,这对于单元测试或者普通的fuzzing测试来说,这个问题很难被发现。
最后。解析表达式的全部代码被重写( https://github.com/spring-projects/spring-data-rest/commit/d48499ad8d61714d9f8f836c3eb859c52f59b673 )。这次,所有的SpEL 表达式解析被放入了一个叫 SpelPath 的类中( https://lgtm.com/projects/g/spring-projects/spring-data-rest/snapshot/dist-48000263-1509466693845/files/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/SpelPath.java?sort=name&dir=ASC&mode=heatmap&excluded=false#L57 ) ,这个类中有一个子类叫 TypedSpelPath ( https://lgtm.com/projects/g/spring-projects/spring-data-rest/snapshot/dist-48000263-1509466693845/files/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/patch/SpelPath.java#L208 ) ,这个类在初始化的时候调用了verifyPath。
回到最原始的问题,是否有一些操作会在TypedSpelPath之外调用。为了验证这个想法,我写了一个简单的查询去寻找这样的操作:
from Method m
where exists(Method e | e instanceof Evaluate and m.polyCalls(e))
select m, m.getDeclaringType()
运行这个查询 ( https://lgtm.com/query/1505836165972/ ) 我发现所有的操作都在TypedSpelPath之中,所以我觉得这个漏洞修补成功了。
在这篇文章中,我们看到怎样使用QL在代码中识别类似的漏洞并且确保漏洞已经被修复完善。这种类型的变体分析已经在很多客户中取得了巨大的成功,并且我希望这些也对你有用。这里有一些更进一步的例子,这个查询 ( https://lgtm.com/query/1505829066076/ )可以识别 CVE-2017-4971 ( https://pivotal.io/security/cve-2017-4971 ) 和 CVE-2017-8039 ( https://pivotal.io/security/cve-2017-8039 )。我已经在更早的文章中解析过这两个漏洞,他们都是因为不安全的SpEL 表达式解析造成的。