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("/") → Falseblog_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 doescapitalize()leave the payload unchanged? > Thecapitalize()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?