使用CodeQL发现Log4j CVE-2021-44228

虽然已经有了一个针对”Potential Log4J LDAP JNDI injection (CVE-2021-44228)”的实验性CWE-020 Query,但这次我想改写CWE-074,使其能够发现CVE-2021-44228。

本文同时提供其他语言的版本: English.

引入

众所周知,Log4j是由受用户控制的JNDI lookup引起的。从文档中,我发现CodeQL Query帮助已经涵盖了它,它的CWE编号是CWE-074。以下是该文档链接: JNDI lookup with user-controlled name

让我们一起学习这个CWE,尝试使用它来查找Log4j CVE-2021-44228漏洞

在本文中,有一些CodeQL专有的术语,我不会将它们转换为中文,但在这里解释一下它们的含义。你也可以在官方的术语库中查阅

  • predicate - 类似普通开发语言的函数
  • source - 类似于起点
  • sink - 类似于终点
  • query - 类似SQL的query,也类似脚本语言中的脚本

解读 CWE-074

CWE-074代码: https://github.com/github/codeql/blob/main/java/ql/src/Security/CWE/CWE-074/JndiInjection.ql

正如我们所看到的,它将大部分代码封装到了semmle.code.java.security.JndiInjectionQuery

通过代码中的注释,我们可以知道这个库被用来提供污点跟踪配置,以用于JNDI注入的Query。

在其中,我们可以发现它需要以下4个库:

  • semmle.code.java.dataflow.FlowSources
    • 提供了表示污点跟踪各种数据来源的类和predicate
    • 这是CodeQL的基本库
  • semmle.code.java.frameworks.Jndi
    • 提供了用于操作Java JNDI API的类和predicate。
  • semmle.code.java.frameworks.SpringLdap
    • 提供了用于操作Spring LDAP API的类和predicate
  • semmle.code.java.security.JndiInjection
    • 提供了用于分析JNDI注入漏洞的类和predicate
    • 这个对我们很重要,因此我们将分析这个库

解读 JndiInjection.qll

Class DefaultJndiInjectionSink

它调用了内部实验性API,在实践中,我发现它可以定位JNDI lookup函数。

以下是我编写的代码,其作用与sinkNode的调用相同。

1
2
3
4
5
6
exists(MethodAccess ma, Method m |
ma.getMethod() = m and
this.asExpr() = ma.getAnArgument() and
m.getDeclaringType().hasQualifiedName("javax.naming","Context") and
m.hasName("lookup")
)

Class ConditionedJndiInjectionSink

该类扩展了JndiInjectionSinkDataFlow::ExprNode,因此它既是一个Node,也是一个ExprNode

以下是CodeQL判断代码:

1
2
3
4
5
6
7
8
9
10
11
exists(MethodAccess ma, Method m |
ma.getMethod() = m and
ma.getArgument(0) = this.asExpr() and
m.getDeclaringType().getASourceSupertype*() instanceof TypeLdapOperations
|
m.hasName("search") and
ma.getArgument(3).(CompileTimeConstantExpr).getBooleanValue() = true
or
m.hasName("unbind") and
ma.getArgument(1).(CompileTimeConstantExpr).getBooleanValue() = true
)

Let’s divide it into 3 parts by the | operand .

我们通过|运算符将其分为3个部分

1
MethodAccess ma, Method m

首先是一个方法访问和方法

1
2
3
ma.getMethod() = m and
ma.getArgument(0) = this.asExpr() and
m.getDeclaringType().getASourceSupertype*() instanceof TypeLdapOperations

该方法访问了m方法,作为表达式的sink是m方法的第一个参数,而这个方法是LDAP操作。

1
2
3
4
5
m.hasName("search") and
ma.getArgument(3).(CompileTimeConstantExpr).getBooleanValue() = true
or
m.hasName("unbind") and
ma.getArgument(1).(CompileTimeConstantExpr).getBooleanValue() = true

该方法可以是search方法,在编译时它的第三个参数应该是true;或者该方法可以是unbind方法,在编译时它的第一个参数应该是true

这是什么意思?我们可以在真实的代码中查看一下。

TypeLdapOperations 包含2个类

  • org.springframework.ldap.core
  • org.springframework.ldap

所以这只是针对具有SpringFramework的情况,但是这一次,我想找到一个更通用的条件,而不需要任何框架。不过,下次分析这一点也是一个不错的想法。

Class ProviderUrlJndiInjectionSink

正如注释所说,它可以找到关于PROVIDER_URL的sink。

1
2
3
4
/**
* Tainted value passed to env `Hashtable` as the provider URL by calling
* `env.put(Context.PROVIDER_URL, tainted)` or `env.setProperty(Context.PROVIDER_URL, tainted)`.
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
exists(MethodAccess ma, Method m |
ma.getMethod() = m and
ma.getArgument(1) = this.getExpr()
|
m.getDeclaringType().getASourceSupertype*() instanceof TypeHashtable and
(m.hasName("put") or m.hasName("setProperty")) and
(
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "java.naming.provider.url"
or
exists(Field f |
ma.getArgument(0) = f.getAnAccess() and
f.hasName("PROVIDER_URL") and
f.getDeclaringType() instanceof TypeNamingContext
)
)
)

m.getDeclaringType().getASourceSupertype*() instanceof TypeHashtable 表示m 应该是 java.util.Hashtable的子类

(m.hasName("put") or m.hasName("setProperty")) 指定了方法的名称

最后一部分指示第一个参数应该是一个字符串java.naming.provider.url或者是一个类型为javax.naming.Context的字段,且名称应为PROVIDER_URL

1
2
3
4
5
6
7
8
9
(
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "java.naming.provider.url"
or
exists(Field f |
ma.getArgument(0) = f.getAnAccess() and
f.hasName("PROVIDER_URL") and
f.getDeclaringType() instanceof TypeNamingContext
)
)

因此,显然,如果用户输入只能控制PROVIDER_URL,则此Query仍然可以找到它。

Class DefaultJndiInjectionAdditionalTaintStep

这是一组在跟踪JNDI注入相关数据流的污点时需要考虑的额外污点步骤,以避免在调用第三方包时出现污点跟踪中断。

  • nameStep(node1, node2) 表示 n1n2 是一个数据流步骤,通过调用 new CompositeName(tainted)new CompoundName(tainted)StringCompositeNameCompoundName 之间进行转换。
  • nameAddStep(node1, node2) 表示 n1n2 是一个数据流步骤,通过调用 new CompositeName().add(tainted)new CompoundName().add(tainted)StringCompositeNameCompoundName 之间进行转换。
  • jmxServiceUrlStep(node1, node2) 表示 n1n2 是一个数据流步骤,通过调用 new JMXServiceURL(tainted)StringJMXServiceURL 之间进行转换。
  • jmxConnectorStep(node1, node2) 表示 n1n2 是一个数据流步骤,通过调用 JMXConnectorFactory.newJMXConnector(tainted)JMXServiceURLJMXConnector 之间进行转换。
  • rmiConnectorStep(node1, node2) 表示 n1n2 是一个数据流步骤,通过调用 new RMIConnector(tainted)JMXServiceURLRMIConnector 之间进行转换。

解读JndiInjectionQuery.qll

现在,让我们进入“query”库,这里包含了一些关于如何进行全局污点追踪的信息

Class JndiInjectionFlowConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JndiInjectionFlowConfig extends TaintTracking::Configuration {
JndiInjectionFlowConfig() { this = "JndiInjectionFlowConfig" }

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
}

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(JndiInjectionAdditionalTaintStep c).step(node1, node2)
}
}

它将 JndiInjectionSink 应用为 Sink 进行跟踪。

isSanitizer 定义了应该删除结果的条件,在这种情况下,如果节点是原始类型或包装原始类型(BoxedType),则会将其删除。

isAdditionalTaintStep 添加了额外的污点步骤,在这种情况下,它使用 JndiInjectionAdditionalTaintStep,使用这个库时,any 过滤器表示我们将使用任何可用的子类,在这里我们将使用已经解释过的 DefaultJndiInjectionAdditionalTaintStep 类。

Class UnsafeSearchControlsSink

一个当接收到一个 setReturningObjFlag 属性为 trueSearchControls 参数时执行 JNDI lookup的方法。

这个类定义了不安全的 Search Controls Sink。

1
2
3
4
5
exists(UnsafeSearchControlsConf conf, MethodAccess ma |
conf.hasFlowTo(DataFlow::exprNode(ma.getAnArgument()))
|
this.asExpr() = ma.getArgument(0)
)

正如我们所看到的,它需要 UnsafeSearchControlsConf,它定义了数据流的Source和 Sink,Source应该是 UnsafeSearchControls,Sink 应该是 UnsafeSearchControlsArgument

因此,Sink 应该是方法访问的第一个参数,方法访问的一个参数将按照 UnsafeSearchControlsConf 中定义的规则进行污染。

使用Java代码测试JndiInjection.ql

JndiInjection.ql 只是简单地使用 JndiInjectionFlowConfig 调用了路径的Query。

这是测试代码,其中部分代码从官方演示中提取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void doGet(HttpServletRequest request, HttpServletResponse response) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); // necessary for Java 8
String name = request.getParameter("name");

Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099"); // 匹配 ProviderUrlJndiInjectionSink
InitialContext ctx = null;
try {
ctx = new InitialContext(env);

// BAD: User input used in lookup
ctx.lookup(name);

// GOOD: The name is validated before being used in lookup
// if (isValid(name)) {
// ctx.lookup(name);
// } else {
// // Reject the request
// }
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:4444/\#Exploit 1099
1
2
jdk8
codeql database create cwe074-test --language=java --source-root=/Users/kano/Workspace/IdeaProjects/demo12

image-20230211221440882

我们得到了预期的结果. 我们可以使用 Quick evaluation 功能来验证之前的分析

  • DefaultJndiInjectionSink 找到了String name = request.getParameter("name");
  • ProviderUrlJndiInjectionSink 找到了env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");

事实证明这些Query工作得非常好

前进到 Log4j CVE-2021-44228

引入org.apache.logging.log4j-2.14.1,你可以在这里找到它 here

为CodeQL准备数据库

配置toolchains-sample-*.xml后,我们可以得到CodeQL数据库。

为了获得更好的性能,我们可以在“modules”部分排除无用的项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<modules>
<module>log4j-api-java9</module>
<module>log4j-api</module>
<module>log4j-core-java9</module>
<module>log4j-core</module>
<!-- <module>log4j-layout-template-json</module>
<module>log4j-core-its</module>
<module>log4j-1.2-api</module>
<module>log4j-slf4j-impl</module>
<module>log4j-slf4j18-impl</module>
<module>log4j-to-slf4j</module>
<module>log4j-jcl</module>
<module>log4j-flume-ng</module>
<module>log4j-taglib</module>
<module>log4j-jmx-gui</module>
<module>log4j-samples</module>
<module>log4j-bom</module>
<module>log4j-jdbc-dbcp2</module>
<module>log4j-jpa</module>
<module>log4j-couchdb</module>
<module>log4j-mongodb3</module>
<module>log4j-mongodb4</module>
<module>log4j-cassandra</module>
<module>log4j-web</module>
<module>log4j-perf</module>
<module>log4j-iostreams</module>
<module>log4j-jul</module>
<module>log4j-jpl</module>
<module>log4j-liquibase</module>
<module>log4j-appserver</module>
<module>log4j-osgi</module>
<module>log4j-docker</module>
<module>log4j-kubernetes</module>
<module>log4j-spring-boot</module>
<module>log4j-spring-cloud-config</module> -->
</modules>
1
codeql database create log4j-db -l java -s logging-log4j2-rel-2.14.1/ -c './mvnw clean install -t toolchains-sample-mac.xml -Dmaven.test.skip=true'

找到source

通过调试,我们可以知道用户输入源位于log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java中的各个日志函数,比如debuginfoerror 和它们都将调用带有messag或messageSupplier作为日志消息的 logIfEnabled 函数。

所以源码应该是这样的:

1
2
3
4
5
6
7
8
9
class Log4jFlowSource extends DataFlow::Node{
Log4jFlowSource(){
this.asParameter().getCallable().hasName("logIfEnabled") and
(
this.asParameter().hasName("message") or
this.asParameter().hasName("messageSupplier")
)
}
}

并且我需要添加一个新的 TaintTracking::Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class JndiInjectionFlowConfigInLog4j extends TaintTracking::Configuration{
JndiInjectionFlowConfigInLog4j() { this = "JndiInjectionFlowConfigInLog4j" }
override predicate isSource(DataFlow::Node source) { source instanceof Log4jFlowSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
}

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(JndiInjectionAdditionalTaintStep c).step(node1, node2)
}
}

from DataFlow::PathNode source, DataFlow::PathNode sink, JndiInjectionFlowConfigInLog4j conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(),
"this user input"

只需要改变 isSource 部分其他部分与 JndiInjectionFlowConfig相同

运行Query,我们得到如下结果

image-20230212233802283

image-20230212233811480

运气不错,我们成功找到一条路径,证明用户输入可以传递给 JNDI lookup。 完整代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @name JNDI lookup with user-controlled name in Log4j Lib
* @description Performing a JNDI lookup with a user-controlled name can lead to the download of an untrusted
* object and to execution of arbitrary code.
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id java/jndi-injection
* @tags security
* external/cwe/cwe-074
*/

import java
import semmle.code.java.security.JndiInjectionQuery
import DataFlow::PathGraph

class JndiInjectionFlowConfigInLog4j extends TaintTracking::Configuration{
JndiInjectionFlowConfigInLog4j() { this = "JndiInjectionFlowConfigInLog4j" }
override predicate isSource(DataFlow::Node source) { source instanceof Log4jFlowSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
}

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(JndiInjectionAdditionalTaintStep c).step(node1, node2)
}
}

class Log4jFlowSource extends DataFlow::Node{
Log4jFlowSource(){
this.asParameter().getCallable().hasName("logIfEnabled") and
(
this.asParameter().hasName("message") or
this.asParameter().hasName("messageSupplier")
)
}
}

from DataFlow::PathNode source, DataFlow::PathNode sink, JndiInjectionFlowConfigInLog4j conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(),
"this user input"

参考

CodeQL CWE Coverage: https://codeql.github.com/codeql-query-help/codeql-cwe-coverage/

CodeQL query help for Java: https://codeql.github.com/codeql-query-help/java/

CodeQL Repository: https://github.com/github/codeql/tree/main/java/ql/src/Security/CWE

作者

4xpl0r3r

发布于

2023-02-14

更新于

2023-02-14

许可协议

评论