Step 290. CTF Debrief Blocks A + C: Tidying Exploit Code — Turning Improvisation into Assets
Level 3 — The CTF Competition Cycle | Difficulty ★★★☆☆ | Estimated time: 2 days (half a day of debriefing + 1.5 days of library building)
Prerequisites: Step 289 (the midterm evaluation competition) complete, the exploit scripts you’ve improvised at competitions and machines so far, Python 3.
- What you need: every exploit script you’ve written so far (competitions, machines, practice), one Git repository, Python 3. The library skeleton writing, index generation, and local demo runs in this chapter are measured (2026-09-09, Python 3.12).
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- This is an asset-tidying chapter — a debrief with block C (code asset-ization) attached to block A (digging unsolved problems to the bottom).
Are you writing new scripts at every competition? Then over six competitions you’ve written the same skeleton six times. Open a socket, receive up to the prompt, send the payload — those 30 lines are identical every time; what differs per problem is a few lines of addresses and values.
The top teams’ secret is a library as much as skill. When a similar type appears, the 30-minute difference between a team that writes from scratch and a team that copies a template and changes only the I/O decides the ranking. Today we gather the traces of improvisation and build a reusable personal exploit library v1.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Apply the debrief block A routine (digging unsolved problems to the bottom) to the midterm competition
- Know the criteria for polishing improvised scripts into "change only the I/O" templates
- Build a three-layer library structure of templates, payloads, and utilities
- Attach "conditions of use" comments to each file, enabling a 10-second search mid-competition
- Version-control the library with Git and share it as a team asset
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 (standard library only — pwntools optional), Git, Markdown |
| Today’s commands | git init/add/commit, python build_index.py, running the templates |
| Concepts needed | Templating (80% reuse), conditions-of-use comments, the three-layer structure, team asset-ization |
| Today’s deliverable | Exploit library v1 (goal: 10 templates) + a table-of-contents README |
2-1. Block C — The Debrief’s Third Axis
If debrief block A is "digging unsolved problems to the bottom" and block B is "recording them as write-ups," block C is turning code into assets. Even after you’ve understood a solution (block A) and recorded it (block B), if the code remains a competition-day temp file, you write it again at the next competition.
Block C’s verdict question is one — "can this code be pulled out and used as-is at the next competition?" If it’s mixed with problem-specific hardcoding, it can’t. So today’s work is separation — what changes per problem (addresses, ports, payload values) versus what’s the same every time (the connect/receive/send skeleton).
2-2. The Three-Layer Structure — Templates / Payloads / Utilities
Split the library into three layers. When roles differ, splitting folders is search speed.
| Layer | Folder | Contents | Examples |
|---|---|---|---|
| Templates | templates/ |
Per-problem-type executable skeletons — copy, then edit only the I/O | TCP prompt solver, BOF skeleton |
| Payloads | payloads/ |
Ingredients to pick and drop in once the type is known — a ‘dictionary’ | SSTI detection strings, SQLi probes |
| Utilities | utils/ |
Functions every template shares | Socket wrapper, encoding helpers |
The template/payload difference matters. A template is an executable file; a payload is a collection of strings to copy in. Make 20 SSTI payloads into 20 templates and management collapses — collecting them as a dictionary (dict) in one file is the right answer.
2-3. Conditions-of-Use Comments — The Secret of the 10-Second Search
The time allowed to the library mid-competition is 10 seconds per file. The question those 10 seconds must answer is "can this file be used on this problem?" So two lines are mandatory at the top of every file.
"""templates/tcp_prompt_template.py — general template for TCP prompt-type problems
Conditions of use: a service that shows a menu/prompt on connect and returns a flag for a specific input.
Edit points: HOST, PORT, TRIGGER (the value to send as the answer), EXPECT (the prompt's trailing string).
"""
Conditions of use answers "on what kind of problem is this used"; edit points answers "where do I edit?" A file without these two lines becomes the owner of a five-minute open-and-discard session mid-competition.
2-4. 80% Reuse — The Trap of Generalization
The source’s warning — a template is only "a starting point for improvisation," not a panacea. Add if-branches to handle every case and, right when you need it mid-competition, the file becomes unreadable.
The criterion is 80%. Stop at a simple skeleton covering 80% of the problem structures you see often, and hand-write the remaining 20% mid-competition. The moment a perfectly general library’s reading time exceeds the time to write fresh, it stops being an asset and becomes a liability.
3. Follow Along
3-1. Block A First — Tidying the Midterm’s Unsolved Problems
Before the library work, run the block A routine. Pick three problems you couldn’t solve at the Step 289 competition and dig them to the bottom — with one extra perspective added today: "could this solution code become a template?"
Block A record form (block C extended edition):
Problem: (name/field/points)
Where I got stuck: (one sentence)
The correct solution: (checked against a public write-up)
Difference from our code: (one sentence)
Template candidate? □ Yes — type: ___ □ No — one-off problem
The last line is today’s added item. If the correct code is a type your library doesn’t have, it’s a candidate for the templates you’ll build today.
3-2. The Utility Layer — Build the Socket Wrapper First
Tidy the common denominator of all templates first. If pwntools is installed, use it — but having one minimal socket wrapper that runs anywhere without dependencies is reassuring. Save the following as ctf_lib/utils/net.py.
"""ctf_lib/utils/net.py — a minimal socket wrapper for when pwntools isn't available
Conditions of use: when the competition server is a TCP (raw socket) service.
Edit points: host/port in remote(), the expected response string after sendline.
"""
import socket
class Tube:
"""A minimal tube with only recvuntil / sendline. Mimics pwntools' tube."""
def __init__(self, host, port, timeout=5.0):
self.sock = socket.create_connection((host, port), timeout=timeout)
self.buf = b""
def recvuntil(self, delim, timeout=5.0):
self.sock.settimeout(timeout)
while delim not in self.buf:
chunk = self.sock.recv(4096)
if not chunk:
raise ConnectionError("the server closed the connection")
self.buf += chunk
idx = self.buf.index(delim) + len(delim)
out, self.buf = self.buf[:idx], self.buf[idx:]
return out
def recvline(self, timeout=5.0):
return self.recvuntil(b"\n", timeout)
def sendline(self, data):
self.sock.sendall(data + b"\n")
def close(self):
self.sock.close()
def remote(host, port):
return Tube(host, port)
recvuntil is the core — socket receives know no boundaries, so you must implement "receive up to the prompt string" yourself with a buffer before you can handle prompt-type problems.
3-3. The Template Layer — Attach Conditions of Use and Edit Points
Lay the template on top of the utility. Below is the skeleton we actually wrote — save it as ctf_lib/templates/tcp_prompt_template.py.
"""templates/tcp_prompt_template.py — general template for TCP prompt-type problems
Conditions of use: a service that shows a menu/prompt on connect and returns a flag for a specific input.
Edit points: HOST, PORT, TRIGGER (the value to send as the answer), EXPECT (the prompt's trailing string).
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from utils.net import remote
# ── Only this part changes per problem ──────
HOST = "127.0.0.1"
PORT = 31337
EXPECT = b"> "
TRIGGER = b"open"
# ────────────────────────────────────────────
def solve():
io = remote(HOST, PORT)
banner = io.recvuntil(EXPECT)
print("[*] banner:", banner.decode(errors="replace").strip())
io.sendline(TRIGGER)
resp = io.recvline().decode(errors="replace").strip()
print("[*] response:", resp)
io.close()
if "flag{" in resp:
print("[+] flag captured:", resp)
else:
print("[-] not a flag format — check the response and adjust TRIGGER")
if __name__ == "__main__":
solve()
The point is visually isolating the edit points as a comment block in the middle of the file. Mid-competition, you edit only this block and don’t even read the rest.
3-4. The Payload Layer and Local Verification — Run It Before You Shelve It
Collect payloads in dictionary form (ctf_lib/payloads/web_payloads.py):
"""payloads/web_payloads.py — web payload collection (comments = conditions of use)"""
SSTI_PROBES = {
"detect": "{{7*7}}", # if 49 appears, suspect SSTI
"jinja2_id": "{{7*'7'}}", # 7777777 means Jinja2, 49 means Twig-family
}
SQLI_BOOL = {
"true": "' OR '1'='1' -- ",
"false": "' OR '1'='2' -- ", # if responses differ, blind SQLi
"time": "' OR SLEEP(3) -- ", # a 3-second delay means time-based
}
And one rule — run every piece of code locally once before shelving it in the library. We spun up a fake verification service (demo_server.py, localhost only) and confirmed the template actually extracts the flag.
Measured (2026-09-09 — demo server + template run):
[*] demo server listening 127.0.0.1:31337
[*] banner: === demo vault ===
enter a command >
[*] response: flag{template_works_local_only}
[+] flag captured: flag{template_works_local_only}
How to read it: the template received the banner, sent the input, and caught the flag. Code shelved on "it’ll probably work" breaks mid-competition. A 5-second local verification prevents that accident — the next step (291) is the story of running this verification at an actual competition.
3-5. Auto-Generating the Index and Git — Finishing the Asset
Last, the index. A hand-written index goes stale fast, so we built a generator (build_index.py) that scrapes each file’s "conditions of use" comment and makes a README.
Measured (2026-09-09 — python build_index.py):
# CTF Exploit Library Index
### templates/
- `tcp_prompt_template.py` — a service that shows a menu/prompt on connect and returns a flag for a specific input.
### payloads/
- `web_payloads.py` — (no conditions-of-use comment — needs to be added)
### utils/
- `net.py` — when the competition server is a TCP (raw socket) service.
Index file created: ...\ctf_lib\README.md
How to read it: web_payloads.py got flagged with "(no conditions-of-use comment)" — the generator detects files missing comments. The index is simultaneously a document and an inspector. After reinforcing the comments per this output, commit to the repository.
cd ctf_lib
git init
git add .
git commit -m "exploit library v1: 10 templates, payloads, utils"
When sharing with the team, push the repository to the team remote and agree on one rule — code hastily fixed mid-competition gets tidied and committed after the competition. A shared repository piled up without tidying becomes a garbage heap nobody can read half a year later.
4. Missions & Exercises
Mission — Complete Exploit Library v1
- Tidy the midterm competition’s three unsolved problems with the block A routine — including the "template candidate?" item.
- Gather every exploit script you’ve written so far into one folder.
- Organize 10 templates into the three-layer structure (
templates/·payloads/·utils/) — conditions-of-use and edit-point comments mandatory on every file. - Run every template once against the local demo service to verify.
- Generate the README with the index generator, then Git-commit and share with the team.
Exercises
Exercise 1. Why do templates and payloads go in different folders? What happens if you make 20 SSTI payloads into 20 templates?
Exercise 2. What question does each of the "conditions of use" and "edit points" comments answer, and why mid-competition are these two lines more important than the whole file?
Exercise 3. The source says "over-generalize and it stops being read — stop at the 80% reuse level." Explain, from the standpoint of "mid-competition reading time," why a perfectly general template is a net loss.
Exercise 4. Why did we make local verification a rule before shelving in the library? Imagine one concrete accident that unverified code causes at a competition.
5. Model Answers & Completion Criteria
Mission Model Answer
Verify against these criteria.
- Block A came first: do the three unsolved problems’ records include the "template candidate?" verdict — the library must grow out of the debrief, not play separately from it.
- The composition of the 10: are the templates spread across types — ten similar problems are one type. BOF, format string, SQLi, SSTI, JWT, prompt-type, etc. — the fields must be mixed.
- Full comment coverage: does every file have conditions of use and edit points at the top — if the index generator flags even one "(no comment)," it’s unfinished.
- Traces of local verification: is there a record (terminal capture, etc.) of actually running each template?
- Git history: does the v1 commit exist, and is the repository in a state teammates can clone and read?
Exercise Answers
Answer 1. Because their roles differ — a template is an executable skeleton; a payload is an ingredient to pick and drop in. Turn payloads into templates and the file count explodes, creating time mid-competition to open and compare 20 files. Collect them in one file’s dictionary with name tags and the search ends with one line of the index. Folder separation is not tidiness aesthetics but search-speed design.
Answer 2. Conditions of use answers "can this file be used on this problem"; edit points answers "where do I edit?" The time allowed to the library mid-competition is 10 seconds per file; without these two lines you must read the whole file to judge, and if that takes 5 minutes it’s no different from writing from scratch. Two comment lines divide "an asset you pull out and use" from "an old file you have to re-read."
Answer 3. A template’s value is "it runs even if you only fix the parts without reading," but as generalization progresses you must understand the branches and options before you can fix anything. The moment reading time exceeds writing-fresh time, the template is a liability. An 80%-coverage simple skeleton only needs its edit points read, but code aiming at 100% is unreadable mid-competition even by its own author. Hand-writing the remaining 20% is faster on a total-time basis.
Answer 4. Because unverified shelving creates the false memory of "it works." If a template pulled out mid-competition dies on a typo, path, or dependency problem, the team ends up fixing the template and solving the problem at the same time — worse than improvising. A 5-second local verification carves a guarantee into the asset: "this file ran as of its last check."
Completion Criteria Checklist
- [ ] I tidied the midterm competition’s three unsolved problems in the block A form
- [ ] I collected past exploit scripts into one folder
- [ ] I built the templates/payloads/utils three-layer structure
- [ ] I attached conditions-of-use and edit-point comments to all 10 templates
- [ ] I verified every template by running it locally once
- [ ] I generated the README with the index generator and brought missing comments to zero
- [ ] I made a Git commit and shared it with the team
6. Common Pitfalls & Fixes
Wall 1. I put Korean text in a bytes literal and the script won’t run
Symptom: a syntax error in server/client code before it even executes.
Measured (2026-09-09 — an error that actually occurred while writing):
conn.sendall(b"=== demo vault ===\n명령을 입력하세요 > ")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: bytes can only contain ASCII literal characters
Cause: b"..." bytes literals allow ASCII only. Put non-ASCII text in a message in network code and you get this error.
Fix: write it as a string and append .encode() — "명령을 입력하세요 > ".encode(). Conversely, when printing received bytes, use .decode(errors="replace"). Encode on send, decode on receive — just remember the direction.
Wall 2. I ran the template and got connection refused
Measured (2026-09-09 — running the template with no server up):
ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it
Cause: the target (server) isn’t up, or the port is wrong. At a competition, a disconnected VPN or an unstarted problem instance produces the same error.
Fix: check in order — ① typos in the target address/port ② if it’s local verification, did you start the demo server first ③ at a competition, the problem instance’s start button and the VPN. This error is not a code problem but a target problem, so look at the target’s state before fixing code.
Wall 3. Trying to collect 10, I keep producing similar templates
Symptom: eight web-problem templates, two for every other field.
Cause: it also means the problems your team solved skew web — library bias is a mirror of team capability bias.
Fix: bring the empty fields’ templates from block A. Tidying an unsolved problem’s correct code into a template is the standard way to fill a gap. That’s why 3-1 asks "template candidate?"
Wall 4. My template keeps getting bloated
Symptom: adding if-branches going "I should handle this case too," and it’s 200 lines now.
Cause: the 2-4 trap — the temptation of generalization.
Fix: cut at the 80% line. Leave exception cases as comments, not code — one line like "this template breaks in case X; then use Y" is enough. Growing comments instead of code is asset management.
Wall 5. A teammate pushes untidied code to the shared repository
Symptom: hastily written code lands right after the competition and unreadable files pile up.
Cause: no agreed commit rule.
Fix: separate with branches — keep improvised code in a wip/ folder or branch, and promote only what has passed tidying (conditions-of-use comment + local verification) to main. The rule needs one sentence — "no comment-less code on main."
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Debrief block C | The debrief axis that polishes solution code into an asset pullable at the next competition |
| Three-layer structure | templates (skeletons) / payloads (ingredient dictionaries) / utils (shared functions) |
| Conditions-of-use comment | "On what problem is this used" — the 10-second search’s answer |
| Edit-points comment | "Where do I edit" — the coordinates of fix-without-reading |
| 80% reuse | The line where generalization stops — the rest is hand-written mid-competition |
| Local verification | A 5-second run before shelving — the procedure that carves in a "it ran" guarantee |
Today’s Tools & Commands
| Tool/command | What it does |
|---|---|
remote() in utils/net.py |
Handling TCP prompts with the standard library alone |
build_index.py |
Conditions-of-use comments → auto README index + missing-comment detection |
demo_server.py |
A fake service for local template verification (localhost only) |
git init/add/commit |
Version control and team asset-ization of the library |
.encode() / .decode() |
Handling the string-bytes boundary in network I/O |
The Core Instinct
The library’s first user is not your teammates but your future self mid-competition. It’s an asset only if your exhausted self can find it in 10 seconds, skip the reading, and fix-and-go. Picture that person as you tidy — the two comment lines are a note from past you to future you.
And a library starts going stale the moment it’s made. At the next competition (Step 291) you’ll actually pull it out and use it, find where it collapses, and those patches become v2. An asset’s value is confirmed not by storage but by live deployment.
Once every box is checked, Step 290 is complete. Click the checkbox in the sidebar to save your progress.