- There is no shell in the container
- Running commands
- Everyday operations
- Where the data lives
- Updating
- Without Compose
- Without make
The image is built FROM scratch. It contains one file: the server binary.
There is no bash, no sh, no ls, no package manager. If you try to open a
shell you get this:
$ docker compose exec app bash
OCI runtime exec failed: exec failed: unable to start container process:
exec: "bash": executable file not found in $PATHThat is expected, and it is the point. A container that stores whatever a target sends it should not also contain tools an attacker could use. Nothing is broken.
Everything you would use a shell for is a subcommand of the binary instead.
The binary lives at /exploit-server inside the container:
docker compose exec app /exploit-server <command>For example:
docker compose exec app /exploit-server config
docker compose exec app /exploit-server instance new --label acme
docker compose exec app /exploit-server instance ls
docker compose exec app /exploit-server log acme --follow
docker compose exec app /exploit-server helpThat is long to type. An alias makes it bearable — wes, for web exploit
server:
alias wes='docker compose exec app /exploit-server'
wes instance ls
wes log acme --followAdd it to ~/.bashrc or ~/.zshrc to keep it. The alias only works from the
directory holding compose.yaml.
To pipe a file in, add -T so Docker does not try to allocate a terminal:
cat payload.html | docker compose exec -T app /exploit-server store acme /exploit -| Task | Command |
|---|---|
| Start | docker compose up -d |
| Stop | docker compose down |
| Restart after a config change | docker compose restart app |
| Follow the server's own output | docker compose logs -f app |
| Is it healthy? | docker compose ps |
| Panel password | docker compose exec app /exploit-server config |
| Everything the CLI can do | docker compose exec app /exploit-server help |
A config change made with config set is written to the volume but read only
at startup, so restart the container afterwards.
In a named Docker volume, not in the project directory. Instances, stored
responses, the access log, captured mail and config.json are all inside it.
docker volume ls | grep exploitdocker compose down keeps it. docker compose down -v deletes it, along with
every instance and every log entry.
To back it up, mount it into a throwaway container that does have a shell:
docker run --rm \
-v exploit-server_data:/data:ro \
-v "$PWD":/out \
alpine:3 tar czf /out/exploit-backup.tar.gz -C /data .To restore:
docker run --rm \
-v exploit-server_data:/data \
-v "$PWD":/in \
alpine:3 tar xzf /in/exploit-backup.tar.gz -C /datadocker compose pull # or: docker compose build
docker compose up -dThe volume is untouched, so instances and logs survive. Back it up first anyway.
The published image runs on its own. Keep the data in a named volume so it survives the container:
docker run -d --name exploit-server \
-p 127.0.0.1:80:80 \
-p 127.0.0.1:8080:8080 \
-v exploit-server_data:/var/lib/exploit-server \
-e EXPLOIT_BASE_DOMAIN=example.com \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--sysctl net.ipv4.ip_unprivileged_port_start=0 \
--read-only --tmpfs /tmp:mode=1777 \
ghcr.io/inneromost/web-exploit-server:latest
docker exec exploit-server /exploit-server config--sysctl net.ipv4.ip_unprivileged_port_start=0 is what lets an unprivileged
user inside the container bind port 80. Without it the server cannot start.
Compose is still the better option: it also creates the isolated network that
deploy/firewall.sh locks down.
Every make target is a short alias. If you do not have make:
| Instead of | Run |
|---|---|
make up |
docker compose up -d |
make down |
docker compose down |
make logs |
docker compose logs -f app |
make image |
docker build -t web-exploit-server:latest . |
make cmd ARGS='instance ls' |
docker compose exec app /exploit-server instance ls |