Step 290. CTF 복기 블록 A + 블록 C: 익스플로잇 코드 정리 — 급조를 자산으로 바꾼다
Level 3 — CTF 대회 사이클 | 난이도 ★★★☆☆ | 예상 소요 시간 2일 (복기 반일 + 라이브러리 구축 1.5일)
전제: Step 289(중간 평가 대회) 완료, 지금까지 대회·머신에서 급조한 익스플로잇 스크립트들, 파이썬 3.
- 준비물: 지금까지 쓴 익스플로잇 스크립트 전부(대회·머신·연습), Git 저장소 1개, 파이썬 3. 이 챕터의 라이브러리 뼈대 작성·목차 생성·로컬 데모 실행은 실측(2026-09-09, Python 3.12)입니다.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 이 챕터는 자산 정리 챕터입니다 — 블록 A(못 푼 문제 끝까지 파기)에 블록 C(코드 자산화)를 붙인 복기입니다.
대회마다 스크립트를 새로 짜고 계십니까? 그러면 여섯 번의 대회 동안 같은 뼈대를 여섯 번 짠 셈입니다. 소켓을 열고, 프롬프트까지 받고, 페이로드를 보내고 — 그 30줄은 매번 같고, 문제마다 다른 것은 주소와 값 몇 줄입니다.
상위 팀들의 비밀은 실력만큼 라이브러리입니다. 비슷한 유형이 나왔을 때 처음부터 짜는 팀과, 템플릿을 복사해 입출력만 바꾸는 팀의 30분 차이가 순위를 가릅니다. 오늘은 급조의 흔적을 모아 재사용 가능한 개인 익스플로잇 라이브러리 v1을 만듭니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 복기 블록 A(못 푼 문제 끝까지 파기)의 루틴을 중간 평가 대회에 적용한다
- 급조 스크립트를 "입출력만 바꾸면 되는" 템플릿으로 다듬는 기준을 안다
- 템플릿·페이로드·유틸의 3층 라이브러리 구조를 만든다
- 각 파일에 "사용 조건" 주석을 달아 대회 중 10초 검색을 가능하게 한다
- Git으로 라이브러리를 버전 관리하고 팀 자산으로 공유한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3(표준 라이브러리만 — pwntools는 선택), Git, 마크다운 |
| 오늘의 명령 | git init/add/commit, python build_index.py, 템플릿 실행 |
| 필요한 개념 | 템플릿화(80% 재사용), 사용 조건 주석, 3층 구조, 팀 자산화 |
| 오늘의 산출물 | 익스플로잇 라이브러리 v1 (템플릿 10종 목표) + 목차 README |
2-1. 블록 C — 복기의 세 번째 축
복기 블록 A가 "못 푼 문제를 끝까지 파는 것", 블록 B가 "Write-up으로 기록하는 것"이라면, 블록 C는 코드를 자산으로 바꾸는 것입니다. 풀이를 이해하고(블록 A) 기록했어도(블록 B), 코드가 대회 당일의 임시 파일로 남아 있으면 다음 대회에서 다시 짭니다.
블록 C의 판정 질문은 하나입니다 — "이 코드, 다음 대회에서 그대로 꺼내 쓸 수 있는가?" 문제 전용 하드코딩이 섞여 있으면 불가입니다. 그래서 오늘의 작업은 분리입니다 — 문제마다 바뀌는 것(주소·포트·페이로드 값)과 매번 같은 것(접속·수신·전송 뼈대).
2-2. 3층 구조 — 템플릿 / 페이로드 / 유틸
라이브러리는 세 층으로 나눕니다. 역할이 다르면 폴더를 나누는 것이 검색 속도입니다.
| 층 | 폴더 | 내용 | 예 |
|---|---|---|---|
| 템플릿 | templates/ |
문제 유형별 실행 뼈대 — 복사 후 입출력만 수정 | TCP 프롬프트 풀이, BOF 뼈대 |
| 페이로드 | payloads/ |
유형이 정해졌을 때 골라 넣는 재료 모음 — ‘사전’ | SSTI 탐지 문자열, SQLi 판별문 |
| 유틸 | utils/ |
모든 템플릿이 공유하는 함수 | 소켓 래퍼, 인코딩 헬퍼 |
템플릿과 페이로드의 차이가 중요합니다. 템플릿은 실행 가능한 파일, 페이로드는 복사해 넣는 문자열 모음입니다. SSTI 페이로드 20개를 템플릿 20개로 만들면 관리가 무너집니다 — 한 파일의 사전(dict)으로 모으는 것이 정답입니다.
2-3. 사용 조건 주석 — 10초 검색의 비밀
대회 중 라이브러리에 허락된 시간은 파일당 10초입니다. 그 10초에 답해야 할 질문은 "이 파일을 이 문제에 쓸 수 있는가"입니다. 그래서 모든 파일 첫머리에 두 줄을 강제합니다.
"""templates/tcp_prompt_template.py — TCP 프롬프트형 문제 범용 템플릿
사용 조건: 접속하면 메뉴/프롬프트가 뜨고, 특정 입력에 플래그를 반환하는 서비스.
수정 지점: HOST, PORT, TRIGGER(답으로 보낼 값), EXPECT(프롬프트 끝 문자열).
"""
사용 조건은 "어떤 문제에서 쓰는가", 수정 지점은 "어디를 고치는가"입니다. 이 두 줄이 없는 파일은 대회 중에 열어보고 버리는 시간 5분의 주인이 됩니다.
2-4. 80% 재사용 — 범용화의 함정
소재의 경고입니다 — 템플릿은 "급조의 시작점"일 뿐 만능이 아닙니다. 모든 경우를 처리하려고 if를 늘리면, 정작 대회 중에 그 파일을 읽을 수 없게 됩니다.
기준은 80%입니다. 자주 보는 문제 구조의 80%를 커버하는 단순한 뼈대로 멈추고, 나머지 20%는 대회 중에 손으로 짭니다. 완벽한 범용 라이브러리는 읽는 시간이 새로 짜는 시간을 넘는 순간 자산이 아니라 부채입니다.
3. 따라 하기
3-1. 블록 A 먼저 — 중간 평가의 못 푼 문제 정리
라이브러리 작업 전에 블록 A 루틴부터 돌립니다. Step 289 대회에서 못 푼 문제 3개를 골라 끝까지 파되, 오늘의 관점이 하나 추가됩니다 — "이 풀이 코드, 템플릿이 될 수 있는가?"
블록 A 기록 양식 (블록 C 확장판):
문제: (이름/분야/배점)
막힌 지점: (한 문장)
정답 풀이: (공개 Write-up 대조)
우리 코드와의 차이: (한 문장)
템플릿 후보? □ 예 — 유형: ___ □ 아니오 — 일회성 문제
마지막 줄이 오늘의 추가 항목입니다. 정답 코드가 우리 라이브러리에 없는 유형이면, 그것이 오늘 만들 템플릿의 후보입니다.
3-2. 유틸 층 — 소켓 래퍼부터 만든다
모든 템플릿의 공통 분모부터 정리합니다. pwntools가 깔린 환경이면 그것을 쓰면 되지만, 의존성 없이 어디서든 도는 최소 소켓 래퍼를 하나 갖추면 든든합니다. 아래를 ctf_lib/utils/net.py로 저장하세요.
"""ctf_lib/utils/net.py — pwntools 없이 쓰는 최소 소켓 래퍼
사용 조건: 대회 서버가 TCP(raw socket) 서비스일 때.
수정 지점: remote()의 host/port, sendline 후 기대 응답 문자열.
"""
import socket
class Tube:
"""recvuntil / sendline 만 갖춘 최소 튜브. 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("서버가 연결을 닫았습니다")
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이 핵심입니다 — 소켓 수신은 경계를 몰라서, "프롬프트 문자열까지 받기"를 직접 버퍼로 구현해야 프롬프트형 문제를 다룰 수 있습니다.
3-3. 템플릿 층 — 사용 조건과 수정 지점을 단다
유틸 위에 템플릿을 올립니다. 아래가 실제로 작성한 뼈대입니다 — ctf_lib/templates/tcp_prompt_template.py로 저장하세요.
"""templates/tcp_prompt_template.py — TCP 프롬프트형 문제 범용 템플릿
사용 조건: 접속하면 메뉴/프롬프트가 뜨고, 특정 입력에 플래그를 반환하는 서비스.
수정 지점: HOST, PORT, TRIGGER(답으로 보낼 값), EXPECT(프롬프트 끝 문자열).
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from utils.net import remote
# ── 여기만 문제마다 수정 ──────────────────
HOST = "127.0.0.1"
PORT = 31337
EXPECT = b"> "
TRIGGER = b"open"
# ──────────────────────────────────────────
def solve():
io = remote(HOST, PORT)
banner = io.recvuntil(EXPECT)
print("[*] 배너:", banner.decode(errors="replace").strip())
io.sendline(TRIGGER)
resp = io.recvline().decode(errors="replace").strip()
print("[*] 응답:", resp)
io.close()
if "flag{" in resp:
print("[+] 플래그 획득:", resp)
else:
print("[-] 플래그 형식이 아님 — 응답을 보고 TRIGGER 수정")
if __name__ == "__main__":
solve()
수정 지점을 파일 중간의 주석 블록으로 시각적으로 격리한 것이 포인트입니다. 대회 중에는 이 블록만 고치고 나머지는 읽지도 않습니다.
3-4. 페이로드 층과 로컬 검증 — 돌려보고 넣는다
페이로드는 사전 형태로 모읍니다 (ctf_lib/payloads/web_payloads.py):
"""payloads/web_payloads.py — 웹 페이로드 모음 (주석 = 사용 조건)"""
SSTI_PROBES = {
"detect": "{{7*7}}", # 49가 나오면 SSTI 의심
"jinja2_id": "{{7*'7'}}", # 7777777이면 Jinja2, 49면 Twig 계열
}
SQLI_BOOL = {
"true": "' OR '1'='1' -- ",
"false": "' OR '1'='2' -- ", # 응답 차이가 나면 블라인드 SQLi
"time": "' OR SLEEP(3) -- ", # 3초 지연이면 time-based
}
그리고 규칙 하나 — 라이브러리에 넣는 코드는 넣기 전에 로컬에서 한 번 돌립니다. 검증용 가짜 서비스(demo_server.py, localhost 전용)를 띄워 템플릿이 실제로 플래그를 뽑는지 확인했습니다.
실측 (2026-09-09 — 데모 서버 + 템플릿 실행):
[*] 데모 서버 대기 중 127.0.0.1:31337
[*] 배너: === demo vault ===
명령을 입력하세요 >
[*] 응답: flag{template_works_local_only}
[+] 플래그 획득: flag{template_works_local_only}
읽는 법: 템플릿이 배너를 받고, 입력을 보내고, 플래그를 잡아냈습니다. "대충 되겠지"로 저장한 코드는 대회 중에 깨집니다. 5초짜리 로컬 검증이 그 사고를 막습니다 — 다음 스텝(291)은 이 검증을 대회에서 해보는 이야기입니다.
3-5. 목차 자동 생성과 Git — 자산의 마감
마지막으로 목차입니다. 손으로 쓰는 목차는 금방 낡으므로, 각 파일의 "사용 조건" 주석을 긁어 README를 만드는 생성기(build_index.py)를 만들었습니다.
실측 (2026-09-09 — python build_index.py):
# CTF 익스플로잇 라이브러리 목차
### templates/
- `tcp_prompt_template.py` — 접속하면 메뉴/프롬프트가 뜨고, 특정 입력에 플래그를 반환하는 서비스.
### payloads/
- `web_payloads.py` — (사용 조건 주석 없음 — 추가 필요)
### utils/
- `net.py` — 대회 서버가 TCP(raw socket) 서비스일 때.
목차 파일 생성: ...\ctf_lib\README.md
읽는 법: web_payloads.py에 "(사용 조건 주석 없음)"이 찍혔습니다 — 생성기가 주석 누락 파일을 탐지합니다. 목차가 문서인 동시에 검사기가 되는 구조입니다. 이 출력대로 주석을 보강한 뒤, 저장소에 커밋합니다.
cd ctf_lib
git init
git add .
git commit -m "exploit library v1: templates 10종, payloads, utils"
팀과 공유할 때는 저장소를 팀 원격에 올리고, 규칙 하나를 합의합니다 — 대회 중 급조로 고친 코드는 대회 후 정리해서 커밋한다. 정리 없이 쌓이는 공유 저장소는 반년 뒤 아무도 못 읽는 쓰레기장이 됩니다.
4. 미션과 연습문제
미션 — 익스플로잇 라이브러리 v1 완성
- 블록 A 루틴으로 중간 평가 대회의 못 푼 문제 3개를 정리합니다 — "템플릿 후보?" 항목 포함.
- 지금까지 쓴 익스플로잇 스크립트를 전부 한 폴더에 모읍니다.
- 3층 구조(
templates/·payloads/·utils/)로 템플릿 10종을 정리합니다 — 각 파일에 사용 조건·수정 지점 주석 필수. - 템플릿 전부를 로컬 데모 서비스로 한 번씩 돌려 검증합니다.
- 목차 생성기로 README를 만들고, Git 커밋 후 팀과 공유합니다.
연습문제
문제 1. 템플릿과 페이로드를 다른 폴더에 두는 이유는 무엇인가요? SSTI 페이로드 20개를 템플릿 20개로 만들면 어떤 일이 생기나요?
문제 2. "사용 조건"과 "수정 지점" 주석이 답하는 질문이 각각 무엇이며, 왜 대회 중에는 이 두 줄이 파일 전체보다 중요한가요?
문제 3. 소재는 "범용화를 과하게 하면 오히려 안 읽힌다 — 80% 재사용 수준에서 멈추기"라고 했습니다. 완벽한 범용 템플릿이 오히려 손해인 이유를 "대회 중의 읽기 시간" 관점에서 설명해 보세요.
문제 4. 라이브러리에 넣기 전 로컬 검증을 규칙으로 정한 이유는 무엇인가요? 검증 없이 저장된 코드가 대회에서 만드는 구체적 사고 하나를 상상해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
검증 기준으로 확인하세요.
- 블록 A의 선행: 못 푼 문제 3개의 기록에 "템플릿 후보?" 판정이 있는가 — 라이브러리가 복기에서 자라야지, 복기와 따로 놀면 안 됩니다.
- 10종의 구성: 템플릿이 유형별로 퍼져 있는가 — 비슷한 문제 10종은 1종입니다. BOF·포맷 스트링·SQLi·SSTI·JWT·프롬프트형 등 분야가 섞여야 합니다.
- 주석의 전수: 모든 파일 첫머리에 사용 조건·수정 지점이 있는가 — 목차 생성기가 "(주석 없음)"을 하나라도 찍으면 미완입니다.
- 로컬 검증 흔적: 각 템플릿을 실제로 실행한 기록(터미널 캡처 등)이 있는가.
- Git 이력: v1 커밋이 있고, 팀원이 clone해서 읽을 수 있는 상태인가.
연습문제 해답
문제 1 해답. 역할이 다르기 때문입니다 — 템플릿은 실행 가능한 뼈대, 페이로드는 골라 넣는 재료. 페이로드를 템플릿으로 만들면 파일 수가 폭증하고, 대회 중 20개 파일을 열어 비교하는 시간이 생깁니다. 한 파일의 사전에 이름표를 달아 모으면 목차 한 줄로 검색이 끝납니다. 폴더 분리는 정리 미학이 아니라 검색 속도의 설계입니다.
문제 2 해답. 사용 조건은 "이 파일을 이 문제에 쓸 수 있는가", 수정 지점은 "어디를 고치는가"에 답합니다. 대회 중 라이브러리에 허락된 시간은 파일당 10초인데, 이 두 줄이 없으면 파일 전체를 읽어 판단해야 하고, 그 시간이 5분이면 처음부터 짜는 것과 다를 바 없습니다. 주석 두 줄이 "꺼내 쓰는 자산"과 "다시 읽어야 하는 옛 파일"을 가릅니다.
문제 3 해답. 템플릿의 가치는 "안 읽고 고치는 부분만 고쳐도 도는 것"인데, 범용화가 진행되면 분기와 옵션을 이해해야만 고칠 수 있게 되기 때문입니다. 읽기 시간이 새로 짜는 시간을 넘는 순간 템플릿은 부채입니다. 80% 커버의 단순한 뼈대는 수정 지점만 읽으면 되지만, 100%를 노린 코드는 저자 본인도 대회 중에는 못 읽습니다. 나머지 20%는 손으로 짜는 것이 총량 기준으로 빠릅니다.
문제 4 해답. 검증 없는 저장은 "된다"는 거짓 기억을 만들기 때문입니다. 대회 중에 꺼낸 템플릿이 오타·경로·의존성 문제로 죽으면, 팀은 템플릿 고치기와 문제 풀기를 동시에 하게 됩니다 — 급조보다 나쁜 상황입니다. 로컬 검증 5초는 "이 파일은 마지막으로 확인된 시점에 돌았다"는 보증을 자산에 새기는 일입니다.
완료 기준 체크리스트
- [ ] 중간 평가 대회의 못 푼 문제 3개를 블록 A 양식으로 정리했다
- [ ] 과거 익스플로잇 스크립트를 한 폴더에 수집했다
- [ ] templates/payloads/utils 3층 구조를 만들었다
- [ ] 템플릿 10종에 사용 조건·수정 지점 주석을 달았다
- [ ] 템플릿 전부를 로컬에서 한 번씩 실행해 검증했다
- [ ] 목차 생성기로 README를 만들고 주석 누락을 0건으로 만들었다
- [ ] Git 커밋을 만들고 팀과 공유했다
6. 흔한 실수와 해결
벽 1. 바이트 리터럴에 한글을 넣었다가 스크립트가 안 돌아요
증상: 서버/클라이언트 코드에서 실행도 전에 문법 오류가 납니다.
실측 (2026-09-09 — 실제로 작성 중 발생한 오류):
conn.sendall(b"=== demo vault ===\n명령을 입력하세요 > ")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: bytes can only contain ASCII literal characters
원인: b"..." 바이트 리터럴은 ASCII만 허용됩니다. 네트워크 코드에서 메시지에 한글을 넣으면 이 오류가 납니다.
해결: 문자열로 쓰고 .encode()를 붙이세요 — "명령을 입력하세요 > ".encode(). 반대로 받은 바이트를 찍을 때는 .decode(errors="replace"). 송신은 encode, 수신은 decode — 이 방향만 기억하면 됩니다.
벽 2. 템플릿을 돌렸는데 연결 거부가 떠요
실측 (2026-09-09 — 서버 없이 템플릿만 실행한 경우):
ConnectionRefusedError: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다
원인: 접속 대상(서버)이 떠 있지 않거나, 포트가 틀렸습니다. 대회라면 VPN 미접속이나 문제 인스턴스 미시작이 같은 오류를 냅니다.
해결: 순서대로 확인하세요 — ① 대상 주소·포트 오타 ② 로컬 검증이라면 데모 서버를 먼저 띄웠는가 ③ 대회라면 문제 인스턴스 시작 버튼·VPN. 이 오류는 코드의 문제가 아니라 대상의 문제이므로, 코드를 고치기 전에 대상 상태부터 봅니다.
벽 3. 10종을 모으려니 자꾸 비슷한 템플릿만 나와요
증상: 웹 문제 템플릿 8개, 나머지 분야 2개.
원인: 팀이 푼 문제가 웹에 치중했다는 뜻이기도 합니다 — 라이브러리 편향은 팀 역량 편향의 거울입니다.
해결: 빈 분야의 템플릿은 블록 A에서 가져오세요. 못 푼 문제의 정답 코드를 정리해 템플릿화하는 것이 공백 메우기의 정석입니다. 그래서 3-1에서 "템플릿 후보?"를 물은 것입니다.
벽 4. 템플릿이 점점 비대해져요
증상: "이 경우도 처리해야지" 하며 if를 추가하다가 200줄이 됐습니다.
원인: 2-4의 함정 — 범용화의 유혹입니다.
해결: 80% 선에서 자르세요. 예외 케이스는 코드가 아니라 주석으로 남깁니다 — "이 템플릿은 X 경우에 깨진다, 그때는 Y로" 한 줄이면 충분합니다. 코드를 늘리는 대신 주석을 늘리는 것이 자산 관리입니다.
벽 5. 팀원이 공유 저장소에 정리 안 된 코드를 push해요
증상: 대회 직후 급조 코드가 그대로 올라와 읽을 수 없는 파일이 쌓입니다.
원인: 커밋 규칙의 합의 부재입니다.
해결: 브랜치로 분리하세요 — wip/ 폴더 또는 브랜치에 급조 코드를 두고, 정리(사용 조건 주석 + 로컬 검증)를 거친 것만 메인에 올립니다. 규칙은 한 문장이면 됩니다 — "주석 없는 코드는 메인에 없다."
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 복기 블록 C | 풀이 코드를 다음 대회에서 꺼내 쓸 수 있는 자산으로 다듬는 복기 축 |
| 3층 구조 | templates(뼈대) / payloads(재료 사전) / utils(공용 함수) |
| 사용 조건 주석 | "어떤 문제에 쓰는가" — 10초 검색의 답 |
| 수정 지점 주석 | "어디를 고치는가" — 안 읽고 고치기의 좌표 |
| 80% 재사용 | 범용화를 멈추는 선 — 나머지는 대회 중에 손으로 짠다 |
| 로컬 검증 | 저장 전 5초 실행 — "돌았다"는 보증을 새기는 절차 |
오늘의 도구·명령
| 도구·명령 | 하는 일 |
|---|---|
utils/net.py의 remote() |
표준 라이브러리만으로 TCP 프롬프트 다루기 |
build_index.py |
사용 조건 주석 → 목차 README 자동 생성 + 주석 누락 탐지 |
demo_server.py |
템플릿 로컬 검증용 가짜 서비스(localhost 전용) |
git init/add/commit |
라이브러리의 버전 관리와 팀 자산화 |
.encode() / .decode() |
네트워크 송수신의 문자-바이트 경계 처리 |
명령어보다 중요한 감각
라이브러리의 첫 번째 사용자는 팀원이 아니라 대회 중의 나 자신입니다. 지친 상태의 내가 10초 안에 찾고, 안 읽고, 고쳐 쓸 수 있어야 자산입니다. 정리할 때 그 사람을 상상하세요 — 주석 두 줄은 과거의 내가 미래의 나에게 남기는 쪽지입니다.
그리고 라이브러리는 만들어지는 순간부터 낡기 시작합니다. 다음 대회(Step 291)에서 실제로 꺼내 쓰며 무너지는 지점을 찾고, 그 패치가 v2가 됩니다. 자산의 가치는 보관이 아니라 실전 투입으로 확인됩니다.
전부 체크되면 Step 290 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.