Introduction

GZ::CTF is an open-source Capture-The-Flag platform built on ASP.NET Core. Upstream it gives organizers highly customizable jeopardy challenges (Static Attachment, Dynamic Attachment, Static Container, Dynamic Container), dynamic scoring with first-blood bonuses, on-demand container distribution over Docker or Kubernetes, real-time scoreboard and submission monitoring over SignalR, TCP-over-WebSocket traffic proxying, Redis/PostgreSQL backends, and object-storage support. For anything that ships in upstream unchanged, the canonical reference is the official documentation at gzctf.gzti.me.

This site documents a fork of that platform. The fork keeps the entire jeopardy platform intact and adds a live, tick-based competition engine on top of it: an Attack & Defense (A&D) mode and a King of the Hill (KotH) mode. These are the additions documented here — everything else behaves as upstream.

Info

This documentation covers the fork's additions only. The platform core remains AGPLv3 (GZTimeWalker/GZCTF); see License below.

Two repos you'll use

The docs are built around two companion repositories:

  • gzctf-platform-template — the deploy scaffold. An interactive wizard plus make targets bring up GZCTF (the published image) behind Traefik + Let's Encrypt TLS, with config in appsettings.json. Start here: Quick start.
  • TCP1PADTesting — ready-to-import example challenges (two A&D, two KotH; OWASP web + PWN). You import it via an admin Repo Binding to populate a game. See Repo bindings. :::

What this fork adds

The fork introduces two new challenge types alongside the four jeopardy types. Both are defined in src/GZCTF/Utils/Enums.cs as members of the ChallengeType enum:

TypeEnum valueShapeOne-line summary
AttackDefense0b100 (4)One container per teamEach team gets a persistent service container that lives for the whole game; teams attack each other's services and defend their own.
KingOfTheHill0b101 (5)One shared containerAll teams attack a single shared container; whoever's rotating token sits in the marker controls "the hill" each tick.

Both types run on the same shared infrastructure — referred to throughout this documentation as the AdEngine: rounds (ticks), live checkers, per-tick scoring, and a dedicated scoreboard. The two modes differ only in topology (per-team service vs. one shared hill) and in how points are awarded each tick.

Attack & Defense

From the ChallengeType.AttackDefense definition in Enums.cs:

Each team gets a persistent container per A&D challenge that lives for the whole game; teams attack each other's services on a shared network, defend their own, and score on attack + defense + SLA per tick.

A&D doesn't fit the historical static/dynamic × attachment/container matrix, so the type helpers treat it explicitly (see The mental model below).

:::tip Teams reach their own and opponents' A&D services over a WireGuard VPN, so the live network is exposed only to participants in the game.

King of the Hill

From the ChallengeType.KingOfTheHill definition in Enums.cs:

ONE shared container per challenge that all teams attack; each tick the team whose rotating token sits in the marker (/koth/king) controls it and earns hold points, and is penalized if the service breaks. Every few ticks the hill resets to base and the per-challenge score leader is briefly network-blocked. Reuses the A&D round/checker/scoreboard engine — handled by explicit cases like A&D.

So KotH reuses the same round/checker/scoreboard machinery as A&D, but instead of a service per team there is a single hill that teams fight to occupy.

The per-tick checker contract

Each tick, the engine runs a checker against the relevant service container(s). The checker reports one of four statuses, defined by the AdCheckStatus enum in Enums.cs (compatible with the enochecker3 contract):

StatusValueMeaning
Ok0Checker succeeded — flag planted and retrieved.
Mumble1Service is up but behaving incorrectly (flag mismatch, partial outage, etc.).
Offline2Service didn't respond at all (TCP refused, timeout).
InternalError3The checker itself failed (bug in checker code, host died).

The mental model

The most important idea for organizers: a single game can mix all three families of challenge at once — jeopardy challenges, A&D challenges, and KotH challenges — within the same game and the same scoreboard infrastructure. Nothing forces a game to be "an A&D game" or "a jeopardy game."

What decides whether a given challenge is driven by the live engine is a single helper, UsesAdEngine(), defined on ChallengeType in src/GZCTF/Utils/Enums.cs:

/// <summary>
/// Does it run on the shared A&D engine (rounds, ephemeral checker,
/// per-tick scoring, scoreboard) — Attack & Defense or King of the Hill.
/// </summary>
public bool UsesAdEngine() => type is ChallengeType.AttackDefense or ChallengeType.KingOfTheHill;

If UsesAdEngine() is true, the challenge is routed to the AdEngine; otherwise it follows the normal jeopardy path. The two new types also participate in the existing type predicates, which were extended to recognize them:

// A&D is per-team (dynamic-ish) and uses containers, so
// IsDynamic() and IsContainer() both return true for it (and for KotH).

public bool IsDynamic() => type is ChallengeType.DynamicAttachment or ChallengeType.DynamicContainer
    or ChallengeType.AttackDefense or ChallengeType.KingOfTheHill;

public bool IsContainer() => type is ChallengeType.StaticContainer or ChallengeType.DynamicContainer
    or ChallengeType.AttackDefense or ChallengeType.KingOfTheHill;

public bool IsAttackDefense() => type is ChallengeType.AttackDefense;
public bool IsKingOfTheHill() => type is ChallengeType.KingOfTheHill;

The practical consequences of these helpers:

HelperAttackDefenseKingOfTheHillWhy it matters
IsStatic()falsefalseNeither uses a single shared flag-set.
IsDynamic()truetrueBoth are treated as per-team/dynamic.
IsAttachment()falsefalseNeither is an attachment challenge.
IsContainer()truetrueBoth spin up containers, so container-management paths apply.
UsesAdEngine()truetrueThe routing switch into the live engine.
Warning

AttackDefense and KingOfTheHill use enum values 0b100 (4) and 0b101 (5), which fall outside the original 2-bit static|dynamic × attachment|container layout used by the four jeopardy types. They are deliberately handled by explicit cases in ChallengeTypeExtensions rather than by bit math. If you add code that switches on ChallengeType, make sure to handle these two values explicitly.

When authoring, you choose the engine for a challenge by setting its ChallengeType to AttackDefense or KingOfTheHill (the values are serialized as those strings). For example, in a challenge definition:

type: AttackDefense   # or: KingOfTheHill

See Challenge YAML and Templates for the full authoring schema, and appsettings for engine-wide configuration.

Where to next

PageWhat you'll find
Quick startStand it up — deploy GZCTF with the gzctf-platform-template wizard, then import the example challenges and run your first live-engine game.
Attack & DefensePer-team service topology, VPN access, attack/defense/SLA scoring.
King of the HillThe shared hill, the /koth/king marker, hold points, resets, and leader blocking.
ScoringHow per-tick AdEngine scoring works alongside jeopardy dynamic scoring.
TemplatesReady-made challenge skeletons, including enochecker3-compatible checkers.

License

The core platform is licensed under the GNU Affero General Public License v3.0 (AGPLv3) as published by the upstream project, GZTimeWalker/GZCTF. This documentation describes only the A&D and KotH additions made by this fork; for upstream behavior, configuration, and any restricted-component terms, consult the upstream project and its official documentation at gzctf.gzti.me.

Info

Deploying a modified GZ::CTF as a public service carries upstream attribution obligations (retain copyright/attribution, state the version and license, and link to the original repository). Review those requirements in the upstream README before going live.