Prancer Blog / SwarmHack Network Pentest
Build Your Own Autonomous Pentest Lab in 5 Minutes
A hands-on Docker Compose lab with two networks, one dual-homed host and an internal-only DVWA — the exact environment used to validate SwarmHack's network kill chain.
SwarmHack Team · 2026-05-04 · 7 min
TL;DR
- A two-network Docker lab that mirrors a real internet-facing app + isolated internal segment
- An internet-facing Target A (Apache + PHP + MySQL + SSH) with realistic vulnerabilities
- An internal Target B (DVWA) reachable only by pivoting through Target A
- Everything you need to follow along with the full autonomous kill chain in Part 2
This is Part 1 of 3 in a hands-on series that walks through a real autonomous pentest engagement against a multi-host Docker lab. By the end of this post you'll have the exact same lab the SwarmHack team uses to validate the network pentesting kill chain — internal: true segmentation included.
Series roadmap:
- Part 1 — Build the lab (you are here)
- Part 2 — Run the engagement (one command, full kill chain)
- Part 3 — Under the hood (architecture, agents, why not LLMs)
1. Why a multi-host lab?
Most "vuln labs" are a single container with a vulnerable web app. That's fine for learning XSS, but it tells you nothing about the part of pentesting that actually matters to defenders: **what happens *after* the first exploit lands**.
Real attackers don't stop at a reflected XSS. They:
1. Pop a web shell or leak credentials from a .env file 2. Pivot via SSH into the internal network 3. Discover internal-only systems 4. Tunnel back through the compromised host 5. Compromise the crown jewels that were never internet-facing
To test (and demo) that chain, you need at least two hosts and enforced network segmentation — not "we promise the firewall blocks it", but Docker actually dropping the packets.
2. The topology
<diagram title="Two-network segmentation — one bridge with no external route">
┌────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ swarmhack spawn --target http://localhost:8880 │
└──────────┬─────────────────────────────────────────────────┘
│ Ports: 8880 (HTTP), 2222 (SSH), 33060 (MySQL)
═══════════╪═══════════════════════════════════════════════════
│ external_net (172.20.0.0/24, bridge)
│
┌──────────┴────────────────────┐
│ TARGET A │ Internet-facing
│ Apache 2.4.41 + PHP + MySQL │ Dual-homed (eth0 + eth1)
│ + OpenSSH │
│ eth0: 172.20.0.10 │
│ eth1: 172.20.1.10 │
└──────────┬────────────────────┘
│
═══════════╪═══════════════════════════════════════════════════
│ internal_net (172.20.1.0/24, internal: true)
│ Docker drops every outbound packet
┌──────────┴────────────────────┐
│ TARGET B │ Internal-only DVWA
│ vulnerables/web-dvwa:latest │ No external route, no port mapping
│ eth0: 172.20.1.20 │
└───────────────────────────────┘
</diagram>
The magic ingredient is one Docker Compose flag:
internal_net:
driver: bridge
internal: true # Docker drops all outbound packets
Once you set that, Target B has zero internet access. The only path to it is *through* Target A — exactly like a real DMZ to internal-network hop.
3. Prerequisites
- Docker Desktop (or Docker Engine + Compose v2) — macOS, Linux, or WSL2
- About 1 GB of disk for the two images
- The
swarmhackbinary if you want to run Part 2 — but you can build the lab today regardless
That's it. No nmap, no Metasploit, no kernel modules.
4. The Docker Compose file
Create a folder pentest-lab/ and drop this in as docker-compose.yml:
networks:
external_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
internal_net:
driver: bridge
ipam:
config:
- subnet: 172.20.1.0/24
internal: true # 🔒 no outbound packets
services:
target-a: # Internet-facing, dual-homed
build: ./target-a
container_name: swmhk-target-a
networks:
external_net:
ipv4_address: 172.20.0.10
internal_net:
ipv4_address: 172.20.1.10
ports:
- "8880:80" # HTTP — the web app
- "2222:22" # SSH — your pivot point
- "33060:3306" # MySQL — backend DB
target-b: # Internal-only DVWA
image: vulnerables/web-dvwa:latest
container_name: swmhk-target-b
networks:
internal_net:
ipv4_address: 172.20.1.20
environment:
- SECURITY_LEVEL=low
- PHPIDS_ENABLED=0
Target A sits on both networks. From the outside it looks like a normal web server. From the inside, it's the gateway every pentester dreams of: one box that touches both segments.
5. Target A — what's intentionally broken
Target A is a custom Ubuntu image running Apache + PHP. It's deliberately seeded with the kind of mistakes you actually find in real audits:
| Endpoint | Vulnerability | Why it matters |
| ---------- | --------------- | ---------------- |
| /ping.php | OS Command Injection (host param) | Full RCE as www-data |
| /search.php | Reflected XSS (q param) | Classic, no encoding |
| /login.php | Session Fixation on PHPSESSID | Token never regenerated |
| /admin.php | XXE + SQLi + CSRF, no auth | Multi-class single endpoint |
| /info.php | phpinfo() exposed | Free recon |
| /.env | Stripe keys, SSH creds, internal IPs | The credential goldmine |
| /.git/config | Repo URL leak | Source-code recon |
The .env file is the star of the show. It contains real-looking values:
DB_HOST=localhost
SECRET_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dc
STRIPE_API_KEY=sk_test_51ABCDeFgHiJkLmNoPqRsTuVwXyZ
INTERNAL_API=http://172.20.1.20/api/v1
SSH_USER=pentest
SSH_PASS=pentest123
Plus two SSH accounts:
root:toorpentest:pentest123— withsudo NOPASSWD: ALL(instant root once you have a shell)
This setup is realistic on purpose. Hardcoded credentials in .env files are still one of the most common findings in real pentests.
6. Target B — the crown jewel you can't reach directly
Target B is just vulnerables/web-dvwa:latest:
SECURITY_LEVEL=lowandPHPIDS_ENABLED=0so every DVWA module is wide open- No port mapping to your host —
docker compose pswon't show a published port - No outbound route —
internal: trueblocks egress
If you try curl http://172.20.1.20 from your host, it fails. The only way in is to go *through* Target A.
7. Stand it up
mkdir -p pentest-lab && cd pentest-lab
# Save the docker-compose.yml from section 4 here
# Then build target-a (Dockerfile + .env + PHP files)
docker compose up -d --build
sleep 30 # let Apache, MySQL, SSH all come online
Sanity checks:
# Web app reachable from your host
curl -s http://localhost:8880/ | head -5
# SSH listener up
nc -zv localhost 2222
# Target B is NOT directly reachable — this should fail
curl --max-time 3 http://172.20.1.20/ || echo "✅ correctly blocked"
# But Target A *can* reach Target B (via internal_net)
docker exec swmhk-target-a curl -sI http://172.20.1.20/login.php | head -1
If those four commands behave as labelled, your segmentation is real and the lab is ready.
This lab is deliberately vulnerable. Run it on a laptop or an isolated VM — never on a host that's exposed to the internet, and never on a corporate network where someone might mistake 172.20.1.20 for production.
8. Tear it down when you're done
docker compose down -v
-v removes the volumes too — the lab is fully ephemeral and safe to spin up again from scratch.
What's next
In Part 2 — Run the Engagement we'll point a single swarmhack spawn command at http://localhost:8880 and watch it:
1. Find the command injection on ping.php 2. Read your .env file via the shell it just popped 3. Correlate the SSH credentials it found 4. Open an SSH session, confirm the dual-homed pivot 5. Build a tunnel to 172.20.1.20 6. Re-scan DVWA *through* that tunnel — and report the internal CVE
All from one command. No human in the middle. We'll also walk through the 11 findings, 35 crown jewels, and 6m 7s wall-clock time the team measured across 11 consecutive runs.
See you in Part 2.