Step 286. CTF Debrief Block B: Reaching 30 Accumulated Write-ups — Records Grow by Compound Interest

Step 286. CTF Debrief Block B: Reaching 30 Accumulated Write-ups — Records Grow by Compound Interest

Level 3 — The CTF Competition Cycle | Difficulty ★★☆☆☆ | Estimated time: 2 days (writing backlogged write-ups + organizing the index)

Prerequisites: Step 282’s experience writing 3 write-ups, Step 278’s presentation→write-up sharing principle. A blog or Markdown repository must be up and running.

  • What you need: every write-up written so far (count irrelevant), a Markdown editor, Python 3. Script run results are measured; the write-up repository examples inside the chapter are fictional demonstration data.
  • ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Before publishing write-ups, always confirm that HTB/THM machines are retired, and that competition problems are cleared by the organizers’ publication rules.
  • This chapter is a record-keeping chapter — not attack techniques, but how to turn piled-up records into "a searchable asset."

One write-up is a diary; around thirty, they become an encyclopedia. "I saw this somewhere" turns into "where’s my write-up," and one search has a past you handing over the solution. What accumulation creates is not volume but the speed of pattern recognition.

Today’s goal is twofold: finish the backlogged write-ups to fill the count of 30, and get those 30 organized into a searchable structure (per-field index + technique tags). Organizing matters as much as writing — a record that can’t be searched is a dead record.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Count the current total with an accumulation tally and plan how to fill the shortfall
  • Review write-up quality against the standard "could I reproduce this 6 months from now?"
  • Apply the header (field · tags · date) system consistently across all write-ups
  • Auto-generate the index page and tag statistics with a Python script
  • Read your technique distribution from the tag TOP 5 statistics and discover bias

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Markdown (write-ups), Python 3 (index generator script)
Today’s command python writeup_index.py writeups — automates tally, index, and tag statistics
Concepts needed Front matter, tag system, reproducibility, the compound interest of accumulation
Today’s deliverable 30 accumulated write-ups + auto-generated index page + TOP 5 statistics

2-1. The Compound Interest of Accumulation — What Appears at 30

A write-up’s value curve is not linear. At five, it’s past-tense record; at around thirty, three new things appear.

First, search by technique works. Finding "how did I solve SSTI again" in your own writing is faster and more accurate than a Google search — because it’s a solution you reproduced by hand. Second, the distribution becomes visible. Which techniques you use often and which you’ve never used emerges as statistics, becoming grounds for training direction. Third, it becomes a public track record. In hiring or team-joining settings, "30 write-ups, here’s the index" outweighs a hundred self-introduction sentences.

2-2. Reproducibility — The Quality Standard While Filling 30

Quality collapsing while you fill volume is this step’s only trap. The standard is one sentence — "could I, 6 months from now, reproduce this from the write-up alone?" What this sentence demands is concrete.

Conditions of a reproducible write-up:
- Environment is written (contest/machine name, date, tool versions used)
- Commands are complete (including key parts of the output — the screen, not "it worked")
- Stalls and switches are present (a success-path-only write-up doesn't help reproduction — Step 275)

Measured against this standard, "candidates for reinforcement" filter themselves out. Write-ups with commands but no reasons, with broken captures, with only conclusions — pick 3 today and reinforce them.

2-3. Headers and Tags — Search Comes from Convention

The secret of a searchable repository is not a fancy blog engine but a consistent header. Place the same four cells at the head of every write-up file.

---
title: CTF#3 jwt-none
category: Web
tags: [jwt, auth-bypass]
date: 2026-07-12
---

Three tag design rules. Use technique names as tags (sqli, rop, pcap — impression tags like "hard" or "mind-blowing" are banned), unify on lowercase-hyphen notation (mixing Auth-Bypass and auth_bypass splits the search), never exceed 5 per write-up (too many tags make noise, not an index). Fix the category to six: Web / Pwn / Reversing / Crypto / Forensics / Machine.

2-4. Index Automation — A Hand-Maintained Table of Contents Always Rots

Past 30, maintaining the table of contents by hand collapses. Fixing the TOC every time you write a new piece is tedious, tedious work gets skipped, and a skipped TOC becomes a lie.

So let a script make the TOC. As long as the headers follow the convention, the script scans the folder and regenerates the per-field list and tag statistics. Human work shrinks to one thing — "keeping the header" — and the index is always current. In the next section you build and run this script yourself.


3. Follow Along

3-1. Tally the Current Count and Compute the Shortfall

Open the repository and count the write-ups so far. Not "about twenty-ish" — count by file number. Then turn the shortfall into a material list.

Tally example (screen example):
- current total: 24
- shortfall: 6
- material list: 3 CTF#3 solves (debrief done, write-ups unwritten)
           + 2 HTB retired machines (publication rules confirmed OK)
           + 1 Step 288 planned boot-camp piece

Caution: if an active (non-retired) machine is mixed into the material list, move that item to the vault. A rule-violating write-up is not a track record — it’s evidence of a violation.

3-2. Unify the Header Across All Write-ups

Go through the existing pieces filling in 2-3’s four-cell header. If, while picking tags, you stall on "what was this technique’s name" — that is exactly a spot needing review. Read the piece, extract the solution’s one core technique, and carve it as a tag.

This work is tedious. But that tedium is the price that turns 30 from "piled up" into "searchable." Five minutes each on 30 is two and a half hours — half a day of these two days belongs to this job.

3-3. Build and Run the Index Generator Script

Save the script below as writeup_index.py. This chapter’s code and output are measured, actually run and verified — verification used a sample repository of 30 write-ups plus 1 header-missing draft.

# writeup_index.py — scan the write-up folder and generate the index page and tag statistics
# usage: python writeup_index.py [writeups_folder]
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path

FRONT = re.compile(r"A---n(.*?)n---n", re.S)

def parse(path: Path):
    text = path.read_text(encoding="utf-8")
    m = FRONT.match(text)
    if not m:
        return None  # files without a header are excluded from the tally, with a warning
    meta = {}
    for line in m.group(1).splitlines():
        key, _, val = line.partition(":")
        meta[key.strip()] = val.strip()
    tags = [t.strip() for t in meta.get("tags", "").strip("[]").split(",") if t.strip()]
    return {
        "title": meta.get("title", path.stem),
        "category": meta.get("category", "uncategorized"),
        "tags": tags,
        "date": meta.get("date", "????-??-??"),
        "file": path.name,
    }

def main(folder):
    root = Path(folder)
    entries, skipped = [], []
    for p in sorted(root.glob("*.md")):
        if p.name == "index.md":
            continue
        e = parse(p)
        (entries if e else skipped).append(e or p.name)

    by_cat = defaultdict(list)
    tag_counter = Counter()
    for e in entries:
        by_cat[e["category"]].append(e)
        tag_counter.update(e["tags"])

    lines = [f"# Write-up index ({len(entries)} total)", ""]
    for cat in sorted(by_cat):
        lines.append(f"### {cat} ({len(by_cat[cat])})")
        for e in sorted(by_cat[cat], key=lambda x: x["date"], reverse=True):
            lines.append(f"- [{e['title']}]({e['file']}) — {e['date']} · "
                         + ", ".join(f"`{t}`" for t in e["tags"]))
        lines.append("")
    lines.append("### Tag TOP 5")
    for tag, n in tag_counter.most_common(5):
        lines.append(f"- `{tag}` — {n}")
    (root / "index.md").write_text("n".join(lines) + "n", encoding="utf-8")

    print(f"tally complete: {len(entries)} (no header: {len(skipped)})")
    for name in skipped:
        print(f"  ⚠ header missing: {name}")
    for cat in sorted(by_cat):
        print(f"  {cat}: {len(by_cat[cat])}")
    print("tag TOP 5:", ", ".join(f"{t}({n})" for t, n in tag_counter.most_common(5)))
    print(f"→ {root / 'index.md'} generated")

if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "writeups")

Measured run result.

python writeup_index.py writeups
tally complete: 30 (no header: 1)
  ⚠ header missing: draft-memo.md
  Crypto: 6
  Forensics: 2
  Machine: 8
  Pwn: 3
  Reversing: 3
  Web: 8
tag TOP 5: cve(4), privesc(3), stack(2), cookie(2), crackme(2)
→ writeupsindex.md generated

How to read it: look at two things. First, the ⚠ header missing warning — a headerless file falls out of the index, so a warned file must get a header immediately to become "a write-up that exists." Second, the per-field distribution — this example repository shows the skew at a glance: Forensics 2, Pwn 3. Overlay it with Step 284’s team weakness analysis and check whether the team weakness and your personal repository’s distribution match. If they do, that’s personal-side evidence of the team weakness.

Part of the generated index.md‘s measured content.

# Write-up index (30 total)

### Crypto (6)
- [CTF#3 base-layers](base-layers.md) — 2026-07-12 · `encoding`
- [CTF#3 lcg-predict](lcg-predict.md) — 2026-07-12 · `lcg`, `prng`
- [CTF#2 xor-repeat](xor-repeat.md) — 2026-06-28 · `xor`, `known-plaintext`
...
### Tag TOP 5
- `cve` — 4
- `privesc` — 3

3-4. Write the Backlogged 6, Reinforce the Sloppy 3

Write the shortfall per 3-1’s material list. Order: freshest debriefs first — problems right after a competition have living memory and write fastest. Add the header with each one and rerun the script to refresh the index — seeing the number climb is what carries you to the end.

Then pick 3 sloppy write-ups by 2-2’s standard and reinforce them. Reinforcement priorities: ① add the reasons for commands, ② add a paragraph of stalls and switches, ③ restore broken captures. Not every item needs fixing in all three — plug the one hole decisive for reproducibility in each.

3-5. The 30-Write-up Retrospective — Reading the TOP 5 Statistics

Read the final run’s "tag TOP 5" and leave a three-line retrospective.

Retrospective example (screen example):
1. most-used techniques: cve(4), privesc(3) — three months centered on machine hacking
2. smallest field: Forensics(2) — matches the Step 284 team weakness; personal reinforcement needed
3. surprise: the sqli tag has 1 — the gut "I'm good at Web" and the data disagree

These three lines are the input for the next boot camp (Step 288) and the next study route. Filling 30 without reading the statistics is doing only half of this step.


4. Missions & Exercises

Mission — 30 Accumulated + a Searchable Index

  1. Tally the current count by file number, and write the shortfall and material list (including active-machine rule checks).
  2. Unify the 4-cell header across all write-ups — tags are technique names, lowercase-hyphen, 5 or fewer.
  3. Run writeup_index.py to generate the index and tag statistics, and drive the header-missing warnings to 0.
  4. Finish the shortfall to reach 30 accumulated.
  5. Reinforce 3 sloppy write-ups against the reproducibility standard.
  6. Read the tag TOP 5 and per-field distribution, and leave a three-line retrospective.

Exercises

Exercise 1. Of the three things that newly appear around 30 write-ups (search / distribution / track record), which directly affects training direction, and why?

Exercise 2. What are the three conditions the standard "could I reproduce this 6 months from now?" demands?

Exercise 3. Why must impression words like "hard" or "fun" not be used as tags?

Exercise 4. Using the expression "a lying table of contents," explain why the index is generated by script rather than managed by hand.


5. Model Answers & Completion Criteria

Mission Model Answer

Check against these verification criteria.

  1. Tally accuracy: 30 by the header standard — the script’s printed count is the completion criterion, and headerless drafts don’t count.
  2. Rule compliance: are all published machine write-ups of retired machines — a trace of rule checking (a link or capture) on the material list is the surest.
  3. Tag system consistency: is the same technique free of split notations — if the script’s tag statistics catch auth-bypass and auth_bypass separately, it’s unfinished.
  4. Substance of reinforcement: do the 3 reinforced write-ups actually have "reasons for commands" or "stalls and switches" added — typo fixes are not reinforcement.
  5. Retrospective connection: did what you read in the TOP 5 statistics connect to the next training plan (boot-camp types, study route)?

Exercise Answers

Answer 1. The distribution. Tag statistics and per-field counts show "what I actually solved a lot of" in numbers, not gut — so they become the grounds for deciding the next training’s targets. Search is a personal reuse convenience and the track record is an external evaluation matter, but the distribution is an input to training design, acting directly. When the gut "I’m good at Web" collides with the data of 1 sqli tag, it’s the gut that must be fixed.

Answer 2. Environment (contest/machine, date, tool versions), complete commands with key output, and records of stalls and switches. Without environment, commands lose the context of why they worked; without output, you can’t verify the commands actually worked; without stalls, you stall again at the same trap. A write-up missing any one of the three may be "something to read" but never "something to reproduce."

Answer 3. Because tags are an index, not impressions. An index’s value lies in "pulling all pieces with the same tag at once," and impression tags play no role in technique search while polluting the tag list. Ten pieces tagged "hard" hold no reusable knowledge, but three pieces tagged ssti are a mini cheat sheet right there.

Answer 4. A hand-fixed TOC must be refreshed every time a new piece appears, and that refresh inevitably gets skipped at some point. A skipped TOC ends up saying "a piece exists that doesn’t" or "no piece exists that does" — when the TOC disagrees with reality, it’s a lying table of contents. Script generation guarantees the match between TOC and reality by convention (the header) instead of human diligence. Humans can’t long keep promises kept by diligence.

Completion Criteria Checklist

  • [ ] I tallied the current count and shortfall by file number
  • [ ] I confirmed the publication rules for the material list (retired machines / competition rules)
  • [ ] I unified the 4-cell header across all write-ups
  • [ ] writeup_index.py runs with 0 header-missing warnings
  • [ ] I filled the count to 30 (by script output)
  • [ ] I reinforced 3 sloppy write-ups against the reproducibility standard
  • [ ] I wrote a three-line retrospective from the tag TOP 5 and field distribution
  • [ ] The bias read in the retrospective was reflected in the next training plan

6. Common Pitfalls & Fixes

Wall 1. Running the script throws FileNotFoundError

FileNotFoundError: [Errno 2] No such file or directory: 'empty_dir\index.md'

Cause: the folder given as the argument doesn’t exist. The script proceeds with 0 scan results even when it can’t find the folder, and only halts when writing index.md. This message was actually reproduced and verified while writing this chapter.

Fix: check the folder name and your current location — list with dir, or step into the write-up folder and run python writeup_index.py ..

Wall 2. Non-ASCII headers get garbled and the tally goes weird

Symptom: the title breaks, or a "header missing" warning fires despite a header clearly being there.

Cause: the file’s encoding isn’t UTF-8 (old Windows Notepad saves as ANSI). This script reads only UTF-8. Also, if any character (including a BOM) precedes the header’s ---, the regex can’t recognize it.

Fix: open the file in VS Code or similar, check the encoding in the bottom-right, and "Save with Encoding → UTF-8." If the missing warning still fires, check that the file’s first bytes are --- — the header must start on the very first line of the file.

Wall 3. Filling toward 30, the write-ups keep getting thinner

Symptom: late-stage write-ups are a few commands plus one line "solved."

Cause: the volume goal ate the quality goal — this step’s signature trap, flagged as a boundary in the assignment itself.

Fix: reverse the filling order — not easiest-to-write (recent problems with fresh memory) first, but most-learned-from first. If thin write-ups already exist, don’t count them; send them to the reinforcement-candidates pool. The meaning of 30 is "30 reproducible write-ups," not "30 files."

Wall 4. Tags fragment too finely — the statistics are meaningless

Symptom: the TOP 5 are all 1–2 occurrences, so no distribution reads out.

Cause: you made tags "fresh per problem." Variants of the same concept multiplied, like rsa-small-n, rsa-smalle, small-rsa.

Fix: when picking a tag, look at the existing tag list first — the script’s tag statistics are the existing list. Make a new tag only "when existing tags can’t find this piece." If fragmentation already happened, unify the variants under one representative tag and rerun the script — index automation’s real value is that re-aggregation like this is free.

Wall 5. I hit 30 and feel empty — like, is this all?

Symptom: the number is filled but there’s no sense of achievement.

Cause: you ended with the number as the goal and skipped the third harvest (reading the distribution). The value of 30 lies not in the number but in the next action.

Fix: if you haven’t written 3-5’s three-line retrospective, write it now. "My last 3 months" told by the TOP 5 tags, "my blank" told by the smallest field — the moment you read those two lines, 30 changes from a pile of records into a map. And that blank becomes Step 288’s boot-camp topic. The compound interest of accumulation is paid only to those who read.


7. Summary

Today’s Concepts

Concept One-line explanation
Compound interest of accumulation What appears from 30 — search by technique, distribution statistics, public track record
Reproducibility "Could I, 6 months from now, reproduce this from this write-up" — the quality standard of the volume-goal era
Header 4 cells title / category / tags / date — search comes from convention
Tag design Technique names · lowercase-hyphen · 5 or fewer · no impression words
Index automation Let the script make the TOC — a hand-managed TOC always rots
Reading the distribution A retrospective that discovers training bias in the TOP 5 statistics

Today’s Tools & Formats

Tool/format What it does
Header convention Turns every write-up into aggregable data
writeup_index.py Folder scan → index + tag TOP 5 auto-generated
Material list The table that turns the shortfall into a queue of "to write"
Retrospective 3 lines Most-used technique / smallest field / surprise

The Instinct Beyond Commands

The real deliverable of 30 write-ups is not 30 files but a channel for conversing with past selves. When you meet a similar problem at the next competition, you’ll ask not Google but a past you — and that past you will answer only as much as you kept the headers and recorded the stalls today.

One more thing: the statistics this tally gave you — most-used techniques and smallest fields — are the personal edition of the team weakness analysis (Step 284). Where the team’s blank and your blank overlap is the next training’s coordinates. Records don’t end at being piled; they’re completed at being read.


Once every box is checked, Step 286 is complete.