KING OF THE HILL

A self-hostable attack-and-defend CTF

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.

View on GitHub Quick start Add a hill
Pure Python stdlib Zero dependencies 4 hills + scoreboard Docker

What it is

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.

Scoreboard

Collector, tick engine, SLA prober, flag service with first blood, live dashboard, team portal, and a green-team admin panel. SQLite + http.server + HMAC.

Beacon agent

Runs on a hill, reads /root/king.txt, validates it, signs a report, and reports the owner to the scoreboard.

Deploy scripts

Bring the scoreboard and the hills up on real machines over SSH, or run the whole thing locally with Docker.

The four hills

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.

Hill 1 // NetOps Console

Command injection

A network-diagnostics panel runs connectivity checks on a value you supply. It reaches a shell.

foothold: cmd injectionprivesc: sudo find
Hill 2 // MathLab Compute

Unsandboxed eval()

An online calculator evaluates expressions server-side with no sandbox, so the field is code execution.

foothold: eval RCEprivesc: SUID bash
Hill 3 // CacheCTL Admin

Admin-login brute force

An admin panel with a weak password from common wordlists; the post-login console runs commands.

foothold: brute + consoleprivesc: writable root cron
Hill 4 // BuildHub CI

Weak creds, hidden console

A CI script console is unlinked from the UI and behind weak HTTP auth. Find it, authenticate, run code.

foothold: weak credsprivesc: sudo tar

Quick start

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.

Add a hill, or bring in an existing project

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 contract

A foothold, then a root path

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.

The king.txt convention

/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.

Bring in an existing project: HackWars

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.

  1. Reuse its container as-is. KOTH hills are compose projects, so you point KOTH at the project's own docker-compose.yml (one Flask service on :8000). No rewrite, no fork.
  2. Register it. Add one entry to 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.
  3. Pick how it scores (below).
  4. Bring it up next to the scoreboard and verify.
# 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

Two ways an existing project scores

As a hold (attack and defend)

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.

As flags (jeopardy)

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.

How the integration works

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.

Layer 1: KOTH's own scoring loop

Beacon agent

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.

Collector + tick engine

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.

SLA prober + green team

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.

Layer 2: bridge into the RedutaCTF platform

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.

  1. The core sends the plugin a signed webhook on every minute tick. The plugin verifies it: an HMAC-SHA256 over timestamp + "." + body in X-Reduta-Signature, with X-Reduta-Delivery for idempotency.
  2. On tick.minute the plugin looks up who currently holds each target (from KOTH's scoreboard state) and posts an award to the core.
  3. The award is idempotent by 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.

Documentation

Before you run it

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.