Description
A note-taking app stores notes as files. Path traversal via backslash + SSTI leads to RCE.
Setup
Open the challenge URL and try creating a note to understand how the app works.
Solution
- Step 1Understand the filename behaviorWhen you create a note, the first 128 characters of your note content become the filename under a notes/ directory. The app filters forward slashes / but not backslashes \.
Learn more
Path traversal (also known as directory traversal) is a vulnerability where user-controlled input is used to construct file system paths without sufficient sanitization. The classic attack sequence
../(dot-dot-slash) navigates up one directory level -- repeated traversal sequences escape the intended directory entirely.This application attempted to block path traversal by filtering forward slashes (
/). However, on Unix-like systems, only/is the path separator -- backslashes (\) in filenames are treated as literal characters. The vulnerability arises because Werkzeug, the Python WSGI library underlying Flask, normalizes URLs and paths, converting\to/before the application processes the result.This filter bypass illustrates a key security principle: input validation must happen on the canonical form of the data, not the raw form. Normalizing (canonicalizing) input before applying filters prevents bypasses via alternate representations. OWASP CWE-22 (Path Traversal) lists dozens of encoding and representation tricks used to bypass path filters.
- Step 2Exploit backslash path traversalPython's Werkzeug url_fix() normalizes \ to / -- so the app's slash filter runs before normalization. Use \..\templates\errors\pwn as the beginning of your note to write a file at templates/errors/pwn, which the app serves as a Jinja2 template.curl -X POST <url>/new -d 'content=\..\templates\errors\pwn{{config.__class__.__init__.__globals__["os"].popen("cat flag.txt").read()}}'
Learn more
Server-Side Template Injection (SSTI) occurs when user-controlled data is embedded directly into a template that is then rendered by a template engine. Jinja2, Flask's default template engine, evaluates expressions inside
{{ }}delimiters as Python code. If an attacker can inject content into a template file, they can execute arbitrary Python.The payload
{{config.__class__.__init__.__globals__["os"].popen("cat flag.txt").read()}}exploits Jinja2's access to Python's object model.configis a Flask context variable;.__class__.__init__.__globals__navigates Python's internal attribute chain to reach the global namespace of the__init__method, which includes theosmodule.os.popen(cmd).read()executes a shell command and returns the output.This two-stage exploit chain (path traversal to write a file + SSTI to execute code) is a powerful combination in web security research. The path traversal places the malicious template in a directory the app serves; the SSTI activates when the app renders that template. Defenses include: never using user input in template rendering, storing uploads outside the web root, and using sandboxed template environments with restricted attribute access.
- Step 3Trigger the SSTIRequest the error page that loads your injected template. The Jinja2 engine evaluates the expression and returns the flag in the response.curl '<url>/?error=pwn'
Learn more
The application loads error templates by name based on the
errorquery parameter:?error=pwncauses it to rendertemplates/errors/pwn-- the file you wrote via path traversal. When Jinja2 renders this file, it evaluates the injected template expression, executes the OS command, and returns the output in the HTTP response body.This request-response flow is the "trigger" step that distinguishes two-stage exploits from direct injection. Stage 1 (the path traversal) plants the payload. Stage 2 (this request) activates it. The gap between the two stages is an opportunity to prepare additional payloads -- for example, first read
/etc/passwdto confirm RCE, then read the flag, then establish a reverse shell.In real-world bug bounty hunting, SSTI is a critical severity finding because it grants full Remote Code Execution on the web server. It is consistently found in Flask, Django (with unsafe template construction), Ruby's ERB, Java's FreeMarker/Thymeleaf, and PHP's Twig/Smarty. The detection payload
{{7*7}}is safe and non-destructive -- if the response contains "49," the injection point is confirmed.
Flag
picoCTF{...}
Path traversal combined with SSTI is a powerful chain -- writing a Jinja2 template to a location the app serves lets you execute arbitrary Python expressions server-side.