Carlos A. Rojas

A javascript-free experience!

Building a Disposable Web App Vuln Lab

Isolated, reproducible, destroyable

Lab / Educational

Status
Lab / Educational
Published
2024-01-26
Difficulty
Intermediate
Est. time
60-90 min
Topics
Testing, Web, Lab
Scope & authorization: Everything here runs on virtual machines you own, on an isolated host-only network with no route to the internet or any third-party system. Never point any tool or payload at a host you do not own or have explicit written authorization to test.

Learning objectives

By the end of this lab you will be able to stand up a throwaway, network-isolated environment for reproducing a web application issue; confirm whether a finding is actually exploitable as deployed; observe the telemetry the activity generates; and destroy the whole environment cleanly so nothing lingers. The point is not any single exploit — it is building the safe workbench that every honest assessment depends on.

Required knowledge & prerequisites

  • Comfort with a Linux shell and basic networking (IP addressing, DNS, HTTP).
  • A host with a hypervisor: VirtualBox, VMware, KVM/libvirt, or Hyper-V.
  • Roughly 8 GB free RAM and 30 GB disk for two small VMs.
  • Installation media for a minimal Linux server (examples below use Debian 12).

Versions used in this write-up (illustrative). Debian 12.5, Docker 24.x, and a deliberately vulnerable training application (e.g. OWASP Juice Shop or DVWA) pulled from its official image. Verify current versions and image digests yourself before running — this guide was written without live access to check today's releases.

Lab topology & isolation

Two VMs on a single host-only (or “internal”) network. No NAT, no bridged adapter, no default gateway to the internet. This is the single most important control in the whole lab: if the vulnerable app cannot reach the internet, a mistake cannot escape into it.

# Conceptual layout (host-only network, e.g. 10.10.10.0/24)
#   [ attacker-vm  10.10.10.10 ]  <-- your tooling (curl, browser, scanner)
#   [ target-vm    10.10.10.20 ]  <-- the deliberately vulnerable app
#
# Host adapter is NOT attached to these VMs for internet access.
# Only attach a temporary NAT adapter to install packages, then REMOVE it.

The staged trick: attach a temporary NAT/bridged adapter only long enough to install packages and pull images, then detach it and switch to host-only before any testing begins. Document this as a deliberate step so it is repeatable and auditable.

Build steps

1. Target VM — install the vulnerable app in a container (keeps it contained and trivially resettable):

# On target-vm, with the temporary NAT adapter attached:
sudo apt-get update && sudo apt-get install -y docker.io
sudo systemctl enable --now docker

# Pull an OFFICIAL deliberately-vulnerable training image.
# (Confirm the current image name/tag from its official project page first.)
sudo docker pull bkimminich/juice-shop:latest     # illustrative tag

# Now DETACH the NAT adapter and switch the VM to host-only networking.
# Run the app bound to the host-only interface only:
sudo docker run -d --name juice -p 10.10.10.20:3000:3000 bkimminich/juice-shop

2. Attacker VM — minimal tooling. You do not need a giant distro; a plain Debian with curl, a browser, and one scanner is enough to practice the triage workflow from the finding vs. exploitability article.

# On attacker-vm:
curl -s http://10.10.10.20:3000/ -o /dev/null -w "%{http_code}\n"   # expect 200

Expected observations

These outputs are illustrative, not captured from a live run. A reachable target returns HTTP 200 on its landing page. A scanner will flag a list of candidate issues. Your job is then to pick one and actually prove it — e.g. submit a crafted input and confirm the application returns the result that a real vulnerability would produce, rather than trusting the scanner's signature match.

Why it works

Containment comes from two independent layers: the network (host-only, no egress) and the process (the app runs in a container you can discard). Neither alone is sufficient — a container with internet access can still exfiltrate or be abused as a pivot, and a host-only VM running the app directly is hard to reset cleanly. Together they give you a workbench that is both isolated and disposable, which is exactly what distinguishes a lab from a liability.

Defensive visibility

Offensive lab activity is also a telemetry generator. If you run a log pipeline (this is a natural place to use the Wazuh/OSSEC rule lookup tool), watch what your own attacks produce: spikes in 4xx/5xx responses, anomalous user-agents, repeated auth failures, and unusual request patterns. Knowing the signature of an attack from the inside is what lets you later recognize it in production logs.

Troubleshooting

  • Target unreachable: confirm both VMs are on the same host-only network and the app is bound to the host-only IP, not 127.0.0.1.
  • App can reach the internet: you left a NAT/bridged adapter attached. Detach it. Re-verify with curl -s https://example.com timing out.
  • Container won't start: check sudo docker logs juice and that the port isn't already bound.

Cleanup & reset

# Reset the app to a pristine state:
sudo docker rm -f juice
sudo docker run -d --name juice -p 10.10.10.20:3000:3000 bkimminich/juice-shop

# Full teardown: delete the container, image, and finally the VMs/snapshots.
sudo docker rm -f juice
sudo docker image rm bkimminich/juice-shop
# Then delete the VM snapshots / disks from your hypervisor.

Snapshot the clean state before you start testing so a reset is one click, and delete the VMs entirely when the exercise is over.

Limitations & further reading

A lab proves exploitability in the lab. It does not prove your production deployment is identically configured — version drift, WAFs, and custom hardening all change the picture, which is why reproduction is one input to a judgment, not the whole verdict. Pair this with the triage workflow in Scanner Finding vs. Proven Exploitability.

Update 2024-01-26: Initial publication.