문제: 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을 띄우고 접속해봤다.
you've already got key! :p — "너 이미 키 가지고 있잖아 :p"라는 문구가 전부다. 페이지 소스를 봐도 다르지 않다.
렌더링된 화면과 HTML 소스가 완전히 같다 — 즉 자바스크립트로 뭔가를 숨겨놓은 것도, HTML 주석 안에 flag를 심어놓은 것도 아니라는 뜻이다. "이미 갖고 있다"는 제목과 문구를 곱씹어보면, 어딘가 이미 우리 눈앞에 와 있는데 우리가 못 보고 있는 데이터가 있다는 뜻으로 읽혔다.
문제 제목 "already got"이 이 문제의 성격을 정확히 요약한다. 보통 워게임 문제는 "찾아라", "뚫어라", "우회하라" 같은 능동적인 동사로 제목이 붙는데, 이 문제는 반대로 "너는 이미 획득한 상태다"라고 선언한다. 이건 단순한 언어유희가 아니라 실제 풀이 방식을 그대로 암시하는 제목이었다 — 우리가 서버에 요청을 보내는 그 순간, flag는 이미 응답 어딘가에 실려서 우리 컴퓨터로 도착해 있다. 남은 건 그 응답을 받아서 "제대로" 읽어내는 것뿐이다.
▶🕳️ 삽질 — 본문(body)만 계속 들여다보고 있었다
처음엔 그냥 반사적으로 브라우저 화면과 페이지 소스만 번갈아 봤다. curl로도 마찬가지로 -s 옵션만 써서 응답 본문(body)만 받아왔다 — 결과는 똑같이 그 문구 한 줄이었다.

HTTP 응답은 사실 "상태줄 + 헤더 + 본문" 세 부분으로 이뤄져 있는데, 브라우저 화면에 렌더링되는 건 이 중 본문뿐이다. 개발자 도구를 열지 않는 한 헤더는 평소에 눈에 들어오지 않는 정보라, "웹 페이지 = 화면에 보이는 텍스트"라는 습관적인 생각에 갇혀 있으면 이 문제의 힌트("HTTP 응답 헤더가 보이나요?")를 곧이곧대로 읽어도 놓치기 쉽다. 문제 설명이 이미 정답을 말해주고 있었는데도, 화면에 뭔가 "찾아야 할" 요소가 있을 거라는 선입견 때문에 잠깐 헤맸다.
💣 핵심 — 헤더까지 함께 보면 flag가 그냥 놓여 있다
curl -I(HEAD 요청) 또는 curl -v(전체 요청/응답 로그)로 헤더를 확인해봤다.
Server, Date, Content-Type, Content-Length 같은 흔한 헤더들 사이에 FLAG: DH{You_C@n_Mak3_Cust0m_HTTP_Head3r}라는 줄이 그대로 끼어 있었다. 서버(Flask/Werkzeug)가 응답을 만들 때 커스텀 HTTP 헤더를 직접 추가하도록 짜여 있었던 것이다. -v 옵션으로 요청·응답 전체를 로그로 남겨보면 더 명확하다.
>로 시작하는 줄은 우리가 보낸 요청, <로 시작하는 줄은 서버가 돌려준 응답이다. 서버는 우리가 그냥 평범한 GET / 요청을 보냈을 뿐인데도, 응답 헤더 안에 flag를 얹어서 돌려주고 있었다. 별도의 취약점을 찾아 페이로드를 만들 필요가 전혀 없었다 — 이미 서버가 매 요청마다 flag를 보내주고 있었는데, 그동안 안 보이는 곳을 보고 있었을 뿐이었다.
HTTP 응답의 구조를 다시 한번 정리해보면 이 문제가 왜 성립하는지 더 명확해진다. HTTP 응답은 크게 세 부분으로 나뉜다 — 상태줄(HTTP/1.1 200 OK처럼 요청이 어떻게 처리됐는지 알려주는 한 줄), 헤더(응답에 대한 메타데이터를 이름: 값 형태로 나열한 여러 줄), 그리고 빈 줄 하나를 사이에 두고 이어지는 본문(실제로 화면에 렌더링될 콘텐츠)이다. 브라우저는 이 셋 중 본문만 화면에 그려주고, 상태줄과 헤더는 페이지 렌더링에 직접 쓰이는 게 아니라 브라우저·프록시·캐시 서버 같은 중간 구성요소들이 "이 응답을 어떻게 처리해야 하는지" 판단하는 데 쓰인다. Content-Type이 본문을 HTML로 해석할지 JSON으로 해석할지 알려주고, Content-Length가 본문이 몇 바이트인지 알려주는 식이다. 이 문제의 서버는 그 메타데이터 영역에 FLAG라는, HTTP 표준에는 없는 임의의 커스텀 헤더를 하나 더 끼워 넣었을 뿐이다 — HTTP 스펙 자체가 애플리케이션이 이런 커스텀 헤더를 자유롭게 추가하는 걸 허용하기 때문에, 문법적으로는 전혀 이상할 게 없는 응답이다.
curl 말고 다른 도구로도 같은 결과를 확인했다. 파이썬 requests 라이브러리로 요청을 보내고 response.headers를 그대로 출력해봤다.
curl과 완전히 같은 헤더가 나온다 — 도구가 무엇이든, HTTP 프로토콜 수준에서 오간 데이터를 있는 그대로 확인하기만 하면 이 문제는 끝난다는 걸 다시 한번 확인했다.
브라우저의 개발자 도구(F12)를 열어 Network 탭에서 이 요청을 다시 살펴봐도 물론 같은 헤더가 보인다 — 브라우저가 헤더 정보를 아예 안 받아오는 게 아니라, 단지 페이지를 "렌더링"할 때 기본적으로 헤더를 화면에 그려주지 않을 뿐이다. curl이나 requests 같은 커맨드라인 도구를 선호하는 이유가 여기 있다 — GUI를 몇 번 클릭해서 패널을 열 필요 없이, HTTP 트랜잭션의 모든 구성 요소를 명령어 한 줄로 텍스트 그대로 받아볼 수 있어서 헤더를 놓치는 실수 자체가 구조적으로 잘 안 생긴다.
🎯 자체 완결 solve
명령 하나로 끝난다.
curl -sI http://host3.dreamhack.games:<port>/ | grep -i flag
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
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…