Greetings π
A Python/Jinja2 SSTI challenge using SandboxedEnvironment and CVE-2025-27516.
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:
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
- Confirmed SSTI with
{{ 6*7 }}, which rendered42. - Noticed the handout pins Jinja2 to
3.1.5. - Found CVE-2025-27516, a
SandboxedEnvironmentbreakout using|attr("format"). - Used Python format-field traversal to reach
sys.modules[os].environ. - Read the
FLAGenvironment variable.