Step 286. CTF 복기 블록 B: Write-up 누적 30개 달성 — 기록은 복리로 불어난다
Level 3 — CTF 대회 사이클 | 난이도 ★★☆☆☆ | 예상 소요 시간 2일 (밀린 Write-up 작성 + 인덱스 정리)
전제: Step 282의 Write-up 3개 작성 경험, Step 278의 발표→Write-up 분담 원리. 블로그 또는 마크다운 저장소가 굴러가고 있어야 합니다.
- 준비물: 지금까지 쓴 Write-up 전부(개수 무관), 마크다운 편집기, 파이썬 3. 스크립트 실행 결과는 실측, 챕터 안의 Write-up 저장소 예시는 설명용 가상 데이터입니다.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. Write-up 공개 전에 HTB/THM 머신은 퇴출(retired) 처리됐는지, 대회 문제는 주최 측의 공개 규정이 허용하는지 반드시 확인합니다.
- 이 챕터는 기록 챕터입니다 — 공격 기법이 아니라, 쌓인 기록을 "검색되는 자산"으로 바꾸는 방법을 다룹니다.
Write-up은 하나씩은 일기이고, 서른 개쯤 모이면 백과사전이 됩니다. "이거 어디서 봤는데"가 "내 글 어딨지"로 바뀌고, 검색 한 번에 예전의 내가 풀이를 건네줍니다. 누적이 만들어내는 것은 양이 아니라 패턴 인식의 속도입니다.
오늘의 목표는 두 가지입니다. 밀린 Write-up을 마저 써서 누적 30개를 채우고, 그 30개가 검색 가능한 구조(분야별 인덱스 + 기법 태그)로 정리되게 만드는 것. 쓰는 것만큼 정리하는 것이 중요합니다 — 검색되지 않는 기록은 죽은 기록입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 누적 집계로 현재 수와 부족분을 계산하고 채우기 계획을 세운다
- "6개월 후의 내가 재현 가능한가" 기준으로 Write-up 품질을 검수한다
- 머리말(분야·태그·날짜) 체계를 모든 Write-up에 일관되게 적용한다
- 파이썬 스크립트로 인덱스 페이지와 태그 통계를 자동 생성한다
- 태그 TOP 5 통계에서 자신의 기법 분포를 읽고 편향을 발견한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 마크다운 (Write-up), 파이썬 3 (인덱스 생성 스크립트) |
| 오늘의 명령 | python writeup_index.py writeups — 누적 집계·인덱스·태그 통계 자동화 |
| 필요한 개념 | 머리말(front matter), 태그 체계, 재현 가능성, 누적의 복리 |
| 오늘의 산출물 | Write-up 누적 30개 + 자동 생성된 인덱스 페이지 + TOP 5 통계 |
2-1. 누적의 복리 — 30개에서 생기는 것
Write-up의 가치 곡선은 선형이 아닙니다. 5개일 때는 과거형 기록이지만, 30개쯤 되면 세 가지가 새로 생깁니다.
첫째, 기법별 검색이 됩니다. "SSTI 풀었던 거 뭐였지"를 내 글에서 찾는 것은 구글 검색보다 빠르고 정확합니다 — 내 손으로 재현했던 풀이니까요. 둘째, 분포가 보입니다. 어떤 기법을 자주 썼는지, 무엇을 한 번도 안 썼는지가 통계로 드러나 훈련 방향의 근거가 됩니다. 셋째, 공개 실적이 됩니다. 채용·팀 합류의 자리에서 "Write-up 30개, 여기 인덱스"는 자기소개 문장 백 개보다 무겁습니다.
2-2. 재현 가능성 — 30개를 채울 때의 품질 기준
양을 채우다 품질이 무너지는 것이 이 스텝의 유일한 함정입니다. 기준은 한 문장 — "6개월 후의 내가 이 글만 보고 재현할 수 있는가." 이 문장이 요구하는 것은 구체적입니다.
재현 가능한 Write-up의 조건:
- 환경이 적혀 있다 (대회명/머신명, 날짜, 사용한 도구 버전)
- 명령이 온전하다 (출력의 핵심 부분 포함 — "되더라"가 아니라 화면)
- 막힘과 전환이 있다 (성공 경로만 있는 글은 재현에 도움이 안 된다 — Step 275)
이 기준으로 보면 "보강 대상"이 자연스럽게 가려집니다. 명령만 있고 이유가 없는 글, 캡처가 깨진 글, 결론만 있는 글 — 오늘 3개를 골라 보강합니다.
2-3. 머리말과 태그 — 검색은 규격에서 나온다
검색되는 저장소의 비결은 멋진 블로그 엔진이 아니라 일관된 머리말입니다. 모든 Write-up 파일의 첫머리에 같은 네 칸을 둡니다.
---
title: CTF#3 jwt-none
category: Web
tags: [jwt, auth-bypass]
date: 2026-07-12
---
태그 설계 규칙은 세 가지입니다. 기법 이름을 태그로 쓴다(sqli, rop, pcap — "어려움", "신기함" 같은 감상 태그 금지), 소문자 하이픈 표기를 통일한다(Auth-Bypass와 auth_bypass가 섞이면 검색이 쪼개짐), 한 글에 5개를 넘기지 않는다(태그가 많으면 색인이 아니라 소음). 분야(category)는 Web / Pwn / Reversing / Crypto / Forensics / Machine 여섯 개로 고정합니다.
2-4. 인덱스 자동화 — 손으로 관리하는 목차는 반드시 썩는다
30개를 넘기면 목차를 손으로 고치는 방식은 무너집니다. 새 글을 쓸 때마다 목차를 고치는 일은 귀찮고, 귀찮은 일은 생략되고, 생략된 목차는 거짓말이 됩니다.
그래서 목차는 스크립트가 만들게 합니다. 머리말만 규격대로 쓰면, 스크립트가 폴더를 스캔해 분야별 목록과 태그 통계를 다시 생성합니다. 사람의 일은 "머리말을 지키는 것" 하나로 줄고, 인덱스는 언제나 최신이 됩니다. 다음 절에서 이 스크립트를 직접 만들어 실행합니다.
3. 따라 하기
3-1. 현재 누적 수를 집계하고 부족분을 계산한다
저장소를 열고 지금까지의 Write-up을 셉니다. "대충 스무 개쯤"이 아니라 파일 수로 세세요. 그리고 부족분을 재료 목록으로 바꿉니다.
집계 예시 (화면 예시):
- 현재 누적: 24개
- 부족분: 6개
- 재료 목록: CTF#3 풀이 3개 (복기 완료, 글 미작성)
+ HTB 퇴출 머신 2대 (공개 규정 OK 확인)
+ Step 288 예정 특훈 1개
주의: 재료 목록에 활성(retired 아닌) 머신이 섞여 있으면 그 항목은 보관함으로 옮깁니다. 플랫폼 규정 위반의 Write-up은 실적이 아니라 위반 증거입니다.
3-2. 머리말을 모든 글에 통일한다
기존 글을 훑으며 2-3의 네 칸 머리말을 채웁니다. 태그를 고르다 "이 기법의 이름이 뭐였지"가 막히면 — 그것이 곧 복습이 필요한 지점입니다. 글을 읽고 풀이의 핵심 기법 하나를 뽑아 태그로 새기세요.
이 작업은 지루합니다. 하지만 이 지루함이 30개를 "쌓인 것"에서 "검색되는 것"으로 바꾸는 대가입니다. 30개에 5분씩이면 2시간 반 — 오늘 이틀 중 반나절이 이 일의 몫입니다.
3-3. 인덱스 생성 스크립트를 만들고 실행한다
아래 스크립트를 writeup_index.py로 저장합니다. 이 챕터의 코드와 출력은 실제로 실행해 확인한 실측입니다 — 검증에는 Write-up 30개와 머리말 누락 초안 1개로 구성된 샘플 저장소를 썼습니다.
# writeup_index.py — Write-up 폴더를 스캔해 인덱스 페이지와 태그 통계 생성
# 사용법: python writeup_index.py [writeups폴더]
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 # 머리말 없는 파일은 집계에서 제외하고 경고
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", "미분류"),
"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 인덱스 (누적 {len(entries)}개)", ""]
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("### 태그 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"집계 완료: {len(entries)}개 (머리말 없음 {len(skipped)}개)")
for name in skipped:
print(f" ⚠ 머리말 누락: {name}")
for cat in sorted(by_cat):
print(f" {cat}: {len(by_cat[cat])}개")
print("태그 TOP 5:", ", ".join(f"{t}({n})" for t, n in tag_counter.most_common(5)))
print(f"→ {root / 'index.md'} 생성")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "writeups")
실행 실측 결과입니다.
python writeup_index.py writeups
집계 완료: 30개 (머리말 없음 1개)
⚠ 머리말 누락: draft-memo.md
Crypto: 6개
Forensics: 2개
Machine: 8개
Pwn: 3개
Reversing: 3개
Web: 8개
태그 TOP 5: cve(4), privesc(3), stack(2), cookie(2), crackme(2)
→ writeups\index.md 생성
읽는 법: 두 가지를 봅니다. 첫째, ⚠ 머리말 누락 경고 — 머리말 없는 파일은 인덱스에서 빠지므로, 경고가 뜬 파일은 즉시 머리말을 달아야 "존재하는 글"이 됩니다. 둘째, 분야별 분포 — 이 예시 저장소는 Forensics 2개, Pwn 3개로 쏠림이 바로 보입니다. Step 284의 팀 약점 분석과 개인 저장소의 분포가 일치하는지 겹쳐 보세요. 일치한다면 그것이 팀 약점의 개인 측면 증거입니다.
생성된 index.md의 실측 내용 일부입니다.
# Write-up 인덱스 (누적 30개)
### 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`
...
### 태그 TOP 5
- `cve` — 4회
- `privesc` — 3회
3-4. 밀린 6개를 쓰고, 엉성한 3개를 보강한다
3-1의 재료 목록대로 부족분을 씁니다. 순서는 복기가 신선한 것부터 — 대회 직후의 문제가 기억이 살아 있어 가장 빨리 써집니다. 쓸 때마다 머리말을 달고 스크립트를 다시 돌려 인덱스를 갱신합니다 — 숫자가 올라가는 것이 보여야 끝까지 갑니다.
그다음 2-2의 기준으로 엉성한 글 3개를 골라 보강합니다. 보강의 우선순위: ① 명령의 이유 추가, ② 막힘과 전환 한 단락 추가, ③ 깨진 캡처 복원. 세 글 모두 모든 항목을 고칠 필요는 없습니다 — 재현 가능성에 결정적인 구멍 하나씩을 메우면 됩니다.
3-5. 30개 달성 회고 — TOP 5 통계를 읽는다
마지막 실행의 "태그 TOP 5"를 읽고 회고 문장 세 줄을 남깁니다.
회고 예시 (화면 예시):
1. 가장 많이 쓴 기법: cve(4), privesc(3) — 머신 해킹 중심의 3개월이었다
2. 가장 적은 분야: Forensics(2) — Step 284 팀 약점과 일치, 개인 보강 필요
3. 놀란 점: sqli 태그가 1개 — "Web 잘한다"는 감과 데이터가 다르다
이 세 줄이 다음 특훈(Step 288)과 다음 학습 루트의 입력입니다. 통계를 읽지 않고 30개만 채우면, 이번 스텝의 절반만 한 것입니다.
4. 미션과 연습문제
미션 — 누적 30개 + 검색 가능한 인덱스
- 현재 누적 수를 파일 수로 집계하고, 부족분과 재료 목록을 적습니다 (활성 머신 규정 확인 포함).
- 모든 Write-up에 4칸 머리말을 통일합니다 — 태그는 기법 이름, 소문자 하이픈, 5개 이하.
writeup_index.py를 실행해 인덱스와 태그 통계를 생성하고, 머리말 누락 경고가 0개가 되게 만듭니다.- 부족분을 마저 써서 누적 30개를 채웁니다.
- 재현 가능성 기준으로 엉성한 글 3개를 보강합니다.
- 태그 TOP 5와 분야별 분포를 읽고 회고 세 줄을 남깁니다.
연습문제
문제 1. Write-up 30개쯤에서 새로 생기는 세 가지(검색/분포/실적) 중, 훈련 방향에 직접 영향을 주는 것은 무엇이며 왜인가요?
문제 2. "6개월 후의 내가 재현 가능한가" 기준이 요구하는 세 가지 조건은 무엇인가요?
문제 3. 태그에 "어려움", "꿀잼" 같은 감상어를 쓰면 안 되는 이유는 무엇인가요?
문제 4. 인덱스를 손으로 관리하지 않고 스크립트로 생성하는 이유를 "거짓말하는 목차"라는 표현을 써서 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
검증 기준으로 확인하세요.
- 집계의 정확성: 머리말 기준 30개 — 스크립트가 출력한 수가 완료 기준이고, 머리말 없는 초안은 수에 포함되지 않습니다.
- 규정 준수: 공개된 머신 Write-up이 전부 퇴출 머신인가 — 재료 목록의 규정 확인 흔적(링크나 캡처)이 남아 있으면 가장 확실합니다.
- 태그 체계의 일관성: 같은 기법이 두 가지 표기로 흩어져 있지 않은가 — 스크립트의 태그 통계에서
auth-bypass와auth_bypass가 따로 잡히면 미완성입니다. - 보강의 실질: 보강한 3개 글에 "명령의 이유" 또는 "막힘과 전환"이 실제로 추가됐는가 — 오탈자 수정은 보강이 아닙니다.
- 회고의 연결: TOP 5 통계에서 읽은 내용이 다음 훈련 계획(특훈 유형, 학습 루트)으로 이어졌는가.
연습문제 해답
문제 1 해답. 분포입니다. 태그 통계와 분야별 개수는 "내가 실제로 뭘 많이 풀었는가"를 감이 아니라 수로 보여 주므로, 다음 훈련의 대상을 정하는 근거가 됩니다. 검색은 개인의 재사용 편의, 실적은 외부 평가의 문제이지만, 분포는 훈련 설계의 입력이라 직접 작용합니다. "Web을 잘한다"는 감과 sqli 태그 1개라는 데이터가 충돌할 때 고쳐야 하는 것은 감입니다.
문제 2 해답. 환경(대회/머신·날짜·도구 버전), 온전한 명령과 핵심 출력, 막힘과 전환의 기록입니다. 환경이 없으면 명령이 왜 통했는지 맥락을 잃고, 출력이 없으면 명령이 실제로 통했는지 검증할 수 없으며, 막힘이 없으면 같은 함정에서 또 막힙니다. 셋 중 하나라도 빠진 글은 "읽는 글"은 돼도 "재현하는 글"은 되지 못합니다.
문제 3 해답. 태그는 색인이지 감상이 아니기 때문입니다. 색인의 가치는 "같은 꼬리표가 달린 글을 한꺼번에 꺼내는" 데 있는데, 감상어 태그는 기법 검색에 아무 역할도 못 하면서 태그 목록을 오염시킵니다. "어려움"이 달린 글 열 개를 모아도 재사용할 지식은 없지만, ssti가 달린 글 세 개를 모으면 그것이 곧 미니 치트시트입니다.
문제 4 해답. 손으로 고치는 목차는 새 글이 생길 때마다 갱신해야 하는데, 그 갱신은 반드시 어느 순간 생략되기 때문입니다. 생략된 목차는 "없는 글이 있다고" 또는 "있는 글이 없다고" 말하게 되고 — 목차가 현실과 다르면 그것은 거짓말하는 목차입니다. 스크립트 생성은 목차와 현실의 일치를 인간의 부지런함 대신 규격(머리말)으로 보장합니다. 인간은 부지런함으로 지키는 약속을 오래 못 지킵니다.
완료 기준 체크리스트
- [ ] 현재 누적 수와 부족분을 파일 수로 집계했다
- [ ] 재료 목록의 공개 규정(퇴출 머신/대회 규정)을 확인했다
- [ ] 모든 Write-up에 4칸 머리말을 통일했다
- [ ]
writeup_index.py실행 결과 머리말 누락 경고가 0개다 - [ ] 누적 30개를 채웠다 (스크립트 출력 기준)
- [ ] 엉성한 글 3개를 재현 가능성 기준으로 보강했다
- [ ] 태그 TOP 5와 분야 분포로 회고 세 줄을 썼다
- [ ] 회고에서 읽은 편향이 다음 훈련 계획에 반영됐다
6. 흔한 실수와 해결
벽 1. 스크립트를 실행하니 FileNotFoundError가 떠요
FileNotFoundError: [Errno 2] No such file or directory: 'empty_dir\\index.md'
원인: 인자로 준 폴더가 존재하지 않습니다. 스크립트는 폴더를 찾지 못해도 스캔 결과가 0개인 채로 진행하다가, index.md를 쓰는 순간에야 폴더가 없어 멈춥니다. 이 메시지는 이 챕터 작성 시 실제로 재현해 확인한 것입니다.
해결: 폴더 이름과 현재 위치를 확인하세요 — dir로 목록을 보거나, Write-up 폴더 안으로 들어가서 python writeup_index.py .로 실행하면 됩니다.
벽 2. 한글 머리말이 깨져서 집계가 이상해요
증상: title이 깨지거나, 분명 머리말이 있는데 "머리말 누락" 경고가 뜹니다.
원인: 파일 인코딩이 UTF-8이 아닙니다(윈도우 메모장 구버전은 ANSI로 저장). 이 스크립트는 UTF-8로만 읽습니다. 또한 머리말의 --- 앞에 다른 문자(BOM 포함)가 있으면 정규식이 못 알아봅니다.
해결: VS Code 등으로 파일을 열어 우하단 인코딩을 확인하고 "UTF-8로 저장"하세요. 그래도 누락 경고가 뜨면 파일의 첫 바이트가 ---인지 확인합니다 — 머리말은 반드시 파일 맨 첫 줄에서 시작해야 합니다.
벽 3. 30개를 채우려니까 갈수록 글이 얇아져요
증상: 후반부 Write-up이 명령 몇 줄에 "풀림" 한 줄입니다.
원인: 양 목표가 품질 목표를 먹었습니다 — 이 스텝의 고유한 함정으로, 소재에서도 경계 대상으로 지목된 지점입니다.
해결: 채우기 순서를 바꾸세요 — 쓰기 쉬운 것(기억 신선한 최근 문제)부터가 아니라 배울 게 많았던 것부터 씁니다. 얇은 글이 이미 있다면 그 글은 수에 넣지 말고 보강 대상 3개의 후보로 보냅니다. 30개의 의미는 "재현 가능한 글 30개"이지 "파일 30개"가 아닙니다.
벽 4. 태그가 너무 잘게 쪼개져요 — 통계가 의미 없어요
증상: TOP 5가 전부 1~2회짜리로, 분포가 안 읽힙니다.
원인: 태그를 "문제마다 새로" 만들었습니다. rsa-small-n, rsa-smalle, small-rsa처럼 같은 개념의 변종이 늘어난 경우입니다.
해결: 태그를 고를 때 기존 태그 목록을 먼저 보세요 — 스크립트의 태그 통계가 곧 기존 목록입니다. 새 태그는 "기존 태그로 이 글을 찾을 수 없을 때만" 만듭니다. 이미 쪼개졌다면 변종을 대표 태그 하나로 통일하는 정리 작업을 하고 스크립트를 다시 돌립니다 — 인덱스 자동화의 진가는 이런 재집계가 공짜라는 데 있습니다.
벽 5. 30개를 달성했는데 허전해요 — 이게 다인가 싶어요
증상: 숫자는 찼는데 성취감이 없습니다.
원인: 숫자를 목표로 끝냈고, 세 번째 수확(분포 읽기)을 안 했기 때문입니다. 30개의 가치는 숫자가 아니라 그 다음 행동에 있습니다.
해결: 3-5의 회고 세 줄을 아직 안 썼다면 지금 쓰세요. TOP 5 태그가 말하는 "나의 지난 3개월", 가장 적은 분야가 말하는 "나의 빈칸" — 이 두 줄을 읽는 순간 30개는 기록 더미에서 지도로 바뀝니다. 그리고 그 빈칸이 Step 288의 특훈 주제가 됩니다. 누적의 복리는 읽는 사람에게만 지급됩니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 누적의 복리 | 30개부터 생기는 것 — 기법별 검색, 분포 통계, 공개 실적 |
| 재현 가능성 | "6개월 후의 내가 이 글로 재현 가능한가" — 양 목표 시대의 품질 기준 |
| 머리말 4칸 | title / category / tags / date — 검색은 규격에서 나온다 |
| 태그 설계 | 기법 이름 · 소문자 하이픈 · 5개 이하 · 감상어 금지 |
| 인덱스 자동화 | 목차를 스크립트가 만들게 — 손 관리 목차는 반드시 썩는다 |
| 분포 읽기 | TOP 5 통계에서 훈련 편향을 발견하는 회고 |
오늘의 도구·서식
| 도구·서식 | 하는 일 |
|---|---|
| 머리말 규격 | 모든 글을 집계 가능한 데이터로 |
writeup_index.py |
폴더 스캔 → 인덱스 + 태그 TOP 5 자동 생성 |
| 재료 목록 | 부족분을 "쓸 것"의 큐로 바꾸는 표 |
| 회고 세 줄 | 가장 많은 기법 / 가장 적은 분야 / 놀란 점 |
명령어보다 중요한 감각
Write-up 30개의 진짜 산출물은 30개의 파일이 아니라 과거의 나와 대화하는 통로입니다. 다음 대회에서 비슷한 문제를 만났을 때, 여러분은 구글이 아니라 예전의 여러분에게 물어볼 것입니다 — 그리고 예전의 여러분은 오늘 머리말을 지키고 막힘을 기록한 만큼만 대답할 것입니다.
또 하나, 이번 집계가 준 통계 — 가장 많이 쓴 기법과 가장 적은 분야 — 는 팀 약점 분석(Step 284)의 개인 판입니다. 팀의 빈칸과 나의 빈칸이 겹치는 곳이 다음 훈련의 좌표입니다. 기록은 쌓는 데서 끝나지 않고, 읽는 데서 완성됩니다.
전부 체크되면 Step 286 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.