CTF7
PricingFree tierFAQ Sign in

How to create CTF challenges (with Docker templates)

A practical playbook for challenge authors: design principles, a difficulty curve with target solve rates, flag formats and dynamic flags, copy-ready Dockerfiles for web and pwn, isolation settings, playtesting, AI-resistant design and a pre-launch checklist.

To create a CTF challenge, pick one technique you want players to learn, design a single intended solve path with no guessing, and place it on your event's difficulty curve. Then write a clear description, package the service in a hardened Docker container, automate a solve script and have someone else playtest it before launch.

One challenge, followed through every stage. The examples below all build the same easy web challenge, web-notes: a notes API that returns any note by its numeric ID without checking who owns it.

  1. Idea: one bug class (broken object-level authorization, or IDOR) and one lesson.
  2. Tier: easy, aimed at 40 to 70 percent of active teams.
  3. Flag: CTF7{idor_notes_…}, derived per team with an HMAC.
  4. Container: a small Flask app on a pinned base image, running as a non-root user.
  5. Testing: a short solve.py in CI, then a blind playtest by someone who did not write it.
  6. AI pass: a reviewer tries the player-facing package with public assistants and records the result before the tier is final.

CTF challenge design principles

Great challenges feel hard while you're stuck and obvious once you've solved them. Bad ones feel arbitrary. The difference usually comes down to a few principles.

1. One idea per challenge

Decide the single concept the challenge teaches: a JWT alg confusion (the JWT decoder shows what a player sees), a format-string leak, a padding oracle, a Wireshark stream reassembly. Build everything around it. Layering three unrelated tricks doesn't make a challenge harder in an interesting way. It makes it longer and more fragile. If you have three ideas, you have three challenges, or one clearly signposted multi-stage chain.

2. Design the intended path first

Write the solve as a numbered list before you write any code. Each step should give players a signal that they're on the right track: an error message, a leaked value, a changed response. Then build the vulnerable code so the intended path is the easiest path, and actively hunt for unintended shortcuts, such as a flag readable through path traversal on a web challenge meant to be about SSTI.

3. Avoid guessing

"Guessy" challenges are the top complaint in CTF feedback. Players should never have to guess:

  • a hidden endpoint or file name not discoverable through normal enumeration or a hint in the description;
  • a password, key or cipher parameter that isn't derivable from the data;
  • the author's personal context (a pet's name, an in-joke) in an OSINT challenge;
  • which of several stego tools and passwords you happened to use.

Fuzzing and brute force are fine when they're the lesson, such as a 4-digit PIN with no rate limit. Say so, and keep the search space small enough that it doesn't hammer your infrastructure.

4. Make it realistic where it matters

A web challenge that mirrors a real bug class (IDOR in an API, SSRF to a metadata endpoint, a deserialization gadget) teaches transferable skills. Realism doesn't mean complexity, though. A 40-line Flask app demonstrating a real flaw beats a 4,000-line app where the bug is hard to find because the code is noisy.

5. Keep it deterministic and robust

A challenge must behave the same for the first team and the 300th. Avoid shared mutable state that one team can break for everyone, time-sensitive tricks that depend on server clocks and exploits that only work 1 time in 50 without saying so.

Difficulty curve and target solve rates

Your challenge board is a product with a learning curve. Most events lose beginners because the first hour is too hard, and lose strong teams because the top end runs out by lunchtime. Plan the mix before anyone writes code.

TierShare of boardTarget solve rate*Typical player time
Warmup / sanity10–15%70–95%Under 15 minutes
Easy25–30%40–70%15–60 minutes
Medium30–35%15–40%1–3 hours
Hard15–20%3–15%3–8 hours
Insane0–5%1–3 solvesMost of the event

*Share of active teams (those that scored at least once) that solve a given challenge in that tier. These are practical rules of thumb, not a standard. Shift the whole curve down for a college beginner event and up for an open international CTF.

  • Calibrate with a multiplier. If the author solves their own challenge in 20 minutes, expect players to need several times that. Blind playtesting gives you a much better number.
  • Every category needs an on-ramp. Each category should have at least one easy challenge, so a web-only team and a crypto-only team both score early.
  • Use dynamic scoring so points decay as more teams solve. It corrects your difficulty estimates automatically. Try settings in the dynamic scoring calculator first. On CTF7 you can choose static or dynamic scoring per event.
  • Aim for no unsolved challenges. A challenge nobody solves taught nobody anything. If an insane challenge has zero solves at the halfway mark, consider releasing a hint.

CTF challenge ideas by category

Pick ideas by category and level, not by cleverness: one easy on-ramp per category, then a few medium and hard challenges that reward reasoning. Each seed below gives the scenario players see and the skill it tests, and stops before the solution. The CTF categories guide explains what each category tests.

The levels: beginner is one concept in one or two steps with a browser, a terminal or a common free tool. Intermediate is two or three steps, some scripting and one moment where players connect the dots. Advanced needs multi-stage reasoning, custom tooling or specialist knowledge.

Web

  • Library loan tiers (beginner): a book-lending site decides who is a premium member from a value the browser stores. Tests client-side versus server-side trust.
  • Fest pass portal (intermediate): digital passes are numbered in order, and the download page never asks whose pass it is. Tests broken access control (IDOR).
  • The last seat (advanced): a booking flow checks availability and books the seat in two separate steps. Tests race conditions.

Cryptography

  • Postcards from Rome (beginner): a travel blogger hides messages to a friend under a classical shift cipher. Tests classical ciphers and key space.
  • Same key, different day (intermediate): two intercepted messages were encrypted with the same stream key. Tests why key reuse breaks stream ciphers.
  • Lucky draw (advanced): an online raffle seeds its random number generator from something players can observe. Tests predictable randomness.

Forensics

  • Holiday snapshot (beginner): a staged photo whose embedded metadata records more than the sender meant to share. Tests image and document metadata.
  • Chatty printer (intermediate): a capture of an office network where one device sends a document over a cleartext protocol. Tests protocol analysis and stream reassembly.
  • Ghost process (advanced): a memory image from a workstation after a fictional malware infection. Tests memory forensics.

Reverse engineering and pwn

  • Vending machine (beginner): a tiny program asks for a service code and leaves its secret readable inside the file. Tests extracting strings from a binary.
  • Name tag printer (beginner): a visitor-badge program stores the name right next to a "staff" setting in memory. Tests memory layout and buffer bounds.
  • Toy CPU (advanced): the real check runs inside a custom virtual machine with its own instruction set. Tests VM and bytecode analysis.

OSINT, steganography and misc

  • Where was this taken (beginner): a staged photo with signage and landmarks that place a fictional persona in a city. Tests image clues and geolocation basics.
  • Quiet pixels (intermediate): a message hides in the lowest bits of an image, and the description names the technique. Tests bit-level image analysis.
  • Python jail (advanced): a restricted interpreter blocks most built-ins and keywords. Tests language internals.

Blue team, cloud and AI

  • Phish in the inbox (beginner): three emails claim to come from the dean's office, and only one does. Tests email header analysis.
  • Polite chatbot (beginner): a support bot is told never to reveal a staff discount code. Tests prompt-injection concepts.
  • Patient zero (advanced): logs from several fictional hosts reveal where an intrusion started. Tests incident timeline reconstruction.

Before you build a seed, check that every step can be reached by reasoning, standard enumeration or a stated hint, and that one team cannot break it for everyone else.

OSINT ethics: never build OSINT challenges around real private individuals. Create fictional personas and host the "evidence" on accounts and sites you control.

Flag formats and dynamic flags

Pick one flag format for the whole event and document it in the rules, for example CTF7{This_is_an_example_flag}. A consistent prefix lets players recognize the flag when they see it, lets you validate submissions with a regex like ^CTF7\{[A-Za-z0-9_!@#\-]{4,64}\}$ and helps you spot flags leaked in public writeups or chat.

  • Avoid ambiguous characters (l/1/I, O/0) when the flag has to be read from an image or audio.
  • Decide on case sensitivity and state it. Case-insensitive matching avoids frustrated players when a flag is recovered from lossy output.
  • Make the flag meaningful when it's static: CTF7{0bj3ct_l3v3l_4uth_m4tt3rs} reinforces the lesson.
  • Never reuse flags between challenges or events.

Static vs dynamic flags

A static flag is the same for everyone, so it can be pasted into a group chat. A dynamic flag is unique per team, so a copied flag fails and names the team it was issued to. The concept, the trade-offs and when to use each are covered in what a dynamic flag is; this section is only about building one into a challenge.

For challenges that run in containers, a common pattern is deriving each team's flag with an HMAC and injecting it when the instance starts, as an environment variable or a mounted file. How injection works depends on your platform, so check its documentation.

import hmac, hashlib

def team_flag(secret: bytes, challenge: str, team_id: str) -> str:
    """Deterministic, unguessable per-team flag."""
    msg = f"{challenge}:{team_id}".encode()
    tag = hmac.new(secret, msg, hashlib.sha256).hexdigest()[:16]
    return f"CTF7{{idor_notes_{tag}}}"

# team_flag(b"event-secret", "web-notes", "team-42")
# -> 'CTF7{idor_notes_<16 hex chars>}'

For downloadable challenges (reverse, crypto, forensics files), dynamic flags mean generating a file per team at build time, which is more work. Many events keep those static and rely on submission monitoring instead.

Writing challenge descriptions and hints

The description is part of the challenge. It should set the scene, point to the category's starting point and give every piece of information players can't discover themselves (connection details, file formats, rules like "no brute force needed"). It should not give away the solution.

name: notes-app
category: web
difficulty: easy
author: your-handle
description: |
  Our intern built a "private" notes API over the weekend.
  Guests can only read their own notes... right?
  Start here: http://{host}:{port}/api/notes
  Flag format: CTF7{...}
hints:
  - cost: 0
    text: "What identifies a note in the URL?"
  - cost: 50
    text: "The API checks that you're logged in. Does it check who owns the note?"
files: []
tags: [idor, api, beginner]

This is a generic metadata format. Adapt the fields to your platform's challenge schema.

  • Tier your hints. The first nudges direction, the second names the technique, the last is nearly a walkthrough. On CTF7, hints can cost points so teams decide when to spend them.
  • Pre-write hints before the event and agree when to release them (for example, zero solves after four hours). Improvising hints under pressure leads to accidental spoilers.
  • Keep lore short. Two lines of story is flavor. Two paragraphs is a reading test.
  • Credit the author and list attached files with checksums so players know they downloaded the right thing.

Dockerizing CTF challenges: templates

Every service-based challenge should ship as a Docker image. It makes the challenge reproducible, portable between your laptop and the event infrastructure, and easy to reset. Below are two templates: a Flask web challenge and a pwn challenge served over TCP.

Flask web challenge

A deliberately tiny warmup with an object-level authorization bug. The flag comes from an environment variable at runtime, so the image never contains the real flag.

web-notes/
├── Dockerfile
├── requirements.txt
├── app.py
└── solve.py
# app.py
import os
from flask import Flask, abort, jsonify

app = Flask(__name__)
FLAG = os.environ.get("FLAG", "CTF7{local_test_flag}")
NOTES = {
    1: {"owner": "admin", "body": FLAG},
    2: {"owner": "guest", "body": "Welcome, guest! Your notes live here."},
}

@app.get("/health")
def health():
    return "ok"

@app.get("/api/notes/<int:note_id>")
def get_note(note_id):
    note = NOTES.get(note_id) or abort(404)
    # INTENDED BUG: no check that note["owner"] matches the caller
    return jsonify(note)
# requirements.txt
flask==3.0.3
gunicorn==22.0.0
# Dockerfile
FROM python:3.12-slim

# Unprivileged runtime user
RUN useradd --create-home --uid 1000 ctf

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Code owned by root, read-only for the app user
COPY --chown=root:root app.py .

USER ctf
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", \
     "--timeout", "10", "app:app"]

Pin the base image and dependencies so the challenge builds identically in three months. For extra reproducibility, pin the image by digest (python:3.12-slim@sha256:…).

Pwn challenge with socat

Pwn challenges usually expose a binary over TCP. Each connection gets a fresh process. socat is the simplest way to do that.

# Dockerfile
FROM ubuntu:24.04

RUN apt-get update \
 && apt-get install -y --no-install-recommends socat \
 && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home --uid 1000 ctf
WORKDIR /home/ctf

# Compiled challenge binary + flag, both owned by root
COPY --chown=root:root chall flag.txt ./
RUN chmod 755 chall && chmod 444 flag.txt

USER ctf
EXPOSE 1337
# -T60: drop idle connections after 60s; fork: one process per player
CMD ["socat", "-T60", "TCP-LISTEN:1337,reuseaddr,fork", \
     "EXEC:/home/ctf/chall,stderr"]

Ship the exact chall binary and, for libc-dependent exploits, the libc.so.6 and ld from the image as downloadable files. Players need the same libc to compute offsets. Building the binary inside the image (or a pinned builder stage) guarantees the match.

Pwn challenge with xinetd

xinetd is the older, widely used alternative. It adds per-source connection limits and CPU limits in its config.

# /etc/xinetd.d/ctf
service ctf
{
    disable     = no
    socket_type = stream
    protocol    = tcp
    wait        = no
    user        = ctf
    type        = UNLISTED
    port        = 1337
    bind        = 0.0.0.0
    server      = /home/ctf/chall
    per_source  = 10     # max concurrent connections per IP
    rlimit_cpu  = 20     # CPU seconds per process
    rlimit_as   = 512M   # address-space limit per process
}

# Dockerfile additions (ubuntu base, as above)
# RUN apt-get install -y --no-install-recommends xinetd
# COPY ctf.xinetd /etc/xinetd.d/ctf
# CMD ["xinetd", "-dontfork"]

xinetd starts as root and drops to the ctf user per connection, so the container needs the SETUID and SETGID capabilities. The socat version runs entirely as an unprivileged user, which is simpler to lock down. For hard pwn challenges, nsjail-based wrappers such as redpwn/jail add stronger per-connection sandboxing. Read their docs, because they typically need elevated container privileges to set up the sandbox.

docker-compose for local testing

# docker-compose.yml
services:
  web-notes:
    build: ./web-notes
    ports: ["8080:5000"]
    environment:
      FLAG: "CTF7{dev_only_flag}"      # real flag injected at deploy time
    read_only: true
    tmpfs: ["/tmp:size=16m"]
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    pids_limit: 128
    mem_limit: 256m
    cpus: 0.5
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3

  pwn-babyrop:
    build: ./pwn-babyrop
    ports: ["1337:1337"]
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    pids_limit: 64                      # also caps concurrent connections
    mem_limit: 128m
    cpus: 0.5
    restart: unless-stopped

Run docker compose up -d --build, then point your solve script at localhost. If it passes locally with these limits applied, it's much more likely to survive the event.

Resource limits and isolation

Players are, by design, trying to break your challenge. Some will get code execution. That's often the point. Your job is to make sure a shell inside one challenge can't take down the event, read other flags or reach the internet.

  • Run as non-root and make code and flags owned by root and read-only to the runtime user, so players can't overwrite the flag or backdoor the app for other teams.
  • Read-only root filesystem with a small tmpfs for /tmp when the app needs scratch space.
  • Drop all capabilities and set no-new-privileges. Add back only what's needed.
  • Limit memory, CPU and PIDs. The PID limit stops fork bombs. Memory limits stop a heap-spray from killing the host.
  • Time out connections and requests (socat -T, gunicorn --timeout) so idle or hung sessions don't pile up.
  • Block outbound traffic at the host or orchestrator level (for example, a Kubernetes NetworkPolicy). This stops reverse shells to the internet and use of your infrastructure for attacks. Docker's internal: true networks also block published ports, so use them for backend services such as a challenge's database.
  • One flag per container. Never mount a shared directory of flags into challenge containers.
  • Per-team instances for challenges where players change state (RCE, database writes, admin takeover). Shared instances are fine for stateless, read-only challenges. Per-team instances cost more, so reserve them for challenges that need them.
  • Auto-restart and reset. Use restart policies and a scheduled reset for shared challenges that players can degrade.

Testing and playtesting

Automated tests

Every challenge ships with a solve.py that runs the intended exploit from start to flag and exits non-zero on failure. Run it in CI on every change and on a schedule during the event against the live instance. It's the fastest way to catch a broken challenge before players do.

Two more passes complete the testing: a blind human playtest (below) and an AI assistant pass that tells you whether the challenge still fits your event's AI policy.

# solve.py (web-notes)
import re, sys, requests

base = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080"
r = requests.get(f"{base}/api/notes/1", timeout=5)
m = re.search(r"CTF7\{[^}]+\}", r.text)
if not m:
    sys.exit("[-] flag not found: challenge broken")
print(m.group(0))

Blind playtesting

  • Give the challenge, exactly as players will see it, to someone who didn't build it. Don't hint. Watch where they get stuck.
  • Record their solve time and compare it with your target tier.
  • Ask what they tried that didn't work. Reasonable approaches that fail silently are a design smell.
  • Check for unintended solutions. Playtesters find them far more often than authors do.
  • Test from a fresh environment: a clean download, a new container and a different OS if files are involved.
  • Run an AI pass as well, with a reviewer and public AI assistants, before the challenge is marked ready.

Load testing

For large events, hit shared services with realistic concurrency (for example, 50 simultaneous exploit runs) and confirm the limits hold. A challenge that works for one tester can fall over with 300 teams.

AI-resistant CTF challenge design

AI assistants can explain a cipher, summarize a decompiled function or draft a script in seconds, and some players run agents that work through a board with little human input. No challenge is AI-proof, and chasing that goal usually produces guessy, unfair challenges. The realistic aim is narrower: a board that still measures what your event is meant to measure, under the AI policy you've published.

There is no settled answer yet, so treat this section as a set of design levers you can combine, not a fixed recipe.

Start from your AI policy

Design choices only make sense against a rule. Decide first whether AI is allowed, restricted, not allowed or split onto separate scoreboards, and publish it before registration closes. Our CTF rules template has copy-ready clauses for each option, and should your CTF allow AI? walks through the trade-offs.

AI policyWhat the board must doDesign emphasis
AllowedReward players who use tools well, without letting easy solves decide the resultDynamic scoring, multi-part and live challenges, fresh content
Restricted (assistants yes, autonomous agents no)Keep a person in the loop on every solveInteractive services, stages that need judgment between steps, winner verification
Not allowedProduce a clean skills signalOn-site-only rounds, required writeups and verification calls for prize winners
Separate scoreboardsKeep both boards meaningfulThe same challenges on both boards, then compare solve rates afterwards to learn which challenges AI changed

Which challenges AI assistants solve fastest

These are rules of thumb, not guarantees. Models change between events, so test every time. The challenges most likely to fall to a single prompt are:

  • Self-contained: one file or a short description holds everything needed to solve it.
  • Textbook: a well-known technique with no twist, such as a classic cipher or a chain of encodings.
  • Previously published: reskins of challenges whose writeups are public, which an assistant with web access can find.
  • Single-step: one insight from start to flag, with nothing to observe or adapt along the way.

Challenges that tend to hold up longer need interaction with a live service whose behavior the player has to observe, span several dependent stages, sit in a realistic environment with noise to work through, or are judged on a written explanation rather than a string. Easy, self-contained challenges still have a place: they're the on-ramp for beginners, and a fast solve on a warmup costs you little.

Design levers that keep a board meaningful

  1. On-site-only elements. For in-person rounds, tie part of a challenge to something that exists only at the venue: a hardware kit at each table, a device on the event network, a clue printed on the badge. It can't be pasted into a chat window, which is why on-site finals are the strongest fit for a no-AI policy. See CTFs for security conferences for running on-site rounds.
  2. Multi-part challenges. Split a scenario into stages where each stage depends on what the player found in the last one, with a flag per stage so partial progress still scores. Players have to understand an intermediate result before they can use it, and per-stage solve rates show exactly where teams, and their tools, stall.
  3. Live targets over static files. A service that responds to what the player does asks for observation and iteration. A single downloadable file with one answer is the easiest thing to hand to an assistant, so keep those for warmups and teaching rounds.
  4. Per-team flags and parameters. Dynamic flags give each team a unique flag, so a flag from a shared chat or a solve transcript passed between teams is worthless and traceable. Be precise about what this does: it stops sharing, not AI solving.
  5. Fresh content. Write new challenges instead of reskinning public ones, and don't reuse last year's board for a prize event. The challenge ideas above give you seeds; the twist you add is what makes a challenge yours.
  6. Writeup and source embargoes. Keep author writeups, solve scripts and challenge source private until the event closes, and ask players not to publish solutions before then. Put the embargo in your rules so a mid-event writeup is a clear breach, then publish everything once the event ends.
  7. Reward reasoning. Ask prize-eligible teams for a short writeup, or hold a call where they walk through two solves, and announce it in advance. Report-based formats such as Raw Ops, where findings are triaged and scored with CVSS 4.0, judge how players explain impact and chain weaknesses, not just whether they found a string.
  8. Let scoring absorb easy solves. With dynamic scoring, a challenge that an assistant solves for half the field loses most of its value automatically, so it can't decide the result. See how CTF scoring works.

Don't trade fairness for AI resistance. Hidden instructions aimed at AI tools, deliberately misleading text and unexplained obscurity break the no-guessing principle above. They confuse honest players, can trip up screen readers and translation tools, and are easy to strip out. A challenge that resists AI only because it's guessy is a bad challenge.

How to test a challenge against public AI assistants

Add an AI pass to playtesting for every challenge before release. It takes minutes per challenge and tells you which ones to retier or rework before players find out for you.

  1. Protect the unreleased challenge. Pasting a challenge into a public tool can expose it before the event. Use a plan or setting that doesn't retain or train on your inputs, or a model you run locally, and check the tool's terms. Test with a dev flag, never the production one.
  2. Use the player-facing package only. A reviewer who didn't write the challenge gives the assistant exactly what players will see: description, files and connection details. No source code, no author notes.
  3. Run two time-boxed passes. A one-shot pass: hand the assistant the challenge and ask for a solution, with about 10 minutes and no human insight. A guided pass: the reviewer works the challenge with the assistant as a co-pilot for about 30 minutes, the way a typical player would under a restricted policy.
  4. Try more than one assistant. Results differ between tools and versions. Try two or three mainstream assistants and note which versions you used.
  5. Record the result next to the solve script (sample below) and decide using the table that follows.
  6. Re-test close to the event. A challenge that held up in August may fall in October. Re-run the one-shot pass in the final week for everything in the medium tier and above.
# ai-playtest.yml (private, stored next to solve.py)
challenge: web-notes
tier_planned: easy
reviewer: playtester-2          # not the author
date: {YYYY-MM-DD}
package: player-facing only, dev flag
assistants_tried: 2
one_shot: solved                # solved | partial | not_solved
guided: solved
decision: retier easy -> warmup
notes: fine as an on-ramp; points will decay under dynamic scoring
AI pass resultAI allowed or restrictedAI not allowed
Solved in the one-shot passKeep as a warmup, or add a dependent stageMove to warmup, or redesign around a live or on-site element
Solved only in the guided passKeep. The player still had to thinkKeep, and rely on winner verification
Not solved in either passKeep, and confirm humans can still solve it in the blind playtestKeep, with the same human check

Your AI policy lives in your published rules, not in a platform setting. Submission logs and anti-cheat signals (shared IPs, identical wrong answers, implausibly fast solve sequences) help you spot flag sharing and unusual patterns whichever policy you choose. Treat them as a reason to look closer, not as proof of AI use.

Writing the author writeup

Write the official writeup before the event, not after. It forces you to confirm the intended path is complete, and it gives your support team a reference when players ask "am I on the right track?" After the event, publish it with the source code. Players learn more from the author's reasoning than from any hint. Encourage teams to post their own writeups once the event closes, and embargo writeups while it's running. State the embargo in your CTF rules so everyone knows when publishing is allowed. For how to structure one, see our guide to writing a CTF writeup.

Common challenge-creation mistakes

  1. Guessy steps: hidden files, arbitrary passwords, unexplained stego tools.
  2. Flag baked into the image and leaked through docker history, public registries or an attachment.
  3. Unintended solutions: path traversal to /flag, debug mode left on, .git directory exposed.
  4. Shared state: one team deletes the admin user or fills the disk and breaks the challenge for everyone.
  5. libc mismatch: the provided libc doesn't match the one in the container, so correct exploits fail.
  6. No solve script: nobody notices the challenge broke at 3 a.m. until the complaints arrive.
  7. Difficulty inflation: the author rates it easy because they built it. Playtest instead of guessing.
  8. Flag format drift: flag{} in one challenge, CTF7{} in another. Players waste time second-guessing.
  9. Recycled public challenges: a light reskin of a challenge with a public writeup falls to a web search or an AI assistant in minutes.

Pre-launch challenge checklist

AreaCheck
DesignOne core idea. Intended path documented. No guessing required.
DifficultyTier assigned. Blind playtest time recorded and matches the tier.
FlagEvent format, regex-valid, unique, dynamic where possible, not in the image.
DescriptionConnection info, file list with checksums, flag format, author credit.
HintsTiered hints written, costs set and release criteria agreed.
ContainerPinned base image and dependencies. Non-root user. Healthcheck.
IsolationRead-only FS, capabilities dropped, no-new-privileges, memory/CPU/PID limits, egress blocked.
FilesAttachments match the deployed version (same binary and libc).
Testssolve.py passes in CI and against the deployed instance.
Unintended pathsPlaytesters tried to cheese it. Debug mode off. No .git or backup files exposed.
LoadSurvives expected concurrency with limits applied.
AI passTested with public AI assistants on a dev flag. Result recorded and tier matches your AI policy.
WriteupAuthor writeup complete and stored privately until the event ends. Embargo stated in the rules.

Deploying challenges on CTF7

On CTF7, organizers upload challenges through the admin panel with descriptions, attachments, external links and hints. Standard events can bring their own Docker images, and Premium events get managed container deployment and challenge hosting. Dynamic flags, static or dynamic scoring and anti-cheat monitoring are built in, and you can pause rounds and track submissions live. Compare the tiers on the pricing page, or read the full guide to hosting a CTF.

Don't have time to write and test 30 challenges? Our CTF challenge development service designs, builds and tests custom challenges for your audience and difficulty curve, from jeopardy categories to attack-defense services.

Plan your next event with us

A free jeopardy event for up to 50 players goes live when you publish it. For flagship events, our team can build the challenges and run the managed formats: attack-defense, King of the Hill and SOC simulations.

CTF7 mascot holding a CTF7 flag