| name | authoring-gzcli-challenge-src |
| description | Use this skill when writing or editing files under a challenge's `src/` directory in a gzcli event. Covers Dockerfile patterns, run.sh shape, brace-safe `GZCTF_FLAG` handling for DynamicContainer, and the rule against spoiler comments or telegraphing identifiers in shipped source. |
Challenge src/ Authoring
For event layout, use authoring-gzcli-ctf-events. For the dist/ (player-shipped) side, use authoring-gzcli-challenge-dist.
What lives in src/
- The service implementation (binary source, Python app, Go server, etc.).
Dockerfile and any docker-compose.yml referenced by dashboard.config.
- Build inputs (
requirements.txt, package.json, generate.py scripts that produce dist/ artifacts).
- For
StaticAttachment types, src/ is often where attachments are built before being copied into dist/.
The watcher treats every change under src/ as a full redeploy.
DynamicContainer flag injection — brace-safe shell
gzctf injects the per-team flag via GZCTF_FLAG. Container entrypoints must read it without breaking on the literal { and } in the flag.
Wrong — POSIX shell parses the first } inside the default as the closer of ${...}, producing flag{local_test_flag}} (with a double brace) when the env var is unset:
FLAG="${GZCTF_FLAG:-flag{local_test_flag}}"
Right:
#!/bin/sh
set -eu
if [ -z "${GZCTF_FLAG:-}" ]; then
FLAG='<PREFIX>{local_test_flag}'
else
FLAG="$GZCTF_FLAG"
fi
export FLAG
exec socat tcp-l:8011,reuseaddr,fork exec:/home/ctf/handler.sh
The local fallback must not be the real production flag; gzctf overwrites it at runtime when GZCTF_FLAG is set.
Dockerfile patterns
- Pin a base image tag (
alpine:3.20, python:3.12-slim) — never :latest.
- For pwn / raw-tcp challenges:
socat tcp-l:<port>,reuseaddr,fork exec:<handler>.
- For HTTP challenges: expose a single port; the
Dockerfile's EXPOSE must match challenge.yml's container.exposePort.
- Drop privileges if running a network listener: create an unprivileged user and
USER ctf. The directory holding any /flag.txt typically needs chmod 0444 so the unprivileged user can read it (but a SUID helper is preferable when the intended chain is "exploit gives RCE → reads flag").
- Avoid leaving debug interpreters (
python3, gdb, full bash for a raw-socket service) in the final image when the intended chain doesn't need them.
No spoiler comments / no telegraphing identifiers
Source under src/ is shipped to anyone who exfiltrates the image and is often included alongside dist/ artifacts. It must read like normal application code, not a CTF README.
Strip / rename:
- Comments that point at the vuln:
// XOR each byte with 0x37 below, # format string bug: name is user-controlled, // TODO: fix SQLi in /search.
- Spoiler identifier names:
char flag[], void win(), secret_xor_key, is_admin_bypass, unsafe_eval.
Rename to neutral: session_token instead of flag[]; admin_dump_motd instead of win(); _0xK instead of secret_xor_key. The explanation belongs in solver/, never in src/.
Self-check before shipping: run git diff src/ and re-read every comment. If a player learns which bug to exploit just from comments or identifier names, replace those.
Build determinism
- For challenges whose binary or asset is shipped under
dist/, make src/build.sh (or equivalent) reproducible — same input must produce the same on-wire bytes so a player's solver works against the file they downloaded.
- Strip debug symbols on pwn binaries (
gcc … -s or strip --strip-all) unless symbol availability is part of the puzzle.
Generator scripts (src/generate.py, src/build.sh)
- Output goes into
../dist/<file>, not into src/. Use pathlib.Path(__file__).resolve().parent.parent / "dist" / "<file>".
- Make the script idempotent: deleting
dist/ and re-running must produce the same bytes.
- If the script embeds the flag, read it from a single constant at the top of the file — never duplicate the flag literal across multiple lines.
Related sub-skills
authoring-gzcli-challenge-yml — wire Dockerfile / exposePort / scripts.start correctly.
authoring-gzcli-challenge-dist — copy built artifacts into dist/ correctly.
authoring-gzcli-challenge-solver — the intended solver consumes the same artifacts.