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 swarmhack binary 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:toor
  • pentest:pentest123 — with sudo 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=low and PHPIDS_ENABLED=0 so every DVWA module is wide open
  • No port mapping to your host — docker compose ps won't show a published port
  • No outbound routeinternal: true blocks 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.