Skip to content

Repository files navigation

Web Exploit Server (WES)

A self-hosted exploit server for authorised web application penetration testing. Stores arbitrary HTTP responses, serves them from a host you control, and logs every request that comes back.

ci release license

The PortSwigger Academy exploit server workflow, built for real engagements rather than labs. Ships as a single static Go binary or a scratch container with no shell, no root and no route off its own bridge.

The control panel

Features

  • Any response, exactly as typed. Head and body, at any path, in three fidelity modes: normal fixes Content-Length, raw emits your head bytes untouched, verbatim inserts nothing at all.
  • Full access log. Source, method, target, status, user agent, and the complete raw request including POST bodies.
  • Email capture. An SMTP sink for password-reset and registration flows.
  • One instance per engagement. Separate hostnames, separate logs, optional expiry, so evidence never mixes.
  • Hostnames you choose. exploit-<id> is a default, not a rule. Serve collaborator-<id>, a bare <id>, or point one instance at legit.com.example.com to test a filter that trusts a name by substring.
  • Payload library. Fifteen to start, covering CSRF, XSS, CORS, clickjacking, CSWSH, postMessage, SSRF, XXE OOB, dangling markup and desync. Editable, with JSON import and export.
  • Scriptable. A JSON API and a CLI that covers everything the panel does.

Quick start

Docker

cp .env.example .env
docker compose up -d
docker compose exec app /exploit-server config    # prints the panel password

The image is FROM scratch and contains only the server binary, so docker compose exec app bash fails — there is no shell in it. Run the CLI directly instead, as above. An alias helps:

alias wes='docker compose exec app /exploit-server'
wes instance ls
wes help              # every command
wes instance help     # just one group

See Running with Docker for logs, backups, updates, and running without Compose.

Binary

make build
sudo install -m 755 exploit-server /usr/local/bin/wes    # optional, but shorter

The edge listens on port 80, which needs privilege. Either grant it once:

sudo setcap cap_net_bind_service=+ep /usr/local/bin/wes
wes run

Or move to unprivileged ports. Use config set rather than flags on run: flags apply to that one process, so URLs printed by other commands would not know about them.

wes config set edge_port 8888 smtp_port 1025
wes run
wes config       # prints the panel password

Either way

Open the panel at http://127.0.0.1:8080/ and log in as admin. Create an instance, and it is served from http://exploit-<id>.localhost/. *.localhost resolves to 127.0.0.1, so there is no DNS to set up.

If a port is already taken the server says which one and exits; change it with wes config set panel_port 9090 or similar.

Throughout the docs wes is the command, whether that is the installed binary or the Docker alias above. Without installing it, use ./exploit-server.

Deploying on a domain

1. DNS

Two records, both pointing at your server. Leave them unproxied — a CDN in front means a WAF filtering your payloads and a proxy rewriting your bytes.

Type Name Value
A example.com your server's IPv4
A *.example.com your server's IPv4

Add matching AAAA records if the host has IPv6. Without them, an IPv6-only target cannot reach you.

If the domain also hosts a website, use a subdomain instead — set EXPLOIT_BASE_DOMAIN=lab.example.com and create the records at lab and *.lab. Everything below works the same way.

2. Choose how TLS is terminated

The server never speaks TLS itself. It serves plain HTTP, and something in front owns port 443. Pick one:

Situation Use
Nothing else on the host uses 443 The bundled Caddy proxy (below)
A website already uses 443 Your existing nginx — see Deployment
HTTP only, no TLS Skip to step 3 and leave EXPLOIT_PUBLIC_SCHEME=http

For the bundled proxy, put a certificate and key at deploy/tls/:

sudo cp /etc/letsencrypt/live/example.com/fullchain.pem deploy/tls/origin.pem
sudo cp /etc/letsencrypt/live/example.com/privkey.pem   deploy/tls/origin.key
sudo chown root:root deploy/tls/origin.*

Important

The proxy runs with all capabilities dropped, so it cannot bypass file permissions. The key must be readable by the user it runs as, which is root. A key owned by anyone else fails with permission denied and the container restarts in a loop.

A Cloudflare Origin Certificate works equally well and never appears in Certificate Transparency.

3. Configure

In .env:

EXPLOIT_BASE_DOMAIN=example.com
EXPLOIT_PUBLIC_SCHEME=https      # only shapes generated URLs
EXPLOIT_TRUST_PROXY=1            # only because the edge is loopback-bound

EDGE_BIND=127.0.0.1              # the proxy reaches it; the internet does not
TLS_BIND=0.0.0.0
TLS_PORT=443

Serving plain HTTP instead? Set EXPLOIT_PUBLIC_SCHEME=http, EDGE_BIND=0.0.0.0 and EXPLOIT_TRUST_PROXY=0, and skip TLS_*.

4. Start it, then deny it any egress

Docker gives every container full outbound access by default. The second command is not optional:

docker compose --profile tls up -d     # omit --profile tls if nginx fronts it
sudo deploy/firewall.sh apply

Open 443 in your host firewall, then check it end to end:

docker compose exec app /exploit-server instance new --label smoke
echo '<h1>it works</h1>' | docker compose exec -T app /exploit-server store smoke /x -
curl https://exploit-<id>.example.com/x

Instances are now served at https://exploit-<id>.example.com/, with no port in the URL.

Keep the panel off the internet. It binds to loopback; reach it over a VPN or an SSH tunnel:

ssh -L 8080:127.0.0.1:8080 user@host

Ports

Service Binds Who talks to it
Edge 127.0.0.1:80 the target, unauthenticated
Panel 127.0.0.1:8080 you, behind HTTP Basic auth
SMTP 127.0.0.1:25 the target's mail sender

The edge is the only service that may face the internet. An unstored path returns a bare 404 that names no software, so a target who stumbles onto the host finds nothing to identify.

Academy serves its panel from the same host as the payload. Set panel_on_edge to match that layout.

Documentation

Document Contents
Running with Docker Commands, logs, backups, updating
Configuration Every setting, hostname patterns, data lifetime
CLI Command reference
Deployment Isolation, firewall, TLS, reachability

Authorised use

This tool is for testing systems you have permission to test. It hosts payloads and records callbacks. It does not scan, exploit automatically, or leave anything behind on a target, and there is no victim-simulation bot: whoever opens your payload is a real person in an agreed scenario.

verbatim mode can emit deliberately malformed responses. Serve those only to infrastructure you are authorised to test.

License

MIT

About

Web Exploit Server (WES) - a self-hosted exploit server for web application penetration testing. Stores arbitrary HTTP responses, serves them from a host you control, and logs every request that comes back.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages