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 20 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 twenty hills

Each hill is a self-contained vulnerable box: a foothold vulnerability, then a local privilege escalation to root. Hills 5-20 (LOTR, Tron, Avatar, and Harry Potter themed) are new and ship as an alpha: validated by the smoke test, but not yet battle-tested in a live event. All are verified end to end by the smoke test except hill-15, whose escalation is a host escape (see its note) and is build-verified only.

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
Hill 5 // Doors of Durin alpha

Repair-gated command injection

LOTR / Moria. The box boots in rescue mode; repair the bootloader record through the rescue console to bring the service online, then the delving console reaches a shell.

foothold: repair + cmd injectionprivesc: sudo find
Hill 6 // Palantir of Orthanc alpha

XXE file read

LOTR. An XML vision parser resolves external entities. Read arbitrary files, leak the attunement key, and unlock the command console.

foothold: XXEprivesc: sudo awk
Hill 7 // Barad-dur Watchtower alpha

JWT alg:none bypass

LOTR / Mordor. The verifier trusts the token's alg field. Forge an unsigned admin token for command execution, then hijack a writable PATH dir run by a root cron.

foothold: JWT bypassprivesc: PATH hijack
Hill 8 // Isengard Forge alpha

Weak CI credentials

LOTR. A build console runs submitted scripts behind guessable foreman creds. A python copy on the box carries the cap_setuid capability.

foothold: weak creds RCEprivesc: cap_setuid
Hill 9 // Grid Portal (I/O Tower) alpha

Server-side template injection

Tron. The program portal renders your name through a naive template engine that evaluates the expression between double braces.

foothold: SSTIprivesc: sudo sed
Hill 10 // MCP Core alpha

Pickle deserialization

Tron. The Master Control Program restores a serialized session object with pickle, so a crafted object is remote code execution.

foothold: insecure deserializationprivesc: SUID find
Hill 14 // Avatar Link Unit alpha

SQL injection auth bypass

Avatar. The biometric link login builds its SQL by string formatting; inject to bypass auth and reach the operator console.

foothold: SQLiprivesc: sudo perl
Hill 17 // Hogwarts Portal alpha

Template injection (spells)

Harry Potter. The house-points portal evaluates the expression between double braces. A different engine and escalation than hill 9.

foothold: SSTIprivesc: sudo python3
Hill 19 // Ministry of Magic alpha

SSRF and LFI relay

Harry Potter. The Owl Post relay fetches any URL and honours file scheme; leak the internal token, then drive the dispatch console.

foothold: SSRF + LFIprivesc: root cron
Hill 11 // Light Cycle Arena alpha

Unauthenticated debug API

Tron. A forgotten debug endpoint runs commands with no auth; a root loop then executes the ExecStart line of a world-writable unit file.

foothold: unauth RCEprivesc: writable unit
Hill 13 // RDA Ops Console alpha

Command injection

Avatar. The sensor diagnostics field is concatenated straight into a shell command.

foothold: command injectionprivesc: sudo dd
Hill 16 // Unobtainium Refinery alpha

Repair, then eval RCE

Avatar. The refinery boots offline; repair the coolant mount, then the calibration formula is evaluated server-side.

foothold: repair + evalprivesc: sudo env
Hill 18 // Gringotts Vault alpha

Repair, then YAML deserialization

Harry Potter. Rebuild the vault ledger, then the vault door parses a manifest with an unsafe YAML loader.

foothold: YAML deserializationprivesc: writable sudoers.d
Hill 12 // Flynn's Arcade alpha

Repair, then LFI + log poisoning

Tron. Fix the boot record, then a local file include plus a poisonable access log lead to RCE; a root cron runs a hijackable PATH.

foothold: LFI + log poisoningprivesc: PATH hijack
Hill 15 // Eywa Network build-only

SSRF, then Docker-socket host escape

Avatar. SSRF leaks internal creds for RCE; the escalation is an exposed Docker socket (full host takeover), so this box is build-verified only and run on a disposable host.

foothold: SSRFprivesc: docker.sock escape
Hill 20 // Room of Requirement alpha

Repair, then hidden debug RCE

Harry Potter. Clear the emergency, then a hidden endpoint runs commands once you speak the passphrase; a capability bypasses file permissions.

foothold: hidden debug RCEprivesc: cap_dac_override

The full 20-challenge design roadmap is now built (LOTR, Tron, Avatar, and Harry Potter sets), including the repair-to-capture pattern and bonus ideas for going further.

Quick start

One host with Docker. Bring up the 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.