Skip to content

YAML

The vulnerability stems from how YAML aliasing interacts with delayed data mutation.Initially, the validation loop checks conf["blogs"][0]["name"]. Later in the execution, the code applies a transform...

Created

Updated

2 min read

Reading time

2 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Reference manipulation

Event Name[CTF Event Name]
GitHub URL[Challenge Repo/Code URL]
Challenge Name[Specific Challenge Name]
Attachments
References

Putting It Together: The Exploit Chain

The vulnerability stems from how YAML aliasing interacts with delayed data mutation.

Initially, the validation loop checks conf["blogs"][0]["name"]. Later in the execution, the code applies a transformation:

conf["user"]["name"] = display_name(conf["user"].get("name", ...))

If conf["user"] and conf["blogs"][0] are the same dictionary object (created via a YAML alias), writing to conf["user"]["name"] silently overwrites conf["blogs"][0]["name"] after the validation checks have already passed.

The Attack Configuration

Here is the malicious YAML payload used to exploit this:

blogs:
  - &ref
    title: "flag"
    name: "._._/._._/flag"
user: *ref

Step-by-Step Execution

1. YAML Parsing

The yaml.safe_load function creates a single dictionary in memory: {"title": "flag", "name": "._._/._._/flag"}. Because of the &ref anchor and *ref alias, both blogs[0] and user point to this exact same dictionary.

2. Validation Loop

The code validates blogs[0]["name"], which is currently "._._/._._/flag". This successfully bypasses all three security checks:

  • "../" in "._._/._._/flag"False (no ../ substring present)
  • "._._/._._/flag".startswith("/")False
  • The resolved path safely stays under blog_path (since there are no actual directory traversals yet).
  • 3. The Mutation

    After the validation loop completes, the code executes the display name mutation:

    conf["user"]["name"] = display_name(conf["user"].get("name", ...))

    Because conf["user"] is the same dictionary as blogs[0], conf["user"].get("name") retrieves the payload "._._/._._/flag". The display_name function then processes it:

    display_name("._._/._._/flag")
    
    # 1. split("_")     → [".", ".", "/.", ".", "/flag"]
    # 2. capitalize()   → [".", ".", "/.", ".", "/flag"]
    # 3. join("")       → "../../flag"
    Why does capitalize() leave the payload unchanged? > The capitalize() method uppercases only the first character and lowercases the rest. The first character in each part of our split payload is either a . or a /. Since non-alphabetic characters have no uppercase form, they pass through unaffected. The remaining letters (flag) are already lowercase, so lowercasing them is a no-op.

    By concatenating the pieces back together, the code builds up ../../flag. This string overwrites blogs[0]["name"]. Since validation has already occurred, it is too late for the application to catch the newly formed directory traversal payload.

    4. Reading the Blog

    When a user visits /blog/<username>, the server attempts to read the file:

    (blog_path / blog["name"]).read_text()

    This resolves the path as blogs/../../flag/flag, effectively printing the contents of the flag file.


    Would you like me to explain how to patch this specific vulnerability in the Python code?

    Categories & Topics

    This note is categorized under the following topics. Click on any category to explore more related content.

    Share this note

    Share:

    Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.