webfreemedium

restaurant-builder

kitctf

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}.

$ ls tags/ techniques/
environment_variable_exfiltrationpydantic_create_model_evalforward_reference_eval_rcetyping_literal_const_schema_exfilarbitrary_expression_evaluation

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

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.

...

$ grep --similar

Similar writeups