| name | authoring-gzcli-challenge-solver |
| description | Use this skill when writing or reviewing the `solver/` directory of a gzcli challenge. Covers the expected files (`solve.{py,sh}` + `README.md`), end-to-end validation from a clean start, and common gotchas like urllib auto-following redirects or cached docker images masking solver regressions. |
Challenge solver/ Authoring
For event layout, use authoring-gzcli-ctf-events. For the challenge code the solver consumes, use authoring-gzcli-challenge-src and authoring-gzcli-challenge-dist.
What solver/ is
solver/ is the intended-solution material the author keeps for verification. The gzcli watcher ignores solver/ for deploy purposes — it is documentation. But the upload validator does require solver/ to exist with meaningful content.
Expected layout:
solver/
README.md # intended-path walkthrough (prose)
solve.py # or solve.sh — runnable, prints the flag to stdout
The solver is for the author and reviewers. A separate public writeup (under writeups/ in the event root) is the post-event audience-facing artifact — keep them distinct.
solve script contract
- Runnable from a clean checkout with one command:
python3 solve.py or sh solve.sh.
- For service-based challenges, accept host / port as CLI args or env vars:
python3 solve.py <host> <port> with sensible defaults (localhost and the documented port).
- Print the recovered flag to stdout in a regex-matchable form (verification extracts it with
grep -oE '<PREFIX>\{[^}]+\}').
- Exit non-zero on failure so CI / scripted verification catches regressions.
For static challenges that ship artifacts via dist/, default the solver to ../dist/<file> and accept an override:
path = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path(__file__).resolve().parent.parent / "dist" / "usb.img"
README.md contract
- One short paragraph: what the intended technique is. No treatise.
- Numbered steps that mirror
solve.py. A player should be able to reproduce the exploit by hand from this README even if solve.py is broken.
- End with the expected flag in a fenced block so reviewers can scan it.
Clean-start validation
Before declaring a challenge ready, validate the full path from scratch:
docker rmi <slug>:latest 2>/dev/null
cd src && docker build -t <slug>:latest .
docker run -d --rm --name <slug>-test -p 8011:8011 <slug>:latest
sleep 1
cd ../solver && python3 solve.py 127.0.0.1 8011
docker stop <slug>-test
The solver must succeed against the just-built container, not against a cached image with stale state. If a dist/ artifact is involved, rebuild it from src/ (python3 src/generate.py / sh src/build.sh) before running the solver.
Common gotchas
Self-check questions
- If a reviewer ran
solver/solve.py against a freshly-built challenge container, would they recover the flag? Test it.
- If
src/ changes (a constant renamed, a port shifted, an offset moved), does solve.py still work? If not, the solver is encoding too much state that should be discovered dynamically.
- Is there a path through the solver that prints a partial answer but exits 0? If yes, CI won't catch regressions.
Related sub-skills
authoring-gzcli-challenge-src — the service / binary the solver attacks.
authoring-gzcli-challenge-dist — the artifacts the solver parses.
authoring-gzcli-challenge-yml — flag-template format determines the regex the solver should match.