Step 176. CTF 맛보기 1: Web 문제 5개 — 실전 경기장에 입장하다

Step 176. CTF 맛보기 1: Web 문제 5개 — 실전 경기장에 입장하다

Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★☆☆ | 예상 소요 시간 5시간

전제: Level 2 완료(Step 175). 웹 취약점 기초(SQLi, XSS, 업로드, 쿠키)를 실습해 봤습니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 드림핵·pwnable.kr 등 이 챕터에 등장하는 플랫폼은 풀라고 만들어진 합법 학습 플랫폼입니다.

  • 준비물: 파이썬(Flask)과 curl. 외부 플랫폼을 쓸 분은 드림핵 계정.
  • 주의: 이 환경에서는 외부 CTF 서버에 접속하지 않습니다. 플랫폼 화면은 "화면 예시"로, 뼈대가 되는 다섯 문제는 여러분이 직접 띄우는 로컬 미니 CTF로 실측합니다.

Level 3의 시작입니다. Level 2까지가 기초 체력이라면, CTF는 실전 경기입니다. 문제마다 서버나 파일이 주어지고, 취약점을 찾아 플래그(flag)를 제출하면 점수가 됩니다. 오늘의 목표는 점수가 아니라 감각입니다 — "실전 문제는 수업과 무엇이 다른가"를 몸으로 재는 날입니다.


1. 학습 목표

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

  • CTF의 형식(플래그, 카테고리, 제출)을 설명한다
  • 웹 문제의 대표 유형(정보 노출, 헤더, 쿠키, 접근 제어)별 접근 순서를 안다
  • "이 기능의 내부 동작 추측"을 먼저 쓰고 푸는 습관으로 문제에 접근한다
  • 풀이 결과를 유형·소요 시간·막힌 지점으로 기록한다
  • 못 푼 문제를 "개념 부족 / 도구 / 리서치"로 분류한다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬(Flask 미니 CTF 서버) + curl (실측: Python 3.12, Flask 3.1)
오늘의 명령어 curl -s(본문), curl -sI(헤더), curl --cookie(쿠키 조작)
필요한 개념 플래그 형식, CTF 카테고리, 정보 노출, 쿠키 조작(Step 134), IDOR(Step 149)
오늘의 산출물 풀이 기록표 5행 + 자가 평가 한 문단

2-1. CTF — Capture The Flag

CTF(Capture The Flag)는 취약한 프로그램에서 숨겨진 문자열(플래그)을 찾아 제출하는 대회 형식입니다. 입문자가 만날 형식은 대부분 제퍼디(Jeopardy) 방식 — 분야별 문제 목록에서 골라 푸는 방식입니다.

플래그는 약속된 형식이 있습니다. DH{...}(드림핵), FLAG{...}, flag{...}처럼 "중괄호 앞의 접두어"가 문제마다 정해져 있고, 그 형식의 문자열을 찾아 제출 칸에 넣으면 정답 처리됩니다. 그래서 CTF 플레이는 곧 "약속된 형태의 문자열 찾기"입니다 — 수상한 문자열을 보는 눈이 첫 번째 도구입니다.

2-2. 카테고리 — 다섯 개의 종목

CTF 문제는 분야로 나뉩니다. 이번 Level 3 초반에 다섯 개를 차례로 맛봅니다:

카테고리 무엇을 하는 종목인가
Web 웹 서비스의 취약점으로 플래그 획득 — Level 2 웹 파트의 실전판
Pwn 바이너리의 메모리 취약점 공격 (Step 177)
Reversing 실행 파일을 해부해 로직 파악 (Step 178)
Crypto 암호 구현의 수학적 허점 찾기 (Step 179)
Forensics/Misc 파일·패킷·이미지 속 증거 복구 (Step 180)

오늘은 그중 Web입니다. Level 2에서 가장 많이 연습한 분야라 첫 경기 종목으로 제격입니다.

2-3. 수업과 실전의 차이 — 힌트가 없다

수업(워게임, DVWA)과 CTF의 가장 큰 차이는 힌트의 부재입니다. DVWA는 취약점의 이름이 메뉴에 적혀 있지만, CTF 문제는 그냥 "서비스가 하나 있습니다"가 전부입니다. 어디가 구멍인지 찾는 것부터가 문제입니다.

그래서 접근 순서가 정해져 있습니다: ① 기능 목록 만들기(이 서비스는 뭐가 되나) → ② 각 기능의 내부 동작 추측 쓰기 → ③ 추측이 어긋나는 지점 = 취약점 후보. "어긋남을 찾는다"가 CTF의 엔진입니다.

2-4. 기록이 점수다 — 오늘의 진짜 과제

오늘 다섯 문제의 성적표는 "해결 여부"가 아니라 기록표입니다. 각 문제마다 네 칸을 채웁니다: 해결 여부 | 취약점 유형 | 소요 시간 | 막힌 지점. 못 풀어도 기록이 있으면 오늘은 성공입니다 — 그 기록이 Step 181(주력 분야 선정)의 원료가 됩니다.


3. 따라 하기

3-1. 대회장 열기 — 로컬 미니 CTF

외부 플랫폼 문제는 서버를 우리가 볼 수 없으니, 뼈대가 되는 다섯 유형을 직접 띄워 풉니다. 아래는 웹 문제의 고전 다섯 유형을 한 서버에 담은 미니 CTF입니다. step176_ctf.py로 저장하세요. (지금은 코드를 읽지 말고 먼저 풀어 보는 것을 권합니다 — 정답지를 보고 시험 보는 셈이니까요. 다 풀고 나서 코드를 읽으며 대조하세요.)

입력 (step176_ctf.py)

from flask import Flask, request, make_response

app = Flask(__name__)

F1 = "FLAG{v1ew_s0urce_1s_th3_f1rst_t00l}"
F2 = "FLAG{r0b0ts_txt_1s_n0t_a_d00r}"
F3 = "FLAG{r34d_th3_h34d3rs_c4r3fully}"
F4 = "FLAG{c00k13s_c4n_b3_c00k3d}"
F5 = "FLAG{1d0r_ch4ng3_th3_numb3r}"

@app.route("/")
def index():
    return f"""<h1>미니 CTF 대회장</h1>
    <p>다섯 개의 플래그를 찾으세요. 힌트는 없습니다.</p>
    <ul><li><a href="/robots.txt">robots.txt</a></li>
    <li><a href="/notice">공지사항</a></li>
    <li><a href="/profile?uid=2">내 프로필</a></li></ul>
    <!-- 개발자 메모: 첫 플래그는 여기. {F1} -->"""

@app.route("/robots.txt")
def robots():
    return "User-agent: *\nDisallow: /secret_backup\n", 200, {"Content-Type": "text/plain"}

@app.route("/secret_backup")
def backup():
    return f"백업 폴더입니다. {F2}"

@app.route("/notice")
def notice():
    resp = make_response("<h1>공지사항</h1><p>이 페이지에는 보이는 것이 전부가 아닙니다.</p>")
    resp.headers["X-Flag"] = F3
    return resp

@app.route("/enter")
def enter():
    if request.cookies.get("admin") == "true":
        return f"관리자님 환영합니다. {F4}"
    resp = make_response("<p>일반 사용자입니다. 관리자만 플래그를 볼 수 있습니다.</p>")
    resp.set_cookie("admin", "false")
    return resp

@app.route("/profile")
def profile():
    uid = request.args.get("uid", "2")
    if uid == "1":
        return f"<h1>관리자 프로필</h1><p>이름: admin / 등급: 최고 / {F5}</p>"
    return f"<h1>사용자 {uid}의 프로필</h1><p>이름: user{uid} / 등급: 일반</p>"

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8176)

실행

python step176_ctf.py

이제 여러분은 참가자입니다. http://127.0.0.1:8176/이 오늘의 대회장입니다. 아래 3-2~3-6은 각 문제의 풀이 해설이니, 먼저 스스로 풀어 보고 막히면 그 문제의 절만 펴서 읽으세요.

예측: 풀기 전, 다섯 문제 각각에 "어디에 숨겼을까" 추측을 한 줄씩 써 두세요. 이 책의 반복 규칙 — 추측 → 확인이 CTF에서도 그대로입니다.

3-2. 문제 1 풀이 — 소스 보기의 고전

메인 페이지의 HTML 전체를 봅니다. 브라우저의 "소스 보기"와 같은 일을 curl로:

curl -s http://127.0.0.1:8176/
    <li><a href="/profile?uid=2">내 프로필</a></li></ul>
    <!-- 개발자 메모: 첫 플래그는 여기. FLAG{v1ew_s0urce_1s_th3_f1rst_t00l} -->

(2026-09-09 실측. 앞부분 생략.)

출력 읽는 법: HTML 주석 <!-- --> 안에 플래그가 있습니다. 브라우저 화면에는 보이지 않지만 소스에는 있는 정보 — 개발자가 지우는 것을 잊은 메모는 실제 사고에서도 플래그만큼 자주 털립니다. 첫 접근은 언제나 전체 소스 읽기입니다.

3-3. 문제 2 풀이 — robots.txt는 문이 아니다

메인 페이지의 링크 목록에 있던 robots.txt를 엽니다:

curl -s http://127.0.0.1:8176/robots.txt
User-agent: *
Disallow: /secret_backup

(2026-09-09 실측.)

Disallow는 "검색 로봇아 여기 오지 마"라는 부탁일 뿐, 잠금장치가 아닙니다. 공격자에게는 오히려 "숨기고 싶은 경로 목록"입니다. 그대로 가 봅니다:

curl -s http://127.0.0.1:8176/secret_backup
백업 폴더입니다. FLAG{r0b0ts_txt_1s_n0t_a_d00r}

(2026-09-09 실측.)

출력 읽는 법: Step 145(정보 노출)의 실전형입니다. 실제 CTF와 모의해킹에서 robots.txt, .git/, .DS_Store, 백업 파일(index.php.bak)은 1순위 점검 목록입니다.

3-4. 문제 3 풀이 — 보이는 것이 전부가 아니다

공지사항 페이지는 본문에 아무것도 없습니다. 이럴 때 보는 곳이 응답 헤더입니다:

curl -sI http://127.0.0.1:8176/notice
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.12.14
Date: Wed, 09 Sep 2026 08:08:51 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 88
X-Flag: FLAG{r34d_th3_h34d3rs_c4r3fully}
Connection: close

(2026-09-09 실측.)

출력 읽는 법: -I는 헤더만 받는 옵션입니다. X-로 시작하는 헤더는 비표준 확장 헤더 — 개발자가 임의로 넣은 것입니다. 본문이 깨끗해 보일 때 헤더를 보는 습관은 Step 175에서 방어 검증용으로 썼던 바로 그 명령입니다. 공격과 방어가 같은 도구를 씁니다.

3-5. 문제 4 풀이 — 쿠키는 구울 수 있다

/enter에 접속하면 서버가 쿠키를 내려줍니다:

curl -si http://127.0.0.1:8176/enter
Set-Cookie: admin=false; Path=/
...
<p>일반 사용자입니다. 관리자만 플래그를 볼 수 있습니다.</p>

(2026-09-09 실측.)

admin=false라는 쿠키가 보입니다. 쿠키는 클라이언트가 보관하는 값 — 즉, 내가 바꿀 수 있습니다 (Step 134). falsetrue로 바꿔 다시 요청합니다:

curl -s --cookie "admin=true" http://127.0.0.1:8176/enter
관리자님 환영합니다. FLAG{c00k13s_c4n_b3_c00k3d}

(2026-09-09 실측.)

출력 읽는 법: 서버가 "쿠키 값"만 믿고 권한을 판단했기 때문에 성공했습니다. 실제 서비스가 이러면 안 되는 이유(그래서 세션은 서버가 관리하고 서명합니다 — Step 131, 134)와, CTF에서는 이런 순진한 서버를 만난다는 것, 둘 다 기억하세요.

3-6. 문제 5 풀이 — 숫자를 바꿔 보자

메인 페이지의 마지막 링크는 /profile?uid=2였습니다. "내 프로필"인데 주소창에 내 번호가 적혀 있습니다. 번호를 바꾸면?

curl -s "http://127.0.0.1:8176/profile?uid=1"
<h1>관리자 프로필</h1><p>이름: admin / 등급: 최고 / FLAG{1d0r_ch4ng3_th3_numb3r}</p>

(2026-09-09 실측.)

출력 읽는 법: IDOR(Insecure Direct Object Reference, Step 149)입니다 — 서버가 "누가 요청했나"를 확인하지 않고 "몇 번을 요청했나"만 봤습니다. 주소의 숫자·이름·파일명을 바꿔 보는 것은 웹 문제의 기본 체조입니다.

3-7. 기록표 채우기 — 오늘의 성적표

다섯 문제의 결과를 표로 정리합니다:

문제 해결 취약점 유형 소요 시간 막힌 지점
1 O/X 정보 노출(HTML 주석) 예: 주석을 볼 생각을 늦게 함
2 O/X 정보 노출(robots.txt)
3 O/X 정보 노출(응답 헤더)
4 O/X 쿠키 조작(인가 우회)
5 O/X IDOR

그리고 자가 평가 한 문단: "다섯 유형 중 무엇이 가장 빨리 풀렸고, 무엇이 막혔는가. 웹이 나의 주력 분야 후보가 될 만한가?" 이 문단이 Step 181에서 분야를 고를 때의 증거 자료입니다.

외부 플랫폼(드림핵)에서 도전할 분은 여기서 난이도 2~3 문제 5개를 골라 같은 기록표로 풀면 됩니다. 문제당 2시간 상한, 넘기면 풀이를 보고 이해한 뒤 "막힌 지점"을 채우세요 (화면 예시: 문제 제목·난이도·서버 접속 주소·플래그 제출 칸이 한 화면에 있는 형태입니다).


4. 미션과 연습문제

미션 — 다섯 문제 기록표 완성

로컬 미니 CTF(또는 드림핵 웹 난이도 2~3 다섯 문제)를 풀고 3-7의 기록표를 완성하세요. 단, 각 문제의 "막힌 지점" 칸에는 못 푼 경우뿐 아니라 푼 경우에도 처음 떠올린 추측이 맞았는지를 적습니다. 추측의 적중률이 여러분의 성장 속도계입니다.

연습문제

문제 1. CTF에서 플래그 형식(예: FLAG{...})이 약속돼 있는 것이 왜 중요한가요? 풀이 과정에서 이 형식이 주는 이점을 설명하세요.

문제 2. robots.txtDisallow가 왜 보안 장치가 아닌지, 오늘 풀이를 근거로 설명하세요.

문제 3. 문제 4(쿠키 조작)가 실제 서비스에서 막히려면 서버는 무엇을 해야 하나요? Step 131/134에서 배운 두 장치를 답하세요.

문제 4. 문제 5(IDOR)와 문제 4(쿠키 조작)는 둘 다 "남의 권한 획득"입니다. 서버가 놓친 검사가 각각 무엇인지 비교하세요.


5. 모범 답안과 완료 기준

미션 모범 답안

기록표의 예 (시간은 각자 다릅니다 — 중요한 것은 칸이 채워졌다는 것):

1. O | HTML 주석 정보 노출 | 5분 | 소스 전체를 읽는 습관이 바로 발동
2. O | robots.txt 경로 노출 | 2분 | Disallow = 숨긴 경로 목록임을 알고 있었음
3. O | 응답 헤더 노출 | 15분 | 본문만 보다 헤더 생각이 늦음 → curl -I 습관화 필요
4. O | 쿠키 조작 | 3분 | admin=false를 보고 즉시 true로 시도
5. O | IDOR | 4분 | uid=2 → 1로 변경, 첫 추측 적중

자가 평가: 정보 노출 유형(1~3) 중 헤더가 가장 늦었다.
"본문 → 소스 → 헤더" 순서로 보는 체크리스트를 손에 붙일 것.
웹은 Level 2 실력이 그대로 통했다 — 주력 분야 후보로 유력.

검증하는 법: ① 다섯 행 모두 유형이 채워졌는가, ② 막힌 지점이 "모름"이 아니라 구체적 행동("헤더를 안 봄")인가, ③ 자가 평가에 다음 행동(체크리스트 등)이 있는가.

연습문제 해답

문제 1 해답. 정답의 모양을 알려 주기 때문입니다. 형식을 알면 ① 수상한 문자열을 스캔할 때 무엇을 찾는지 명확하고(FLAG{로 검색 가능), ② 찾은 것이 정답인지 확인할 수 있으며, ③ 제출 시스템이 자동 채점을 할 수 있습니다. 실전 팁: 어떤 문제든 먼저 플래그 형식을 확인하고 그 접두어로 검색부터 하는 것이 시간을 아낍니다.

문제 2 해답. Disallow는 로봇에게 보내는 "부탁"이지 접근 통제가 아니기 때문입니다. 오늘 실측에서 /secret_backup은 아무 인증 없이 그대로 열렸습니다. 보안은 "알려 주지 않는 것"이 아니라 "요청해도 거부하는 것(인증·인가)"에서 나옵니다 — robots.txt는 정작 숨기고 싶은 경로의 목록을 공개하는 셈입니다.

문제 3 해답. ① 권한 정보를 클라이언트 쿠키에 두지 않고 서버 측 세션으로 관리할 것(쿠키에는 뜻 없는 세션 ID만, 서명된 값만 둠). ② 쿠키를 쓰더라도 위조를 막는 서명(HMAC)을 붙일 것 — Flask의 session이 하는 것이 이것입니다 (Step 131). 핵심은 "클라이언트가 보낸 값은 전부 조작 가능하다"는 전제에서 설계하는 것입니다.

문제 4 해답. 문제 4는 인증 상태의 저장 위치를 놓쳤습니다 — "관리자 여부"를 클라이언트 쿠키에 맡겼습니다. 문제 5는 인가(authorization) 검사 자체를 놓쳤습니다 — 로그인한 사용자가 uid=1의 자원을 볼 자격이 있는지를 묻지 않았습니다. 둘의 공통 뿌리는 "요청을 보낸 주체를 서버가 검증하지 않음"이고, 이것이 웹 취약점의 절반을 관통하는 주제입니다.

완료 기준 체크리스트

  • [ ] CTF의 형식(플래그·카테고리·제출)을 설명할 수 있다
  • [ ] 플래그 형식(접두어+중괄호)의 용도를 안다
  • [ ] 로컬 미니 CTF의 다섯 문제를 풀었다(또는 플랫폼 5문제에 도전했다)
  • [ ] "소스 → robots.txt 등 숨은 경로 → 헤더 → 쿠키 → 파라미터" 접근 순서를 체득했다
  • [ ] 기록표(해결 여부/유형/시간/막힌 지점)를 다섯 행 채웠다
  • [ ] 자가 평가 한 문단(주력 분야 후보 판단)을 썼다
  • [ ] 못 푼 문제는 풀이를 이해한 뒤 기록으로 대체했다

6. 흔한 실수와 해결

벽 1. 어디서부터 시작해야 할지 모르겠다

증상: 페이지를 열었는데 멍하니 바라만 봅니다.
원인: CTF는 메뉴(취약점 이름)를 주지 않습니다. 찾는 것부터가 문제입니다.
해결: 오늘의 접근 순서를 그대로 따라 하세요 — ① 페이지 소스 전체 읽기 ② robots.txt·흔한 파일명 시도 ③ 헤더 보기(curl -sI) ④ 쿠키 확인 ⑤ 주소의 파라미터 바꾸기. 이 다섯 동작이면 입문 웹 문제의 대부분이 열립니다.

벽 2. 다섯 문제 중 서너 개를 못 푼다

증상: Level 2를 마쳤는데도 연패합니다.
원인: 정상입니다. 수업과 실전 사이에는 "힌트 없음"이라는 벽이 하나 더 있고, 그 벽은 경험으로만 낮아집니다.
해결: 점수가 아니라 분류를 하세요 — "개념을 몰랐다 / 도구를 몰랐다 / 알았는데 시도를 안 했다" 중 어디인지. 오늘의 완료 기준은 해결 수가 아니라 기록표입니다. 2시간 넘긴 문제는 풀이를 보고 이해한 뒤 "막힌 지점"만 정확히 채우면 그 문제는 다음에 여러분 것이 됩니다.

벽 3. curl 출력이 한글이면 깨져 보인다

증상: Git Bash에서 한글 포함 응답이 깨집니다.
원인: 터미널 인코딩 차이입니다 (UTF-8 vs CP949).
해결: 플래그는 ASCII라 풀이에는 지장이 없습니다. 확인이 필요하면 curl -s URL | iconv -f utf-8로 변환하거나 브라우저로 여세요.

벽 4. 서버를 띄웠는데 접속이 안 된다

증상: curl이 연결 거부됩니다.
원인: 서버 프로세스가 안 떴거나, 이전 실습 프로세스가 포트를 잡고 있습니다.
해결: netstat -ano | findstr 8176으로 포트 상태를 확인하고, 이미 있으면 그 PID를 taskkill /PID 번호 /F로 정리 후 재기동. Step 174 벽 2와 같은 처치입니다.

벽 5. 외부 플랫폼 문제가 로컬 문제보다 훨씬 어렵다

증상: 미니 CTF는 다 풀었는데 드림핵 난이도 2가 안 풀립니다.
원인: 당연한 차이입니다. 미니 CTF는 오늘 배운 다섯 유형만 담았고, 실전 문제는 취약점의 조합·필터·함정을 섞습니다.
해결: 로컬에서 익힌 접근 순서를 그대로 적용하되, 각 단계에서 "조합"을 의심하세요. 그리고 Step 154의 약점 목록을 옆에 펴세요 — 자주 나오는 유형이 거기 정리돼 있습니다.


7. 정리

오늘의 개념

개념 한 줄 설명
CTF / 플래그 숨겨진 약속 문자열을 찾아 제출하는 대회 — 형식(접두어)이 힌트
제퍼디 방식 분야별 문제 목록에서 골라 푸는 입문 표준 형식
접근 순서 소스 → 숨은 경로 → 헤더 → 쿠키 → 파라미터
정보 노출 주석·robots.txt·헤더 — "지우는 것을 잊은" 정보의 습격
기록표 해결 여부보다 유형·막힌 지점이 자산이다

오늘의 명령어

명령 하는 일
curl -s URL 페이지 소스 전체 받아 읽기 (브라우저 소스 보기의 CLI판)
curl -sI URL 응답 헤더만 보기 — X- 확장 헤더 점검
curl -s --cookie "k=v" URL 쿠키를 바꿔치기해 요청
curl -s "URL?p=1" 파라미터 값 바꿔 보기 (IDOR 체조)

명령어보다 중요한 감각

오늘 다섯 문제는 새 기술이 하나도 없었습니다. 주석 읽기, robots.txt, 헤더, 쿠키, 파라미터 — 전부 Level 2의 재료였습니다. 달라진 것은 무대입니다. 메뉴가 없는 문제를 만났을 때, 어디서부터 손대는지의 순서가 몸에 붙었는지가 오늘의 실질 점수입니다.

그리고 기록의 습관 — "막힌 지점을 구체적으로 쓴다"는 것은 CTF를 넘어 이 바닥 전체의 공부법입니다. 오늘의 기록표 다섯 행이 Level 3의 첫 페이지입니다. 웹이 어울리는지 아닌지는 이 기록이 말해 줄 것입니다.


전부 체크되면 Step 176 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.