Step 317. 후보 검증과 영향도 확인
Level 4 — 버그바운티 | 난이도 ★★★★☆ | 예상 소요 시간 3시간
전제: Step 316(기능별 탐색)을 마쳤다.
후보목록.md가 손에 있다.
- 준비물: 파이썬 3, Step 316의 확장 랩(
app.py),requests, Step 316의 후보 목록. - ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 주의: 검증 과정 자체가 피해를 주면 안 됩니다 — 타인 데이터 대량 열람, 서비스 부하, 실제 사용자 영향은 전부 금지입니다. 최소한의 요청으로 영향을 증명하고 멈추는 것이 프로의 검증입니다.
Step 316에서 후보 여섯 개를 얻었습니다. 그런데 후보는 취약점이 아닙니다. "에러가 난다"는 취약점이 아니고, "타인의 데이터를 읽을 수 있다"가 취약점입니다. 오늘은 후보를 심판대에 올립니다 — 재현 가능한가, 보안 영향이 있는가, 그리고 그 검증이 무해한가. 세 질문을 통과한 것만 제보로 갑니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- "버그인가 아닌가"의 판정 기준(재현성 + 보안 영향)을 적용한다
- 후보를 영향도 순으로 정렬해 검증 순서를 정한다
- 테스트 계정 A/B로 최소 검증을 수행하고 즉시 멈춘다
- 요청/응답 원문을 증거로 저장하고 민감 정보를 마스킹한다
- 영향 증명이 막힌 후보를 보류하고 다음으로 넘어가는 판단을 한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3, Flask 모사 랩, requests |
| 오늘의 도구 | (실습) 최소 검증 + 증거 저장 스크립트 verify.py, (개념 소개) Burp의 raw request 저장 |
| 필요한 개념 | 재현성, 보안 영향, PoC 최소화, 영향 범위 추정, 증거 마스킹 |
| 오늘의 산출물 | 유효 취약점 확정 1건 + evidence_*.txt 증거 파일 |
2-1. 취약점의 두 조건
후보가 진짜 취약점이 되려면 두 가지를 동시에 만족해야 합니다.
- 재현 가능 — 같은 단계를 따라 하면 누구든(트리아저도) 같은 결과를 얻는다
- 보안 영향 — 공격자가 이것으로 무엇을 할 수 있는지가 구체적으로 존재한다
둘 중 하나라도 없으면 제보는 거절됩니다. "이상한 응답이 온다"는 재현이어도 영향이 없으면 정보성(informative) 판정입니다. 반대로 영향이 있어 보여도 재현이 안 되면 심사를 통과할 수 없습니다.
2-2. 영향도 순 정렬 — 어디부터 검증할까
검증 시간은 유한하므로, 영향이 큰 후보부터 확인합니다. 일반적인 순서입니다.
| 순위 | 유형 | 이유 |
|---|---|---|
| 1 | 타인 데이터 접근 (IDOR 등) | 조건 없이 즉시 정보 유출 |
| 2 | 인증 우회 / 계정 탈취 | 피해자 계정 전체를 장악 |
| 3 | 저장형 XSS | 다른 사용자 브라우저에서 실행 |
| 4 | 반사형 XSS, 정보 노출 등 | 피해자 행동이 필요하거나 영향이 제한적 |
2-3. PoC 최소화 — 증명의 경계
PoC(Proof of Concept, 개념 증명)의 규칙은 "증명에 필요한 최소"입니다. IDOR이라면 타인 데이터 1건을 읽는 것으로 성립이 증명됩니다 — 100건을 읽는 것은 증명이 아니라 수집이고, 그 시점에 침해가 됩니다. DoS 계열은 실전에서 검증 자체가 금지되는 경우가 많아, "이론적 가능성" 수준에서 멈추고 제보서에 그 사실을 명시합니다.
검증의 경계를 한 줄로 — "성립이 보이는 순간 멈춘다".
2-4. 증거의 수집과 마스킹
제보에는 증거가 따라갑니다 — 요청/응답 원문(Burp의 raw request), 스크린샷, 화면 녹화. 이때 증거에 타인의 개인정보나 내 세션 쿠키가 그대로 박제되면 안 됩니다. 세션 값, 주소, 이메일은 마스킹한 사본을 만들고, 원문은 제보 채널 안에서만 다룹니다. 증거의 위생이 곧 제보자의 신뢰입니다.
3. 따라 하기
3-1. 검증 순서 정하기
Step 316의 후보 목록을 2-2의 기준으로 정렬합니다.
1순위: T5 IDOR (타인 개인정보 열람) — 조건 없이 즉시 유출
2순위: T6 재설정 토큰 예측 (계정 탈취) — 토큰 사용 경로 확인 필요
3순위: T1 세션 무효화 부재 — 세션 탈취가 선행돼야 함
4순위: T4 dev /admin 노출 — dev 환경 한정, 데이터 없음
5순위: T2 반사형 XSS — 피해자 클릭 필요
6순위: T3 업로드 우회 — 실행 여부 미확인
읽는 법: 오늘은 1순위 T5를 끝까지 검증하고, 나머지의 운명도 함께 결정합니다. 랩이 떠 있지 않으면 python app.py로 기동하세요.
3-2. 최소 검증 — verify.py
IDOR 후보를 "딱 2개 요청"으로 확정합니다. 내 것 하나, B 것 하나 — 이게 전부입니다.
import requests, re
API = "http://127.0.0.1:5003"
a = requests.Session()
a.post(f"{API}/api/v1/login", json={"user": "alice", "pw": "alice-pass!"})
# 최소 검증: 내 리소스와 B 리소스 딱 1건씩만
mine = a.get(f"{API}/api/v1/users/1001")
bob = a.get(f"{API}/api/v1/users/1002")
print("[검증] A 세션으로 A(1001):", mine.status_code)
print("[검증] A 세션으로 B(1002):", bob.status_code)
print("[판정]", "영향 증명됨 — 타인 데이터 1건 열람, 여기서 중단"
if bob.status_code == 200 else "영향 미증명 — 보류")
# 증거 저장: 요청/응답 원문 + 민감정보 마스킹
raw_req = bob.request
req_text = (f"{raw_req.method} {raw_req.path_url} HTTP/1.1\r\n"
f"Host: 127.0.0.1:5003\r\nCookie: session=<세션값 마스킹>\r\n")
resp_masked = re.sub(r'"email":\s*"[^"]+"', '"email": "<마스킹>"', bob.text)
resp_masked = re.sub(r'"addr":\s*"[^"]+"', '"addr": "<마스킹>"', resp_masked)
with open("evidence_idor.txt", "w", encoding="utf-8") as f:
f.write("== 요청 원문 ==\n" + req_text +
"\n== 응답(민감정보 마스킹) ==\nHTTP/1.1 200 OK\n" + resp_masked + "\n")
print("\n[증거] evidence_idor.txt 저장:")
print(req_text + "---\nHTTP/1.1 200 OK\n" + resp_masked)
# 범위 추정: 연쇄 ID인지 확인 (추가 요청 딱 1건 — 경계 확인용)
probe = a.get(f"{API}/api/v1/users/1003")
print("\n[범위] 존재하지 않는 ID(1003):", probe.status_code,
"— 연쇄 ID 구조, 계정 수만큼 노출 추정")
입력:
python verify.py
출력 (2026-09-09 실측):
[검증] A 세션으로 A(1001): 200
[검증] A 세션으로 B(1002): 200
[판정] 영향 증명됨 — 타인 데이터 1건 열람, 여기서 중단
[증거] evidence_idor.txt 저장:
GET /api/v1/users/1002 HTTP/1.1
Host: 127.0.0.1:5003
Cookie: session=<세션값 마스킹>
---
HTTP/1.1 200 OK
{"addr": "<마스킹>","email": "<마스킹>","id":1002,"phone":"010-****-1002","user":"bob"}
[범위] 존재하지 않는 ID(1003): 404 — 연쇄 ID 구조, 계정 수만큼 노출 추정
읽는 법: 세 단계를 봅니다. ① 검증은 요청 2개로 끝났습니다 — 1002를 읽은 순간 성립이 증명됐고 거기서 멈췄습니다. ② 증거 파일에서 세션 쿠키와 이메일·주소가 마스킹됐습니다. ③ 범위 추정은 "존재하지 않는 ID"로 했습니다 — 404가 돌아오는 것으로 "ID가 연쇄 숫자 구조"임을 확인했고, 다른 실존 계정의 데이터는 추가로 열지 않았습니다. 이 마지막 절제가 PoC 최소화입니다.
3-3. 범위의 기록 — 검증과 추정의 분리
방금 확인한 것과 추정한 것을 나눠 적습니다. 이 구분이 다음 챕터(제보서)의 신뢰를 결정합니다.
검증됨: A 계정 세션으로 B 계정(id 1002)의 이메일·전화번호·주소 열람 가능.
추정: ID가 연쇄 숫자(1001, 1002, ...)이므로 전체 계정 수만큼 노출 가능할 것.
(다른 실존 ID는 열어 보지 않음 — 1건 증명으로 충분)
읽는 법: "검증됨"은 요청/응답 원문이 증거로 있는 사실이고, "추정"은 구조로부터의 논리적 확장입니다. 둘을 섞어 쓰면 과장이 되고, 트리아저는 과장된 제보를 신뢰하지 않습니다.
3-4. 나머지 후보의 심판 — 판정 연습
나머지 후보에도 같은 두 조건(재현성 + 보안 영향)을 적용해 봅니다. 랩 기준 판정입니다.
| 후보 | 재현성 | 보안 영향 | 판정 |
|---|---|---|---|
| T5 IDOR | 있음 (2요청) | 있음 (개인정보 유출) | 유효 — 제보 진행 |
| T6 토큰 예측 | 연속 숫자 확인 | 토큰으로 실제 재설정이 되는지 미확인 | 보류 — 영향 증명 막힘 |
| T1 세션 재사용 | 있음 | 탈취 선행 조건 필요 — 영향 제한적 | 정보성 후보 |
| T4 dev /admin | 있음 | dev 환경, 실데이터 없음 | 정보성 후보 |
| T2 반사형 XSS | 반사 확인됨 | 피해자 클릭 필요, 세션 탈취 시나리오 구성 가능 | 보류(영향 서술 약함) |
| T3 업로드 우회 | 통과 확인 | 업로드된 파일이 실행/배포되는지 미확인 | 보류 |
읽는 법: "보류"의 기준을 보세요 — 영향이 증명되지 않는 후보는 제보해도 거절됩니다. T6는 연속 숫자까지는 확인했지만, 그 토큰으로 실제 비밀번호를 바꾸는 경로를 확인하지 못했으므로 지금 제보하면 N/A를 받습니다. 보류하고 다음 관점(다른 서브도메인, 다른 기능)으로 넘어가는 것이 효율입니다.
4. 미션과 연습문제
미션 — 유효 취약점 1건 확정과 증거 패키지
verify.py를 실행해 출력을 저장하고evidence_idor.txt가 생성됐는지 확인합니다- 증거 파일의 마스킹을 검토합니다 — 세션 값, 이메일, 주소가 가려졌는지
- 3-4의 판정 표를 자기 후보 목록 기준으로 다시 작성합니다 — 각 항목에 "재현성/영향/판정" 세 칸
- "보류"로 판정한 후보 하나에 대해, 영향을 증명하려면 어떤 확인이 더 필요한지를 한 문장으로 적습니다 (실행은 하지 않아도 됩니다)
연습문제
문제 1. "에러가 난다"와 "취약점이다"의 차이를 두 조건(재현성·보안 영향)으로 설명해 보세요.
문제 2. IDOR 검증에서 타인 데이터 1건 열람은 증명인데 100건 열람은 침해라고 하는 이유를 설명해 보세요.
문제 3. 3-2에서 범위 추정을 위해 존재하지 않는 ID(1003)를 요청한 것은 왜 안전한 확인인지 설명해 보세요.
문제 4. "검증됨"과 "추정"을 제보에서 분리해야 하는 이유를 트리아저의 관점에서 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
"보류 후보의 추가 확인" 문장 예시:
T6 토큰 예측 — "발급된 토큰 값이 응답이나 메일 모사 경로로 관측 가능한지,
그리고 그 토큰으로 실제 비밀번호 변경 엔드포인트가 동작하는지"를 확인해야
계정 탈취 영향이 증명된다. 현재는 '연속 숫자 발급' 사실만 확인된 상태.
검증하는 법: ① verify.py 출력이 저장됐는가. ② 증거 파일에 마스킹이 적용됐는가. ③ 판정 표에 세 칸이 있는가. ④ 보류 사유가 "무엇이 더 필요한가"로 쓰였는가. 전부 ‘예’이면 완성입니다.
연습문제 해답
문제 1 해답. 에러는 재현되더라도 그것만으로는 공격자가 얻는 것이 없습니다 — 보안 영향이 없는 오작동은 취약점이 아니라 품질 결함입니다. 반대로 영향이 있어 보여도 재현 단계가 없으면 심사자가 확인할 수 없어 판정이 불가능합니다. "재현 가능하고 + 보안 영향이 있는" 것만이 취약점입니다.
문제 2 해답. 1건 열람은 "소유자 검증이 없다"는 결함의 성립을 보이는 데 필요한 최소 행위입니다. 2건부터는 증명이 아니라 실제 개인정보 수집이 되며, 그 행위는 대부분의 프로그램 규칙과 법이 금지하는 침해입니다. 증명의 충분조건이 위반의 시작점이기도 하므로, 프로는 성립이 보이는 순간 멈춥니다.
문제 3 해답. 존재하지 않는 ID에 대한 요청은 어떤 사용자의 데이터도 반환하지 않습니다 — 404는 "없다"는 정보만 줍니다. 그러면서도 "ID가 연쇄 숫자 구조이고 서버가 순번으로 조회한다"는 구조 정보는 얻을 수 있습니다. 타인 데이터를 열지 않고 범위를 추정하는, 피해 없는 확인법입니다.
문제 4 해답. 트리아저는 제보서를 근거로 등급과 보상을 정합니다. 검증된 사실에는 증거(요청/응답)가 붙지만 추정에는 없으므로, 둘이 섞여 있으면 어느 문장까지 믿을 수 있는지 판단할 수 없습니다. "검증: 타인 게시물 1건 열람 가능 / 추정: 전체 사용자 대상일 것"처럼 분리된 제보는 심사가 빠르고, 이런 정직함이 제보자의 평판이 됩니다.
완료 기준 체크리스트
- [ ] 취약점의 두 조건(재현성 + 보안 영향)을 말할 수 있다
- [ ] 후보를 영향도 순으로 정렬할 수 있다
- [ ]
verify.py로 IDOR을 요청 2건으로 확정하고 멈췄다 - [ ] 증거 파일에 세션·개인정보 마스킹을 적용했다
- [ ] "검증됨"과 "추정"을 분리해 기록했다
- [ ] 영향 미증명 후보를 보류하는 판단의 기준을 설명할 수 있다
6. 흔한 실수와 해결
벽 1. 될 듯하면서 안 되는 후보에 매달린다
증상: 토큰 예측처럼 "거의 될 것 같은" 후보를 며칠씩 붙듭니다.
원인: 매몰 비용 — 여기까지 온 게 아까워서입니다.
해결: 기준은 하나입니다 — "영향이 증명되지 않는 취약점은 제보해도 거절된다." 증명이 막히면 보류하고 다음 후보나 다음 자산으로 넘어가세요. 보류는 포기가 아니라 순서 바꾸기입니다.
벽 2. 영향을 "더 크게" 보이려다 선을 넘는다
증상: 더 많은 ID, 더 많은 데이터를 요청해 피해 규모를 보여주고 싶어집니다.
원인: 영향도 과시의 유혹입니다.
해결: 영향의 크기는 문장("연쇄 ID 구조로 전체 계정 노출 추정")으로 보이고, 존재는 1건으로 증명하세요. 대량 열람은 보상이 아니라 규칙 위반 판정의 사유가 됩니다.
벽 3. 증거에 내 세션 쿠키가 그대로 찍힌다
증상: raw request를 복사했더니 Cookie: session=eyJ...가 통째로 들어갑니다.
원인: 원문 그대로 붙여넣은 것입니다.
해결: 3-2처럼 session=<세션값 마스킹>으로 바꾼 사본을 만드세요. 세션 쿠키는 그 자체로 내 계정의 열쇠입니다. 스크린샷의 이메일·주소도 같은 원칙입니다.
벽 4. FileNotFoundError — 증거 파일이 안 생긴다
증상:
FileNotFoundError: [Errno 2] No such file or directory: 'evidence_idor.txt'
원인: 드물게 작업 디렉터리가 다르거나, 경로에 없는 폴더를 지정한 경우입니다.
해결: 스크립트와 같은 폴더에서 실행하세요. 파일은 상대 경로로 현재 디렉터리에 생깁니다 — 생성 후 목록(ls evidence_idor.txt)으로 확인하는 습관을 붙이세요.
벽 5. "취약점 없음" 결론을 하루 만에 내린다
증상: 후보가 다 보류되자 "이 타깃은 안전하다"고 결론 내립니다.
원인: 탐색 관점이 하나였던 것입니다.
해결: "취약점 없음"은 수 주의 탐색 뒤에 내리는 결론입니다. 다른 서브도메인, 모바일 API, Step 315에서 JS로 찾은 숨은 엔드포인트(/api/internal/metrics 같은)로 돌아가 보세요. 관점을 바꾸면 지도가 달라 보입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 취약점의 두 조건 | 재현 가능 + 보안 영향 — 하나라도 없으면 거절 |
| 영향도 순 정렬 | 타인 데이터 접근 > 인증 우회 > 저장형 XSS > 기타 |
| PoC 최소화 | 성립이 보이는 순간 멈춘다 — 1건이 증명, 100건은 침해 |
| 검증/추정 분리 | 증거 있는 사실과 구조적 추정을 나눠 쓴다 |
| 증거 마스킹 | 세션·개인정보는 가린 사본 — 증거의 위생 = 제보자의 신뢰 |
| 보류 판단 | 영향 미증명 후보는 제보하지 않는다 — 다음 관점으로 |
오늘의 명령어·코드
| 도구 | 하는 일 |
|---|---|
A 세션으로 GET /users/1002 1건 |
IDOR 최소 검증 — 성공 즉시 중단 |
| 존재하지 않는 ID 요청 (404 확인) | 피해 없는 범위 추정 |
re.sub(패턴, "<마스킹>", 응답) |
증거의 민감정보 마스킹 |
요청 원문 파일 저장 (evidence_*.txt) |
재현 단계의 증거 확보 |
명령어보다 중요한 감각
검증의 품격은 "얼마나 깊이 들어갔나"가 아니라 "어디서 멈췄나"로 결정됩니다. 요청 2개로 영향을 증명하고, 범위는 404 하나로 추정하고, 증거는 마스킹해서 남기는 — 이 절제가 있어야 제보가 신뢰받고, 여러분이 프로그램 안에서 계속 활동할 수 있습니다. 이제 유효 취약점 1건과 증거 패키지가 손에 있습니다. 남은 것은 이것을 트리아저가 5분 안에 재현할 수 있는 글로 옮기는 일입니다.
전부 체크되면 Step 317 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.