Step 321. CVE 재현과 분석 보고서 공개 — 분석의 증명은 재현이다

Step 321. CVE 재현과 분석 보고서 공개 — 분석의 증명은 재현이다

Level 4 — 제보·CVE 분석·오픈소스 기여 | 난이도 ★★★★☆ | 예상 소요 시간 3시간

전제: Step 320에서 패치 diff 분석과 분석 노트 작성을 완료했다. oneday-lab 연습 저장소가 있다.

  • 준비물: Step 320의 연습 저장소(oneday-lab)와 분석 노트, Git Bash, 파이썬 3.10+.
  • 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 오늘의 재현은 전부 여러분이 직접 만든 로컬 파일에서만 실행합니다. 실제 CVE 재현도 반드시 내가 설치한 VM·컨테이너 안에서만 합니다 — 운영 중인 시스템에 PoC를 실행하는 것은 패치 여부와 무관하게 불법입니다.

Step 320에서 여러분은 diff를 읽어 취약점의 원리를 주장할 수 있게 됐습니다. 오늘은 그 주장을 증명합니다. 증명의 형식은 하나입니다 — 취약 버전에서는 공격이 성공하고, 패치 버전에서는 막히는 것을 둘 다 보여 주는 것. 이 대비 실험이 성공하면 분석 노트는 분석 보고서가 되고, 보고서의 블로그 공개는 연구자로서의 포트폴리오가 됩니다. 오늘의 끝에는 공개 전에 생각해야 할 "책임감 있는 공개"의 규칙도 배웁니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • "취약 버전 성공 + 패치 버전 차단"이라는 재현 증명의 구조를 설명한다
  • 분석 노트를 바탕으로 최소한의 트리거(PoC 스크립트)를 작성한다
  • 재현 결과를 읽고 "분석이 맞았는가"를 판정한다
  • 취약점 분석 보고서(개요/원인/영향/재현/수정)를 작성한다
  • 공개 전 확인 사항(패치 공개 여부, 벤더 통보, 법적 범위)을 체크리스트로 적용한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 Git Bash + 파이썬 3.10+ (표준 라이브러리만)
오늘의 명령 python poc.py 대상파일, git show 해시:파일 (Step 320 복습)
필요한 개념 PoC, 재현 증명의 대비 구조, 책임감 있는 공개(responsible disclosure)
오늘의 산출물 PoC 스크립트 1개 + CVE 분석 보고서 1편

2-1. 재현 증명의 구조 — 왜 "둘 다"인가

"취약 버전에서 공격이 됐다"만으로는 부족합니다. 그것은 그냥 프로그램이 이상하게 동작한 것일 수도 있습니다. 증명이 되려면 대조군이 필요합니다.

취약 버전 + 공격 입력 → 성공 (취약!)
패치 버전 + 같은 입력 → 차단 (막힘)

두 결과가 동시에 성립해야 "패치가 막은 바로 그것이, 내가 분석한 바로 그 취약점"이라는 결론이 성립합니다. 취약 버전에서조차 안 되면 트리거가 틀린 것이고, 패치 버전에서도 되면 패치 분석이 틀린 것입니다. 어느 쪽이든 diff로 돌아가 다시 읽습니다.

2-2. PoC — 개념 증명의 최소 단위

PoC(Proof of Concept, 개념 증명)는 Step 115에서 "공격 코드"로 만났던 그것입니다. 오늘은 우리가 직접 씁니다. 좋은 PoC의 조건은 세 가지입니다.

  • 최소성 — 취약점을 발동시키는 데 필요한 것만 담는다 (부수 효과 없음)
  • 재현성 — 다른 사람이 실행해도 같은 결과가 나온다
  • 안전성 — 랩 안에서만 동작하는 대상을 상대로 한다

오늘 만들 PoC는 세 칸을 모두 만족합니다 — 로컬 파일 두 개를 상대로, 결과를 출력으로만 보여 주는 스크립트입니다.

2-3. 분석 보고서의 다섯 칸

Step 320의 분석 노트(유형/트리거/영향/패치 원리)는 "나를 위한 메모"였습니다. 보고서는 "남을 위한 문서"입니다. 다섯 칸으로 확장합니다.

내용
개요 CVE 번호(또는 대상), 영향 버전, 한 줄 요약
원인 분석 패치 diff와 함께 "왜 취약했는가"
영향 성공 시 무엇이 가능한가, 누가 위험한가
재현 환경, PoC, 취약/패치 대비 결과
수정과 대응 패치 원리, 사용자가 취할 조치

2-4. 책임감 있는 공개 — 공개의 윤리

책임감 있는 공개(responsible disclosure)의 핵심은 순서입니다. ① 벤더에게 먼저 통보 → ② 벤더가 패치할 시간 확보 → ③ 패치 공개 후 분석 공개. 이 순서를 지키면 분석 공개는 방어를 돕는 선행이 됩니다.

오늘의 연습 대상은 여러분이 직접 만든 가상 취약점이라 법적 문제가 없습니다. 하지만 실제 CVE 분석을 공개할 때의 확인 사항은 지금부터 형식으로 익혀 둡니다 — 3-6의 체크리스트가 그것입니다.


3. 따라 하기

3-1. 실험 재료 준비 — 두 버전 파일 꺼내기

Step 320의 oneday-lab 저장소로 이동해, 두 버전을 파일로 꺼냅니다 (이미 했다면 건너뛰세요).

입력:

cd oneday-lab
git show HEAD~1:app.py > app_v1.py
git show HEAD:app.py > app_v2.py
ls

출력 (2026-09-09 실측):

app.py  app_v1.py  app_v2.py

읽는 법: app_v1.py가 취약 버전, app_v2.py가 패치 버전입니다. 실제 CVE 재현에서는 이 자리에 "취약 버전을 설치한 VM/컨테이너"가 옵니다. 구 버전 확보가 재현의 절반 이상을 차지하는 경우가 많습니다 — 벤더 아카이브나 vulhub 같은 취약 환경 모음을 활용합니다.

3-2. PoC 작성 — 분석 노트를 코드로 옮기기

Step 320 분석 노트의 트리거 칸을 봅니다: "아이디 입력에 admin' --". 이것을 검증하는 최소 스크립트를 만듭니다. poc.py로 저장하세요.

"""poc.py — SQL 인젝션 재현 스크립트 (랩 전용)
usage: python poc.py <app_v1.py|app_v2.py>
"""
import importlib.util
import sys

path = sys.argv[1]
spec = importlib.util.spec_from_file_location("target_app", path)
app = importlib.util.module_from_spec(spec)
spec.loader.exec_module(app)

conn = app.init_db()

print("=== [1] 정상 로그인 시도 ===")
ok1 = app.login(conn, "guest", "guest123")

print()
print("=== [2] 인젝션 공격 시도: 비밀번호 없이 admin 로그인 ===")
ok2 = app.login(conn, "admin' --", "아무거나")

print()
print("=== 결과 ===")
print(f"정상 로그인: {'성공' if ok1 else '실패'}")
print(f"인젝션 공격: {'성공(취약!)' if ok2 else '차단됨'}")

읽는 법: 파일 이름을 인자로 받아 그 파일을 모듈로 불러오는(importlib) 구조입니다. 덕분에 같은 PoC로 취약 버전과 패치 버전을 번갈아 시험할 수 있습니다 — 대조 실험의 조건(같은 입력, 같은 절차)이 자동으로 지켜집니다. 공격 입력은 비밀번호 칸이 무엇이든 성공하는지가 관건이므로, 비밀번호에는 '아무거나'를 넣었습니다.

3-3. 재현 1막 — 취약 버전

입력:

python poc.py app_v1.py

출력 (2026-09-09 실측):

=== [1] 정상 로그인 시도 ===
[DEBUG] 실행된 쿼리: SELECT * FROM users WHERE username = 'guest' AND password = 'guest123'
[OK] 로그인 성공: guest

=== [2] 인젝션 공격 시도: 비밀번호 없이 admin 로그인 ===
[DEBUG] 실행된 쿼리: SELECT * FROM users WHERE username = 'admin' --' AND password = '아무거나'
[OK] 로그인 성공: admin

=== 결과 ===
정상 로그인: 성공
인젝션 공격: 성공(취약!)

읽는 법: [DEBUG] 줄을 주목하세요. 입력한 admin' -- 때문에 쿼리가 username = 'admin' --' AND ...으로 변형됐습니다. -- 뒤는 SQL 주석이라, 비밀번호 검사 전체가 사라졌습니다. 그래서 비밀번호가 "아무거나"인데도 admin으로 로그인됩니다. 취약점 발동 확인.

3-4. 재현 2막 — 패치 버전 (대조군)

입력:

python poc.py app_v2.py

출력 (2026-09-09 실측):

=== [1] 정상 로그인 시도 ===
[OK] 로그인 성공: guest

=== [2] 인젝션 공격 시도: 비밀번호 없이 admin 로그인 ===
[FAIL] 로그인 실패

=== 결과 ===
정상 로그인: 성공
인젝션 공격: 차단됨

읽는 법: 같은 PoC, 같은 입력인데 패치 버전에서는 차단됐습니다. [DEBUG] 줄이 사라진 것도 보이죠 — 바인딩 방식에서는 쿼리가 입력에 따라 변형되지 않으므로 보여 줄 "오염된 쿼리"가 없기 때문입니다. 입력 admin' --가 문법이 아니라 그저 이상한 아이디 문자열로 처리돼, 그런 사용자가 없으니 로그인 실패가 정상입니다.

증명 완료: 취약 버전 성공 + 패치 버전 차단. Step 320의 diff 분석이 맞았습니다. 이 두 출력이 분석 보고서의 심장입니다.

3-5. 분석 보고서 작성 — 다섯 칸 채우기

이제 2-3의 형식으로 보고서를 씁니다. 아래는 오늘 실측 결과로 채운 모범 예시입니다.

[제목] guestbook v1.0 SQL 인젝션 분석 (가상 CVE-2026-EX01)

[개요]
- 대상: guestbook v1.0 (연습용 미니 로그인)
- 유형: CWE-89 SQL Injection
- 한 줄 요약: 로그인 쿼리를 문자열 조립으로 만들어, 아이디 입력으로
  비밀번호 검사를 무력화 가능

[원인 분석]
- v1.0 → v1.1 패치 diff에서 f-문자열 쿼리가 ? 바인딩으로 교체됨을 확인
- 취약 위치: login()의 쿼리 조립 줄
- 트리거: 아이디에 ' 와 SQL 주석 -- 포함 (예: admin' --)

[영향]
- 비밀번호 없이 임의 계정 로그인 (인증 우회)
- 동일 패턴이 다른 쿼리에 있으면 데이터 유출로 확장 가능

[재현]
- 환경: 로컬 파이썬 3.12, 두 버전을 파일로 추출(git show)
- 방법: python poc.py app_v1.py / app_v2.py 대비 실행
- 결과: v1.0 인젝션 성공 / v1.1 차단 — 분석과 일치

[수정과 대응]
- 패치 원리: 쿼리 문장과 데이터 분리(파라미터 바인딩)
- 사용자 조치: v1.1 이상으로 업데이트

(실제 보고서 파일에서는 각 [칸]을 마크다운 소제목(## 개요 등)으로 씁니다.)

읽는 법: Step 320 분석 노트의 네 칸이 그대로 "원인 분석"과 "영향"으로 들어가고, 오늘의 재현 결과가 "재현" 칸을 채웁니다. 보고서는 분석 노트에 재현 증거를 얹은 것입니다.

3-6. 공개하기 전 — 책임감 있는 공개 체크리스트

보고서를 블로그에 올리기 전, 다섯 가지를 확인합니다. 오늘의 연습 보고서는 가상 취약점이지만, 실제 CVE 분석 글을 쓸 때 이 형식을 그대로 쓰세요.

[ ] 벤더 패치가 이미 공개됐는가? (미공개 취약점의 상세는 공개하지 않는다)
[ ] 벤더 통보 절차를 거쳤는가? (내가 발견한 신규 취약점이라면 공개가 아니라 제보가 먼저)
[ ] PoC가 랩 재현용으로 최소화됐는가? (실제 공격 페이로드나 데이터 유출 도구가 아닌가)
[ ] 재현 환경이 내 랩임이 명시됐는가? (운영 시스템 대상 실험 흔적이 없는가)
[ ] 보고서에 "방어를 위한 분석"임이 드러나는가? (수정·대응 칸 포함)

읽는 법: 다섯 번째가 중요합니다. 재현만 있고 대응이 없는 글은 공격 매뉴얼로 읽힙니다. "수정과 대응" 칸이 있어야 분석 보고서입니다. 이미 패치가 공개된 취약점의 분석 공개는 — 위 다섯 가지를 지킨다는 전제에서 — 책임 있는 공개의 모범 사례로 인정받습니다.

왜 하는가: 보안 연구자의 커리어는 "얼마나 깊이 파는가"와 "얼마나 책임감 있게 공개하는가"의 곱입니다. 어느 한쪽이 0이면 0입니다.


4. 미션과 연습문제

미션 — 검색 기능 취약점의 재현과 보고서

  1. Step 320 미션에서 만든 search.py(검색 기능, 취약/패치 두 커밋)의 두 버전을 git show로 파일로 추출합니다
  2. 오늘의 poc.py 구조를 참고해 검색 기능용 PoC를 작성합니다 — 정상 검색과 인젝션 입력(%' OR '1'='1)을 대비시키는 구조
  3. 취약 버전에서 인젝션 성공, 패치 버전에서 차단을 확인합니다
  4. 3-5의 다섯 칸 형식으로 분석 보고서를 완성합니다
  5. 3-6 체크리스트 다섯 항목에 답한 뒤, 블로그(또는 연습용 마크다운 파일)에 공개합니다

연습문제

문제 1. "취약 버전에서 공격이 성공했다"는 결과만으로는 왜 불충분한가요? 대조군이 있어야 하는 이유를 설명해 보세요.

문제 2. 좋은 PoC의 세 조건(최소성/재현성/안전성)을 각각 한 문장으로 설명하고, 오늘의 poc.py가 각 조건을 어떻게 만족하는지 말해 보세요.

문제 3. 재현 결과가 "취약 버전에서도 안 됨"이었다면 무엇이 틀린 것이고, "패치 버전에서도 됨"이었다면 무엇이 틀린 것인가요?

문제 4. 패치가 아직 공개되지 않은 취약점을 자신이 발견해 분석했습니다. 블로그 공개 전에 해야 할 일을 순서대로 말해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

PoC의 핵심 구조는 오늘의 poc.py와 같습니다 — 대상 파일을 인자로 받아, 정상 입력과 공격 입력을 번갈아 넣고 결과를 대비합니다. 검색 기능 버전의 공격 입력 예시:

rows = app.search(conn, "%' OR '1'='1")
print(f"반환된 행 수: {len(rows)}")   # 취약 버전: 전체 사용자 수, 패치 버전: 0

취약 버전에서는 전체 사용자(2명)가 반환되고, 패치 버전에서는 0행이 나오면 증명 성공입니다. 보고서는 3-5의 예시 형식에 "대상: search.py 검색 기능"으로 바꿔 쓰면 됩니다.

검증하는 법: ① PoC 실행 결과가 두 버전에서 다르게 나오는가. ② 보고서 다섯 칸(개요/원인/영향/재현/수정)이 전부 있는가. ③ "재현" 칸에 환경·방법·결과가 있고, 결과가 실제 출력과 일치하는가. ④ 3-6 체크리스트에 답했는가.

연습문제 해답

문제 1 해답. 취약 버전에서의 성공만으로는 그것이 "분석한 그 취약점" 때문인지, 다른 버그나 환경 차이 때문인지 구분할 수 없기 때문입니다. 패치 버전에서 같은 입력이 차단돼야 "패치가 막은 것 = 내가 분석한 것"이라는 인과가 성립합니다. 대비 실험은 재현을 주장에서 증명으로 바꿉니다.

문제 2 해답. 최소성은 취약점 발동에 필요한 것만 담는다는 것, 재현성은 누가 실행해도 같은 결과가 나온다는 것, 안전성은 랩 안의 대상만 상대한다는 것입니다. 오늘의 PoC는 로그인 시도 두 번만 수행하고(최소성), 같은 스크립트에 파일만 바꿔 끼우는 구조라(재현성), 로컬 파일 안의 함수만 호출합니다(안전성).

문제 3 해답. 취약 버전에서도 안 됐다면 트리거(공격 입력이나 발동 조건)가 틀린 것입니다 — 분석 노트로 돌아가 조건을 다시 확인합니다. 패치 버전에서도 됐다면 패치 분석이 틀린 것입니다 — 고친 곳이 취약 지점이 아니었거나, 공격이 다른 경로로 들어간 것이므로 diff를 다시 읽습니다.

문제 4 해답. ① 벤더에게 먼저 제보합니다(Step 318~319의 제보 절차). ② 벤더가 패치를 개발·배포할 시간을 줍니다(일반적으로 90일 안팎의 공개 유예 관행이 있습니다). ③ 패치가 공개된 뒤에야 분석 글을 공개합니다. 순서를 어기면 패치 없는 시스템들에 공격 설명서를 배포하는 셈이 됩니다.

완료 기준 체크리스트

  • [ ] "취약 버전 성공 + 패치 버전 차단"의 대비 구조를 설명할 수 있다
  • [ ] 분석 노트의 트리거를 PoC 스크립트로 옮길 수 있다
  • [ ] poc.py를 두 버전에 실행해 결과 대비를 확인했다
  • [ ] 재현 실패 시 원인 후보(트리거 오류 / 분석 오류)를 구분할 수 있다
  • [ ] 다섯 칸(개요/원인/영향/재현/수정)의 분석 보고서를 작성할 수 있다
  • [ ] 책임감 있는 공개 체크리스트 다섯 항목을 설명할 수 있다
  • [ ] 미션: 검색 기능 취약점의 재현과 보고서를 완성했다

6. 흔한 실수와 해결

벽 1. poc.py가 "모듈을 찾을 수 없다"고 한다

증상 (2026-09-09 실측 유사 오류):

FileNotFoundError: [Errno 2] No such file or directory: 'app_v1.py'

원인: git show로 파일을 추출하지 않았거나, 다른 폴더에서 실행했습니다.
해결: lsapp_v1.py, app_v2.py, poc.py가 같은 폴더에 있는지 확인하세요. 없다면 3-1의 git show HEAD~1:app.py > app_v1.py부터 다시 합니다.

벽 2. 취약 버전인데 인젝션이 안 된다

증상: app_v1.py에서도 "로그인 실패"가 나옵니다.

원인: 열에 아홉은 추출 방향이 반대입니다 — HEAD~1이 아니라 HEAD(패치 버전)를 꺼냈거나, 공격 입력의 따옴표가 셸에서 깨졌습니다. poc.py 안에 입력이 있으니 셸 문제는 없지만, 파일이 뒤바뀐 경우가 있습니다.
해결: git log --oneline으로 아래쪽(오래된) 커밋이 취약 버전인지 확인하고, head app_v1.py 첫 줄이 v1.0인지 확인하세요.

벽 3. 패치 버전에서도 되는 것 같다

증상: app_v2.py에서도 로그인이 성공합니다.

원인 가능성 둘: ① 정상 로그인(guest/guest123)이 성공한 것을 공격 성공으로 착각 — 출력의 [1][2]를 구분하세요. ② 정말 공격이 됐다면 그 파일은 패치 버전이 아닙니다.
해결: 출력 마지막 줄 "인젝션 공격: 차단됨"을 확인하는 것이 판정 기준입니다. 정상 로그인 성공은 두 버전 모두에서 나와야 하는 결과입니다 (대조 실험의 건전성 확인).

벽 4. 보고서의 "재현" 칸이 텅 비었다

증상: 원인 분석은 썼는데 재현 칸에 뭘 써야 할지 모릅니다.

원인: 재현 칸은 "실행한 것과 본 것"을 그대로 적는 칸입니다. 새로 분석할 것이 없습니다.
해결: 세 줄이면 됩니다 — 환경(파이썬 버전, 파일 추출 방법), 방법(실행한 명령 두 줄), 결과(두 버전의 마지막 출력 줄). 오늘 실측한 출력을 복사해 넣으세요. 스크린샷이 있으면 더 좋습니다.

벽 5. 공개가 무서워서 보고서를 영구 보류한다

증상: "이 정도 분석을 공개해도 되나" 하는 마음에 글을 묵힙니다.

원인: 완벽주의입니다. 첫 분석 글은 세계적 연구자에게도 조악했습니다.
해결: 3-6의 체크리스트만 통과하면 공개해도 됩니다 — 패치 공개된 취약점, 랩 재현, 대응 칸 포함. 틀린 부분이 발견되면 수정 이력과 함께 고치면 됩니다. 공개는 완성 후가 아니라 성장 과정에서 하는 것입니다 (이 주제는 Step 323에서 코드 공개로 이어집니다).


7. 정리

오늘의 개념

개념 한 줄 설명
재현 증명 취약 버전 성공 + 패치 버전 차단의 대비 실험
대조군 "분석이 맞다"를 증명하는 패치 버전 쪽 실행
PoC 취약점 발동을 증명하는 최소 스크립트 — 최소성·재현성·안전성
분석 보고서 개요/원인/영향/재현/수정 다섯 칸의 공개 문서
책임감 있는 공개 벤더 통보 → 패치 → 공개의 순서를 지키는 공개 윤리

오늘의 명령어와 형식

도구 하는 일
git show 해시:파일 > 추출파일 실험용 두 버전 파일 추출
python poc.py app_v1.py 취약 버전 재현
python poc.py app_v2.py 패치 버전 대조 실행
보고서 다섯 칸 개요/원인/영향/재현/수정
공개 체크리스트 패치 공개·벤더 통보·최소 PoC·랩 명시·대응 포함

명령어보다 중요한 감각

분석의 증명은 재현이고, 재현의 증명은 대비입니다. "됐다"가 아니라 "취약 버전에서만 됐다"가 답입니다. PoC를 두 버전에 번갈아 실행하는 오늘의 손동작이, 앞으로 모든 1-day 분석의 마무리 의식이 될 것입니다.

그리고 공개는 두려움의 대상이 아니라 순서의 문제입니다. 벤더 먼저, 패치 다음, 공개는 그 뒤. 이 순서를 지키는 한, 여러분의 분석 보고서는 방어자에게는 경보이고 채용 담당자에게는 실력의 증거이며, 여러분에게는 연구자라는 이름표입니다.


전부 체크되면 Step 321 완료입니다.