1
Web Security INTERMEDIATE 6 min read Updated

Log4Shell, explained: how one logged string became remote code execution

CVE-2021-44228 from first principles — what a JNDI lookup is, why Log4j evaluated one inside a log message, and the fixes that actually closed it.

founder & security researcher · 2 writeups
Log4Shell, explained: how one logged string became remote code execution

On 9 December 2021 a proof of concept for a bug in Apache Log4j 2 was published, and within hours it was being exploited across the internet. It was assigned CVE-2021-44228, nicknamed Log4Shell, and scored 10.0 — the maximum. It is still worth a careful read, not for the panic, but because it is a textbook case of data being interpreted as instructions in a place nobody thought of as an input parser: the logger.

This writeup explains the mechanism conceptually, then spends most of its time on what fixed it and how to check your own systems. There is no exploit code here — you do not need any to understand the bug or to defend against it.

Scope

Everything below is for understanding and for checking systems you own or are authorized to assess. The commands only read from your own machines.

01The feature that became the bug

Log4j 2 supports lookups: expressions of the form ${prefix:name} that are replaced with a value when a log event is formatted. ${java:version} becomes the Java version, ${env:HOSTNAME} becomes an environment variable. They were designed for configuration files, where the author is trusted.

The mistake was that, before version 2.15.0, lookups were also evaluated inside the message being logged. So when an application did something as ordinary as this, the logger scanned the user's text for expressions and evaluated whatever it found:

java
// a completely normal line of application code
logger.info("Login failed for user {}", username);

Nothing in that line looks dangerous, and that is the point. The developer handed a string to the logger believing it would be written down. The logger treated it as a small program.

02What a JNDI lookup does

One of the available prefixes was jndi. JNDI — the Java Naming and Directory Interface — is a standard API for asking a directory service for an object by name. Given ${jndi:ldap://host/name}, the logger would connect to the LDAP server named in the string and ask it for that entry.

An LDAP server can answer with more than plain data. It can return a serialized Java object, or a reference that tells the client where to load a class from. Older Java runtimes would fetch and instantiate that class. Newer ones (8u191, 11.0.1 and later) refuse remote class loading by default, so attackers instead returned objects that abused classes already present in the application. Either way, a server chosen by the attacker got to influence what ran inside the victim process.

from a logged string to an outbound connection
Request fieldheader, username… logged Log4j 2evaluates ${…} JNDI LDAP serverchosen by attacker Objectin the JVM
what the logger did (illustrative)
input  → a request field containing ${jndi:ldap://server/name}
log4j  → message contains a lookup, evaluate it
jndi   → connect to server, ask for name
ldap   → answers with an object or a class reference
         # the application now processes what the server sent

03Why it spread so far

Three properties combined. The bug needed no credentials and a single request. Any string that reached a log line was an entry point — a header, a username, a search term, a chat message. And Log4j is a dependency of a dependency in an enormous amount of software, so many operators were running it without knowing.

PropertyValueWhy it mattered
Attack vectorNetworkReachable from anywhere the application is
Privileges requiredNoneThe vulnerable code runs before any login check
User interactionNoneSending the request is enough
ScopeChangedThe logger's flaw compromises the whole application
The flaw was reported to Apache on 24 November 2021 by Chen Zhaojun of the Alibaba Cloud Security Team. The first fixed release, 2.15.0, shipped on 10 December.— Apache Log4j security advisory

04The fixes, in order

The first patch was not the last. Follow-up research found gaps, and each release closed one. If you are reading a version number off a server, this is the table to compare it with:

ReleaseWhat changed
2.15.0Message lookups off by default, JNDI restricted. Later found incomplete in some non-default configurations (CVE-2021-45046).
2.16.0Message lookups removed entirely, JNDI disabled by default.
2.17.0Fixes a denial of service through recursive lookups (CVE-2021-45105).
2.17.1Fixes code execution through the JDBC appender when an attacker can change the logging configuration (CVE-2021-44832).

For applications stuck on old Java versions, the same fixes were back-ported: 2.12.4 for Java 7 and 2.3.2 for Java 6.

Common misconception

Setting log4j2.formatMsgNoLookups=true was the first mitigation everyone shared. Apache later withdrew it as a complete fix, because other code paths could still trigger a lookup. Upgrading, or removing the lookup class, are the measures that hold.

Was Log4j 1.x affected?FAQ

Not by this CVE — the 1.x line has no message lookup mechanism. That is not a reason to keep it: Log4j 1.x reached end of life in 2015 and has its own unfixed issues, including CVE-2021-4104 in the JMS appender when it is configured in a particular way.

05Checking your own systems

Find every copy of the library

Start with the filesystem. Remember that application archives nest jars inside jars, so a clean result here is a first pass, not a verdict — a dependency scanner will see what find cannot.

Compare the version

For this CVE, log4j-core from 2.0-beta9 up to and including 2.14.1 is vulnerable. Aim for 2.17.1 or later rather than the first fixed release.

Upgrade, or remove the class

If an upgrade has to wait, deleting the JNDI lookup class from the jar removes the vulnerable code path. Restart the service afterwards — the old class stays loaded until you do.

Look for attempts, then for success

Lookup strings in your logs show that someone tried. Unexpected outbound connections from the application server show that it worked.

bash
# 1. every copy of log4j-core on this machine
find / -name "log4j-core-*.jar" 2>/dev/null

# 3. stop-gap when you cannot upgrade yet (official mitigation)
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

# 4. attempts recorded in the logs; obfuscated variants nest other lookups
grep -rIE '\$\{jndi:' /var/log 2>/dev/null | head

The last check is about the network. An application server has very few reasons to open an LDAP or RMI connection to the internet, so watching for one is a cheap and reliable signal:

bash
# outbound LDAP / RMI from this host — on a web server this should stay silent
sudo tcpdump -i eth0 -nn 'tcp port 389 or tcp port 1389 or tcp port 1099'
Defense note

Default-deny egress would have turned Log4Shell from code execution into a failed connection on most servers. It is the control that keeps working against the next bug of this shape, whatever library it is in.

06What to take from it

Log4Shell was not an exotic memory-corruption bug. It was a convenience feature evaluating text that came from outside, in a component everyone considered passive. The specific fix is a version number; the durable lessons are about knowing what you run and limiting what it can reach.

Mitigation summary
Run Log4j 2.17.1 or later (2.12.4 on Java 7, 2.3.2 on Java 6). It closes this CVE and the three that followed.
Know your dependencies. Keep a software bill of materials so the next "are we affected?" takes minutes, not days.
Deny outbound traffic by default from servers, and alert on the exceptions.
Treat everything you log as untrusted input. A logger, a template engine and a query builder all parse what you hand them.

references

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Web →

discussion

1 comment
`code` supported · comments are reviewed before they appear
adminAUTHOR
Corrections are welcome here. If a version number or a detail in this writeup is out of date, say so and it gets fixed.