Server-Side Template Injection: Jinja2 Exploitation Walkthrough

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.

  1. Access url_for (a built-in function).
  2. Access its __globals__ to get a dictionary of global variables.
  3. Access the os module.
  4. 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 os module 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