Using CodeQL to find out Log4j CVE-2021-44228

Although there is a experimental CWE-020 query used for “Potential Log4J LDAP JNDI injection (CVE-2021-44228)” already, but at this time, I want to refit the CWE-074 to make it could find out CVE-2021-44228

Introduction

As we all know, Log4j is caused by user-controlled JNDI lookup, from the document, I found CodeQL query help covered it and it’s CWE number is CWE-074. Here is the doc: JNDI lookup with user-controlled name

Let’s walk through this CWEs and try to use it to find the Log4j CVE-2021-44228

Interpret the CWE-074

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

As we can see, it encapsulated the mots code into semmle.code.java.security.JndiInjectionQuery

By the comments in the code, we can know that this lib is used to provide taint tracking configurations to be used in JNDI injection queries.

And in it, we can find that it requires 4 libs as following

  • semmle.code.java.dataflow.FlowSources
    • Provides classes representing various flow sources for taint tracking
    • This is a basic lib for CodeQL
  • semmle.code.java.frameworks.Jndi
    • Provides classes and predicates for working with the Java JNDI API.
  • semmle.code.java.frameworks.SpringLdap
    • Provides classes and predicates for working with the Spring LDAP API.
  • semmle.code.java.security.JndiInjection
    • Provides classes and predicates to reason about JNDI injection vulnerabilities.
    • It’s important for us, so we will analyze it

Interpreter JndiInjection.qll

Class DefaultJndiInjectionSink

It invokes the internal experimental API, and in practice, I found that it could locate the JNDI lookup function

This code written by myself as following works as same as the sinkNode invokes.

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

This class extends JndiInjectionSink and DataFlow::ExprNode, so it’s a Node and also a ExprNode.

The codeql judge code is as bellow

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 .

1
MethodAccess ma, Method m

Firstly, there are a method access and a method.

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

The method access accessed the m method and the sink as expression is the method access’ first argument and the method is Ldap operation

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

the method could be search and it’s third argument should be true at compile time or the method could be unbind and it’s first argument should be true at compile time

What’s the meaning of this? Let’s check it in real code.

TypeLdapOperations includes 2 classes

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

So this is only for the condition with SpringFramework, but this time, I want to find out a more general conditional without any framework. However, it’s a good idea to analyze it next time.

Class ProviderUrlJndiInjectionSink

As the comment said, it could find out the sink about the provider URL.

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 means m Method should be sub of java.util.Hashtable.

(m.hasName("put") or m.hasName("setProperty")) indicates the name of the method

The final part indicates the first parameter should be a String java.naming.provider.url or a Field with type javax.naming.Context and the name should be 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
)
)

So, obviously, if the user input could only control the provider URL, this query still could locate it.

Class DefaultJndiInjectionAdditionalTaintStep

A set of additional taint steps to be considered for taint tracking JNDI injection related data flows, in order to avoid taint tracking breaks when invoking third-party packages.

  • nameStep(node1, node2) holds if n1 to n2 is a dataflow step that converts between String and CompositeName or CompoundName by calling new CompositeName(tainted) or new CompoundName(tainted).
  • nameAddStep(node1, node2) holds if n1 to n2 is a dataflow step that converts between String and CompositeName or CompoundName by calling new CompositeName().add(tainted) or new CompoundName().add(tainted).
  • jmxServiceUrlStep(node1, node2) holds if n1 to n2 is a dataflow step that converts between String and JMXServiceURL by calling new JMXServiceURL(tainted).
  • jmxConnectorStep(node1, node2) holds if n1 to n2 is a dataflow step that converts between JMXServiceURL and JMXConnector by calling JMXConnectorFactory.newJMXConnector(tainted).
  • rmiConnectorStep(node1, node2) holds if n1 to n2 is a dataflow step that converts between JMXServiceURL and RMIConnector by calling new RMIConnector(tainted).

Interpreter JndiInjectionQuery.qll

Now, let’s advance to the “query” lib, here contains some information about how to perform global taint tracking.

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)
}
}

It applied the JndiInjectionSink as the Sink to track

isSanitizer defines the condition that the result should be removed, in this case, if the node is a primitiveType or a BoxedType (Wrapped primitiveType), it will be removed.

isAdditionalTaintStep adds additional taint steps, in this case, it uses JndiInjectionAdditionalTaintStep, while using this lib, the any filter indicates that we will any available subclass and here we will use class DefaultJndiInjectionAdditionalTaintStep which has been interpreted.

Class UnsafeSearchControlsSink

A method that does a JNDI lookup when it receives a SearchControls argument with setReturningObjFlag = true

This class defined the unsafe search controls sink

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

As we can see, it requires UnsafeSearchControlsConf, it defines the source and the sink of the flow, the source should be UnsafeSearchControls and the sink should be UnsafeSearchControlsArgument.

So the sink should be a method access’ first argument and one of the method access ‘ arguments will be flowed in following the rule defined in UnsafeSearchControlsConf.

Test JndiInjection.ql with Java code

JndiInjection.ql just simply invoked path query with JndiInjectionFlowConfig.

Here is the test code and part of it is extracted from the official demo.

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"); // Match 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

We got the expected result. Verify the previous analysis with Quick evaluation

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

Excellent, this query works very well.

Advance to Log4j CVE-2021-44228

Introduce org.apache.logging.log4j-2.14.1 which you can find here

Prepare the database for CodeQL

After configuring the toolchains-sample-*.xml, we can get the CodeQL database.

For better performance, we can exclude useless projects in modules section.

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'

Locate the source

Through debugging, we can know that the user input source is located in the various log functions in log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java, like debug, info,error, and all of them will invoke logIfEnabled with “message” or “messageSupplier” parameter as log message.

So the source should be like this

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")
)
}
}

And we need a new 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"

Just changed the isSource part and the other remains the same as JndiInjectionFlowConfig.

Run this query, we got this

image-20230212233802283

image-20230212233811480

Lucky! We successfully find a path proved that the user input could be passed to JNDI lookup. Full code is shown as bellow.

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"

References

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

Author

4xpl0r3r

Posted on

2023-02-14

Updated on

2024-03-19

Licensed under

Comments