Skip to content
Z3tra
All articles
3 min read

Building a CTF platform taught me more security than solving CTFs

Running code that is designed to be attacked forces a kind of threat modelling that solving challenges never does. Notes from building Hackuten.

I have solved a fair number of capture-the-flag challenges. I learned more about security in the first month of building a platform to host them than in any comparable stretch of solving them — and the reason is simple. When you solve a challenge, you attack a box someone else secured. When you host one, you run code written specifically to attack you.

That inversion changes everything about how you think.

Hostile input is the product

In a normal web application, malicious input is an edge case you defend against. In a CTF platform, malicious input is the entire point. Players should be running exploits. Some of them will, entirely reasonably, try to escape the challenge and reach the platform itself — because that is a more interesting flag.

So the threat model is not "what if someone sends something bad". It is "assume every challenge container is already compromised, and design so that it does not matter".

What that forces you to learn

Container isolation stops being a checkbox and becomes a set of independent controls, each removing a different escape route.

  • No outbound network by default. A challenge that does not need the internet does not get it. This alone removes most pivoting and exfiltration.
  • Non-root, dropped capabilities. The process inside runs as nobody, with the kernel capabilities it will never use stripped away.
  • Read-only filesystem where possible. If the challenge does not need to write, it cannot.
  • Hard resource limits. CPU and memory ceilings, because "denial of service against the host" is a boring but real escape.
  • Short TTLs. An instance that lives for minutes is a much smaller target than one that lives for days.

None of these are exotic. What was new to me was the discipline of treating them as layers — assuming each one will individually fail, and checking that the next one still holds.

The scoreboard problem

Not everything interesting was about isolation. The scoreboard turned out to be a genuinely instructive distributed-systems problem hiding in plain sight.

During a competition everyone submits at once, and the leaderboard has to be correct — a double-counted solve or a lost first-blood is the kind of thing that ends the community's trust in the platform.

The design I landed on stores solves as immutable events rather than mutable counters:

-- One row per (player, challenge). The constraint does the integrity work.
CREATE TABLE solves (
  player_id   BIGINT NOT NULL,
  challenge_id BIGINT NOT NULL,
  solved_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (player_id, challenge_id)
);

The uniqueness constraint means a resubmission cannot double-score — the second insert simply fails. And because the scoreboard is derived from these events rather than stored as a running total, a scoring bug is fixable retroactively by recomputing, instead of being permanently baked into a counter nobody can safely reset.

Seeing both sides

The thing I did not expect is how much building this changed how I attack other applications. Having written the vulnerable-by-design side, I now recognise the shortcuts — because they are the same ones I was tempted to take under deadline.

The developer who exposed one extra field to unblock the mobile team, the endpoint that checks the first element of a batch and trusts the rest, the log line that quietly became a plaintext archive of things that should never have been logged. I have been that developer. That is exactly why I know where to look.

Related

Research4 min

Encrypting content is easy. Hiding who talks to whom is not.

Notes from designing Orbyte: why metadata is often more sensitive than message content, and the spectrum of defences between 'we encrypt messages' and actual metadata resistance.

  • Research
  • Web Security
  • Cryptography
Research4 min

When being offline is the emergency: designing a reliable dead man's switch

Notes from Batelys on building a lone-worker alert system, where availability is a safety property and a missed check-in has to be as loud as a pressed button.

  • Research
  • Linux
  • Architecture
Article3 min

Understand the application before you test it

Scanners find what they were told to look for. The bugs that matter live in the gap between what an application believes about itself and what it actually enforces.

  • Web Security
  • OWASP
  • Methodology