Server-Side Template Injection: Jinja2 Exploitation Walkthrough
Challenge Info:
- Platform: picoCTF 2025 - SSTI1
- Category: Web Exploitation
- Difficulty: Easy
- Technique: Server-Side Template Injection (SSTI)
Understanding the Challenge
We are presented with a simple web application that allows users to "announce" a message. The text we input is rendered on the screen. The name of the challenge ("SSTI1") and the hint strongly suggest the vulnerability is Server-Side Template Injection.
Vulnerability Detection
To confirm if the server is interpreting our input as code rather than just text, we perform a simple math test. In Jinja2 (the template engine likely used here since it's a Python/Flask app), we use double curly braces {{ }} to execute expressions.
Input:
{{ 7 * 7 }}
Result:
The server returns 49. This confirms the application evaluates the code inside the braces. We have a valid injection point.

Figure 1: Injecting a math expression results in the server calculating the answer, confirming SSTI.
Gaining Remote Code Execution
Now that we can execute code, we need to access the underlying operating system to read files. In Python web apps, we often try to access the os module.
Attack Strategy
A common technique is to "climb" the object hierarchy. We can use standard Flask functions like url_for to access the global scope.
- Access
url_for(a built-in function). - Access its
__globals__to get a dictionary of global variables. - Access the
osmodule. - Run a shell command using
popen().
File System Enumeration
Before we can read the flag, we need to know its filename. A common mistake is assuming it is named flag.txt. We should run the ls command first.
Payload:
{{ url_for.__globals__.os.popen('ls').read() }}
Result:
The server returns the file list: __pycache__, app.py, flag, and requirements.txt.
Crucially, notice the file is named just flag, not flag.txt.

Figure 2: Listing the current directory reveals the file named 'flag'.
Capturing the Flag
Now that we have the correct filename, we modify our payload to read the content of the flag file using the cat command.
Final Payload:
{{ url_for.__globals__.os.popen('cat flag').read() }}
Result: The server executes the command and prints the flag on the screen.
Flag: picoCTF{s4rv3r_s1d3_t3mp14t3_1nj3ct10n5_4r3_c001_bcf73b04}

Figure 3: Reading the content of the flag file.
Technical Breakdown
Why this works: This vulnerability occurs because the application likely uses a line of code similar to:
return render_template_string("Hello " + user_input)
Instead of passing the user input as a safe variable (data), the developer concatenated it directly into the template string. This allows the template engine to parse the input as actual Python code.
Key Takeaways
SSTI Detection
- Test with
{{ 7 * 7 }}to confirm template injection - Double curly braces execute Jinja2 expressions
- Math expression evaluation confirms code execution
Exploitation Techniques
- Use
url_for.__globals__to access Python globals - Access
osmodule for system command execution popen().read()executes shell commands and returns output- Always enumerate before assuming filenames
Prevention
- Never concatenate user input into template strings
- Use
render_template()with separate variables - Implement input validation and sanitization