Several machines, each running a vulnerable service. Break in, escalate to root, plant your team beacon, and hold the box while everyone else tries to take it from you.
King of the Hill is attack-and-defend. Points accrue for the time you hold a hill (a periodic tick) plus captured flags and first bloods. The entry vulnerability and the privilege-escalation path are restored on a schedule, so a box can never be permanently patched shut. Defense has to be active: monitor, kick other teams' sessions, and overwrite their beacon.
Collector, tick engine, SLA prober, flag service with first blood, live dashboard, team portal, and a green-team admin panel. SQLite + http.server + HMAC.
Runs on a hill, reads /root/king.txt, validates it, signs a report, and reports the owner to the scoreboard.
Bring the scoreboard and the hills up on real machines over SSH, or run the whole thing locally with Docker.
Each hill is a self-contained vulnerable box: a foothold vulnerability, then a local privilege escalation to root. Every one is verified end to end by the smoke test.
A network-diagnostics panel runs connectivity checks on a value you supply. It reaches a shell.
An online calculator evaluates expressions server-side with no sandbox, so the field is code execution.
An admin panel with a weak password from common wordlists; the post-login console runs commands.
A CI script console is unlinked from the UI and behind weak HTTP auth. Find it, authenticate, run code.
One host with Docker. Bring up the four hills, then run the scoreboard against the sample config.
# bring up the four vulnerable hills for d in deploy/hills/*/; do (cd "$d" && docker compose up -d --build); done # run the scoreboard (hills reachable on 8081-8084) cd scoreboard KOTH_CONFIG_DIR=../config KOTH_FLAGS=../flags/flags.json \ KOTH_BIND=0.0.0.0:8000 KOTH_ADMIN_KEY=change-me KOTH_TEAM_PASS=change-me \ python3 scoreboard.py # open the scoreboard, team portal, and green-team panel http://localhost:8000/ # scoreboard + team login http://localhost:8000/team # reveal your beacon by access code http://localhost:8000/admin # green-team control (pause, rotate, revert, adjust)
Want a scripted demo without deploying anything? python3 poc/simulate.py runs the
scoring scenarios end to end. To verify the challenges, bash deploy/smoke-test.sh.
A hill is nothing more than a docker-compose project under deploy/hills/ that
honours a small contract, plus two config entries. Because it is just a compose project, an existing
dockerized target slots in with almost no glue. This section shows both: the contract for a hill from
scratch, then onboarding an existing project using the real HackWars web CTF as the example.
The box grants a shell to an unprivileged user (that is user.txt), with a planned local escalation to root (that is root.txt). This is what makes the hold mechanic possible.
/root/king.txt, owner root:root, mode 600, empty at start. Taking the hill is writing your team token there, which needs root. A host-side beacon agent reads it and the tick engine scores you.
Building one from scratch means following any of the four shipped hills: a Dockerfile, an
entrypoint.sh that plants the flags and an empty king.txt, a vulnerable
server.py, a sudoers.golden privesc, and a reset.sh that revives the
vuln without touching king.txt.
HackWars: The Vulnerabilities Strike Back is a real, already-dockerized Flask web CTF (a Star Wars themed jeopardy game: view-source, XSS, SQL injection, broken access control). It is a good example because you change almost nothing to run it on KOTH.
docker-compose.yml (one Flask service on :8000). No rewrite, no fork.config/hills.json so the scoreboard and SLA prober know it exists, and drop its flags into flags/flags.json. HackWars already ships every flag in flags.txt and ctfd/challenges.yml, so this is a copy.# config/hills.json: one entry points KOTH at the existing service {"id":"hackwars","name":"HackWars","service_host":"127.0.0.1","service_port":8000,"hmac_key":"changeme-hackwars","url":"http://localhost:8000"} # run the existing project unchanged, then the scoreboard alongside it cd path/to/CS50-Hackwars/project && docker compose up -d --build cd scoreboard && KOTH_CONFIG_DIR=../config KOTH_FLAGS=../flags/flags.json python3 scoreboard.py
If the box grants a shell and a root path, add the king.txt convention and point the host-side beacon agent at its container. Teams then take and hold it, and the tick engine scores ownership. Best for targets with RCE and a privesc.
A web CTF like HackWars is solved for flags, not held, so you score it through the flag service (first blood included) and skip the agent and king.txt. The same flags can also be lifted into a RedutaCTF event through the plugin.
KOTH's core loop is attack-and-defend, so a pure jeopardy app like HackWars plugs in for
its flags, while a box with a root foothold can additionally be held. Either way the point is how little
glue an existing, already-dockerized project needs: one hills.json entry, its own compose, and
its flags. HackWars has its own page at hackwars.cyberjuly.com.
Two layers plug together. Inside KOTH, agents and the scoreboard decide who holds each hill. Outside, an optional bridge feeds that into the RedutaCTF platform so a King of the Hill game can score alongside jeopardy challenges on one scoreboard.
Runs on the host, reads the hill's king.txt (via docker exec or a no-follow open), validates the token, and HMAC-signs a report with a per-hill key and a nonce.
The scoreboard verifies the signature, rejects replays and symlink swaps, and every few seconds awards the current holder of each hill a tick, plus flag points and first bloods.
An out-of-band prober checks each service is still up (defending by breaking it costs points), and a green-team panel can pause, rotate keys, revert, or adjust.
Because the signing key lives on the host and the token is validated by exact match, rooting a hill does not let a team forge ownership. Keys rotate from the green-team panel on suspicion of a leak.
RedutaCTF treats King of the Hill as an out-of-process plugin: a small HTTP service that the core
talks to over signed webhooks and a token-scoped awards API. The reference koth-plugin that
ships with RedutaCTF is the bridge.
timestamp + "." + body in X-Reduta-Signature, with X-Reduta-Delivery for idempotency.tick.minute the plugin looks up who currently holds each target (from KOTH's scoreboard state) and posts an award to the core.ref_id, so a redelivered tick never double counts.# the plugin awards the current holder, once per tick per target POST http://core:8080/api/v1/plugin/v1/awards Authorization: Bearer plg_... {"event_id":"...","team_id":"<current holder>","points":5, "ref_id":"tick:2026-09-02T10:00:00Z:hill-1","reason":"KotH tick hill-1"}
The bridge is configured with environment variables, and the only KOTH-specific part is mapping a KOTH team to a RedutaCTF team and reading the current holder from the KOTH scoreboard:
KOTH_WEBHOOK_SECRET # shared secret for verifying core webhooks KOTH_CORE_URL # RedutaCTF core, e.g. http://server:8080 KOTH_PLUGIN_TOKEN # plg_... token issued when the plugin is registered KOTH_EVENT_ID # the RedutaCTF event this game scores into KOTH_TARGET # which hill/target this bridge scores
Verify a bridge before an event with reduta plugin verify, which checks it accepts signed
webhooks, is idempotent on redelivery, and rejects bad signatures. The platform side of this contract is
documented in the RedutaCTF wiki.
Everything here is intentionally vulnerable. It is meant for an isolated lab
or CTF network. Do not expose these services to the public internet and do not run them
on machines you care about. Flags use the CTF{...} format.