[🥉 Bronze 3] 뚫을 필요가 없었다, 제대로 보기만 하면 됐다 — DreamHack [wargame.kr] already got 풀이

2026-07-21·1분 읽기·

[🥉 Bronze 3] 뚫을 필요가 없었다, 제대로 보기만 하면 됐다 — DreamHack [wargame.kr] already got 풀이

옛 wargame.kr에서 리마스터해 가져온 문제. 페이지를 열어보면 "you've already got key! :p"라는 한 줄짜리 문구뿐이고, HTML 소스를 봐도 똑같다. 문제 설명이 "HTTP 응답 헤더가 보이나요?"라고 대놓고 힌트를 주는데, 이 짧은 문제 하나가 사실 "브라우저 화면에 보이는 것"과 "서버가 실제로 보낸 것"이 다르다는 웹 리버싱의 가장 기초적인 교훈을 정확히 짚고 있었다. 취약점을 찾아 뚫는 문제가 아니라, HTTP 프로토콜을 있는 그대로 관찰하기만 하면 되는 문제였다.

문제: DreamHack — [wargame.kr] already got 분류: Web 난이도: 🥉 Bronze 3 FLAG: DH{You_C@n_Mak3_Cust0m_HTTP_Head3r}

문제 설명이 아주 짧다. Can you see HTTP Response header? — "HTTP 응답 헤더가 보이나요?" 딱 이 한 줄뿐이다. 그리고 이 문제는 옛날 wargame.kr에 있던 문제를 드림핵이 다시 만들어 가져온 것이라는 설명도 함께 붙어 있다. 짧은 문제 설명이지만, 사실 이게 풀이의 전부이기도 했다.

이 시리즈에서 지금까지 다룬 웹 문제들은 대부분 SQL 인젝션, SSRF, 인증 우회, 파일 업로드처럼 서버 로직의 결함을 찾아 적극적으로 공격 페이로드를 만들어 넣는 유형이었다. 이 문제는 결이 완전히 다르다 — 아무 페이로드도 만들 필요가 없고, 그냥 서버가 원래 하던 응답을 있는 그대로, 하지만 평소보다 조금 더 자세히 들여다보기만 하면 끝난다. Bronze 3라는 상대적으로 낮은 난이도도 여기서 나온다 — 기술적으로 어려운 게 아니라, "웹 요청이 뭘로 이루어져 있는지"에 대한 기초적인 이해를 묻는 문제였다.


문제 개요

VM을 띄우고 접속해봤다.

브라우저로 접속한 페이지 — 주소창에 host3.dreamhack.games 포트가 보이고, 화면엔 you've already got key! :p 라는 문구 하나뿐

you've already got key! :p — "너 이미 키 가지고 있잖아 :p"라는 문구가 전부다. 페이지 소스를 봐도 다르지 않다.

브라우저 view-source로 페이지 원본 HTML을 확인한 화면 — 렌더링된 화면과 마찬가지로 문구 한 줄뿐, 숨겨진 주석이나 스크립트 없음

렌더링된 화면과 HTML 소스가 완전히 같다 — 즉 자바스크립트로 뭔가를 숨겨놓은 것도, HTML 주석 안에 flag를 심어놓은 것도 아니라는 뜻이다. "이미 갖고 있다"는 제목과 문구를 곱씹어보면, 어딘가 이미 우리 눈앞에 와 있는데 우리가 못 보고 있는 데이터가 있다는 뜻으로 읽혔다.

문제 제목 "already got"이 이 문제의 성격을 정확히 요약한다. 보통 워게임 문제는 "찾아라", "뚫어라", "우회하라" 같은 능동적인 동사로 제목이 붙는데, 이 문제는 반대로 "너는 이미 획득한 상태다"라고 선언한다. 이건 단순한 언어유희가 아니라 실제 풀이 방식을 그대로 암시하는 제목이었다 — 우리가 서버에 요청을 보내는 그 순간, flag는 이미 응답 어딘가에 실려서 우리 컴퓨터로 도착해 있다. 남은 건 그 응답을 받아서 "제대로" 읽어내는 것뿐이다.


▶🕳️ 삽질 — 본문(body)만 계속 들여다보고 있었다

처음엔 그냥 반사적으로 브라우저 화면과 페이지 소스만 번갈아 봤다. curl로도 마찬가지로 -s 옵션만 써서 응답 본문(body)만 받아왔다 — 결과는 똑같이 그 문구 한 줄이었다.

curl -s(본문만)과 curl -sI(헤더만)를 나란히 비교한 캡쳐 — 본문에는 문구뿐이지만 헤더에는 FLAG 항목이 그대로 들어있음

HTTP 응답은 사실 "상태줄 + 헤더 + 본문" 세 부분으로 이뤄져 있는데, 브라우저 화면에 렌더링되는 건 이 중 본문뿐이다. 개발자 도구를 열지 않는 한 헤더는 평소에 눈에 들어오지 않는 정보라, "웹 페이지 = 화면에 보이는 텍스트"라는 습관적인 생각에 갇혀 있으면 이 문제의 힌트("HTTP 응답 헤더가 보이나요?")를 곧이곧대로 읽어도 놓치기 쉽다. 문제 설명이 이미 정답을 말해주고 있었는데도, 화면에 뭔가 "찾아야 할" 요소가 있을 거라는 선입견 때문에 잠깐 헤맸다.


💣 핵심 — 헤더까지 함께 보면 flag가 그냥 놓여 있다

curl -I(HEAD 요청) 또는 curl -v(전체 요청/응답 로그)로 헤더를 확인해봤다.

curl -sI 로 확인한 HTTP 응답 헤더 전체 — Server, Date, Content-Type 같은 일반적인 헤더들 사이에 FLAG: DH{...} 커스텀 헤더가 그대로 포함돼 있음

Server, Date, Content-Type, Content-Length 같은 흔한 헤더들 사이에 FLAG: DH{You_C@n_Mak3_Cust0m_HTTP_Head3r}라는 줄이 그대로 끼어 있었다. 서버(Flask/Werkzeug)가 응답을 만들 때 커스텀 HTTP 헤더를 직접 추가하도록 짜여 있었던 것이다. -v 옵션으로 요청·응답 전체를 로그로 남겨보면 더 명확하다.

curl -sv 로 확인한 전체 요청/응답 로그 — > 로 표시된 요청 헤더(GET / HTTP/1.1 등)와 < 로 표시된 응답 헤더가 순서대로 나오고, 그 중 FLAG 헤더가 포함돼 있음

>로 시작하는 줄은 우리가 보낸 요청, <로 시작하는 줄은 서버가 돌려준 응답이다. 서버는 우리가 그냥 평범한 GET / 요청을 보냈을 뿐인데도, 응답 헤더 안에 flag를 얹어서 돌려주고 있었다. 별도의 취약점을 찾아 페이로드를 만들 필요가 전혀 없었다 — 이미 서버가 매 요청마다 flag를 보내주고 있었는데, 그동안 안 보이는 곳을 보고 있었을 뿐이었다.

HTTP 응답의 구조를 다시 한번 정리해보면 이 문제가 왜 성립하는지 더 명확해진다. HTTP 응답은 크게 세 부분으로 나뉜다 — 상태줄(HTTP/1.1 200 OK처럼 요청이 어떻게 처리됐는지 알려주는 한 줄), 헤더(응답에 대한 메타데이터를 이름: 값 형태로 나열한 여러 줄), 그리고 빈 줄 하나를 사이에 두고 이어지는 본문(실제로 화면에 렌더링될 콘텐츠)이다. 브라우저는 이 셋 중 본문만 화면에 그려주고, 상태줄과 헤더는 페이지 렌더링에 직접 쓰이는 게 아니라 브라우저·프록시·캐시 서버 같은 중간 구성요소들이 "이 응답을 어떻게 처리해야 하는지" 판단하는 데 쓰인다. Content-Type이 본문을 HTML로 해석할지 JSON으로 해석할지 알려주고, Content-Length가 본문이 몇 바이트인지 알려주는 식이다. 이 문제의 서버는 그 메타데이터 영역에 FLAG라는, HTTP 표준에는 없는 임의의 커스텀 헤더를 하나 더 끼워 넣었을 뿐이다 — HTTP 스펙 자체가 애플리케이션이 이런 커스텀 헤더를 자유롭게 추가하는 걸 허용하기 때문에, 문법적으로는 전혀 이상할 게 없는 응답이다.

curl 말고 다른 도구로도 같은 결과를 확인했다. 파이썬 requests 라이브러리로 요청을 보내고 response.headers를 그대로 출력해봤다.

파이썬 requests 라이브러리로 같은 서버에 요청을 보내고 response.headers를 순회하며 출력한 결과 — curl과 동일하게 FLAG 헤더가 포함됨

curl과 완전히 같은 헤더가 나온다 — 도구가 무엇이든, HTTP 프로토콜 수준에서 오간 데이터를 있는 그대로 확인하기만 하면 이 문제는 끝난다는 걸 다시 한번 확인했다.

브라우저의 개발자 도구(F12)를 열어 Network 탭에서 이 요청을 다시 살펴봐도 물론 같은 헤더가 보인다 — 브라우저가 헤더 정보를 아예 안 받아오는 게 아니라, 단지 페이지를 "렌더링"할 때 기본적으로 헤더를 화면에 그려주지 않을 뿐이다. curl이나 requests 같은 커맨드라인 도구를 선호하는 이유가 여기 있다 — GUI를 몇 번 클릭해서 패널을 열 필요 없이, HTTP 트랜잭션의 모든 구성 요소를 명령어 한 줄로 텍스트 그대로 받아볼 수 있어서 헤더를 놓치는 실수 자체가 구조적으로 잘 안 생긴다.


🎯 자체 완결 solve

명령 하나로 끝난다.

curl -sI http://host3.dreamhack.games:<port>/ | grep -i flag
전체 풀이 흐름 다이어그램 — 본문만 봐서는 flag가 없다는 걸 확인, HTTP 응답이 상태줄/헤더/본문으로 이뤄져 있다는 걸 상기, curl -I로 헤더까지 확인해 flag를 곧바로 획득하는 3단계

DH{You_C@n_Mak3_Cust0m_HTTP_Head3r}가 그대로 나온다. 이 flag를 드림핵에 제출해 정답으로 확정됐고 VM 크레딧도 곧바로 환급됐다.


📝 결론

이 문제는 짧고 단순하지만, 웹 문제를 풀 때 습관적으로 지나치기 쉬운 지점을 정확히 짚어준다.

  • "화면에 보이는 것"과 "서버가 실제로 보낸 것"은 다르다. 브라우저는 HTTP 응답 중 본문만 렌더링해서 보여주고, 상태 코드나 헤더 같은 메타데이터는 기본적으로 숨긴다. 웹 애플리케이션을 분석할 때는 눈에 보이는 화면 그 자체가 아니라, 그 화면을 만들어낸 원본 응답 전체를 확인하는 습관이 필요하다.
  • curl -I/curl -v는 가장 기본적이면서도 가장 자주 잊히는 정찰 도구다. 복잡한 취약점 스캐너를 돌리기 전에, 일단 서버가 정확히 뭘 돌려주는지부터 확인하는 게 순서다. 이 문제처럼 "취약점"이랄 것도 없이 그냥 데이터가 그 자리에 있는 경우도 실제로 있다.
  • 문제 설명(혹은 힌트)이 정답을 그대로 말해줄 때도 있다. "HTTP 응답 헤더가 보이나요?"라는 문장을 곧이곧대로 받아들이는 게 이 문제에서는 최선의 접근이었다. 어려운 문제를 많이 풀다 보면 오히려 "이렇게 쉬울 리 없다"고 의심하며 더 복잡한 길을 찾게 되는데, 가끔은 힌트를 있는 그대로 믿는 편이 빠르다.
  • 여러 도구로 같은 결과를 재현해보면 확신이 커진다. curl -I, curl -v, 파이썬 requests까지 서로 다른 세 가지 방법으로 같은 헤더를 확인했다. 굳이 한 가지 도구의 결과만 믿기보다, 완전히 다른 구현체로 같은 값을 다시 확인하는 습관은 이렇게 사소해 보이는 문제에서도 "내가 뭔가 착각한 건 아닐까"하는 의심을 깔끔하게 지워준다.

방어자 관점에서 보면, 이 문제는 디버깅이나 내부 용도로 추가한 커스텀 HTTP 헤더가 실서비스에 그대로 남아 민감한 정보를 흘리는 사고를 정확히 재현한다. 실무에서도 X-Debug-Info, X-Powered-By, 혹은 이 문제처럼 아예 직관적인 이름의 커스텀 헤더에 내부 버전 정보, 서버 경로, 심지어 인증 관련 값이 그대로 노출되는 사례가 드물지 않다. 응답 헤더는 body 못지않게 "외부에 공개되는 데이터"라는 인식을 갖고, 배포 전 체크리스트에 반드시 "우리 서버가 어떤 헤더를 보내고 있는가"를 확인하는 항목을 넣어두는 게 이런 사고를 막는 가장 간단한 방법이다.

이런 유형의 정보 노출은 실제로 심각한 사고로 이어진 전례도 여럿 있다. 개발 편의를 위해 남겨둔 X-Powered-By 헤더가 정확한 프레임워크 버전을 노출해서 그 버전에 알려진 취약점을 곧바로 겨냥한 공격으로 이어지는 경우, 로드밸런서나 리버스 프록시가 내부 네트워크 구조를 유추할 수 있는 X-Forwarded-* 계열 헤더를 그대로 클라이언트에 반사해버리는 경우, 심지어 세션 토큰이나 임시 인증 값을 실수로 커스텀 헤더에 실어 보내는 경우까지 — 전부 "본문만 조심하면 된다"는 좁은 시야에서 비롯된 실수다. 로깅·모니터링 시스템을 점검할 때도 마찬가지다. 요청 로그를 남기면서 본문만 기록하고 헤더는 소홀히 검토하다가, 정작 민감한 값이 커스텀 헤더 쪽으로 새고 있었다는 걸 뒤늦게 발견하는 경우도 흔하다. 이 문제가 농담처럼 가벼운 톤으로 포장돼 있지만, 그 밑에 깔린 교훈 자체는 실무에서 꽤 자주 반복되는 실수라는 점에서 결코 가볍지 않다.

이 글이 도움이 됐나요?

Comments

댓글

0개

댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)

댓글 불러오는 중…

Related

관련 글

3개
[🥉 Bronze 4] robots.txt 가 가리킨 플래그가 가짜였다 — DreamHack fakeday 풀이
blog

[🥉 Bronze 4] robots.txt 가 가리킨 플래그가 가짜였다 — DreamHack fakeday 풀이

만우절에 올라온 블랙박스 웹 문제. HTML 주석이 robots.txt 로, robots.txt 가 /secret-flag 로 보내고 거기 있는 값은 "Do you believe me?" 라고 놀린다. 서버에는 파일이 세 개뿐이라 더 팔 곳이 없는데, 정작 진짜 플래그는 "블랙박스"라고 써 놓고 같이 내려준 zip 안에 placeholder 모양으로 들어 있었다.
#dreamhack#ctf#web+6
2026-10-24#dreamhack +4
[🌱 Sprout] GET·POST 요청 체이닝 — 리다이렉트 따라가며 토큰 이어 붙이기 — DreamHack simple-web-request 풀이
blog

[🌱 Sprout] GET·POST 요청 체이닝 — 리다이렉트 따라가며 토큰 이어 붙이기 — DreamHack simple-web-request 풀이

STEP1 → STEP2 → FLAG 세 단계를 정해진 파라미터로 차례로 호출해야 플래그가 나오는 요청 입문 문제다. 각 단계는 앞 단계가 발급한 난수 토큰을 들고 와야 통과하고, 그 토큰은 한 요청 흐름 안에서만 유효하다. 브라우저로 한 번, 그리고 requests.Session 으로 리다이렉트를 자동 추적하며 hidden 토큰을 이어 붙여 플래그를 받았다.
#dreamhack#ctf#web+5
2026-06-24#dreamhack +4
[🥉 Bronze 3] 이스케이프한 건 원문이었지, 해시가 아니었다 — DreamHack [wargame.kr] md5 password 풀이
blog

[🥉 Bronze 3] 이스케이프한 건 원문이었지, 해시가 아니었다 — DreamHack [wargame.kr] md5 password 풀이

mysqli_real_escape_string()이 사용자 입력을 꼼꼼히 이스케이프한다. 그런데 정작 쿼리에 들어가는 건 그 입력이 아니라 md5($ps, true) — raw 16바이트 바이너리 해시다. 해시 결과는 이스케이프 대상이 아니었다. MD5 출력은 사실상 무작위 바이트열이라, 그 바이트들이 우연히 SQL 문법으로 읽히는 문자열을 만들어낼 수 있다. 임의 문자로 시작하는 해시는 1/256 확률로 몇 초 안에 찾을 수 있지만, 실제로 익스플로잇이 되려면 5바이트가 정확히 맞아떨어져야 해서 무작위 탐색은 비현실적이다 — 대신 잘 알려진 매직 스트링을 직접 계산으로 검증하고 그대로 썼다.
#dreamhack#wargame.kr#ctf+5
2026-07-20#dreamhack +3