Introduction

Cette année, j’ai été heureux de faire partie du comité d’organisation de la conférence GreHack et j’ai pu créér quelques challenges pour le CTF. Merci à tous les participants et organisateurs, l’événement était une fois de plus génial 🔥 💚 Un grand merci à Elweth qui a fait la plus grande partie du challenge .

Challenge

Beer me up before you go-go, Leave a pint for me, don’t be a no-no. Beer me up before you go-go, I don’t want to miss out when you pour that froth

Ce challenge est accompagné de l’archive Beer_Me_Up_Before_You_Format.zip. Il s’agit du code source de l’application active sur https://beer-me-up-before-you-format.ctf.grehack.fr/.
Cette archive contient l’arborescence suivante :

.
├── app
│   ├── api
│   │   ├── queries.py
│   │   └── Secret.py
│   ├── app.py
│   ├── db
│   │   └── database.db
│   └── templates
│       ├── index.html
│       └── secret.html
├── docker-compose.yml
├── Dockerfile
├── flag.txt
└── requirements.txt

Il s’agit d’une application python, utilisant le Flask Framework pour démarrer un serveur web.
Une analyse du code source révèle les endpoints suivants :

@app.route("/")

@app.route("/api/<endpoint>/<id_user>")

@app.route("/api/password-reset", methods=["POST"])

@app.route("/api/login", methods=["POST"])

@app.route("/api/admin", methods=["POST"])

@app.route(f"/api/{SECRET_ENDPOINT}", methods=["POST"])

Le fichier api/queries.py contient les requêtes SQL faites à la base de données SQLite, qui semblent être sécurisées en utilisant des requêtes préparées.

Dans le fichier api/secret.py, on note la présence de la classe “Secret”, ainsi qu’une variable globale SECRET_ENDPOINT contenant la route que nous allons trouver.

import os

SECRET_ENDPOINT = "secret"

class Secret:
	def __init__(self, secret):
		self.secret = secret

	def __repr__(self):
		return f"The secret endpoint is : /{self.secret} !"

403 Bypass

Le premier endpoint est accessible est :

# To manage multi-endpoints
@app.route("/api/<endpoint>/<id_user>")
def parse(endpoint, id_user):
    if endpoint.lower() in ENDPOINTS:
        if endpoint == "users":
            return jsonify(error="This endpoint is for admins only."), 403
        return jsonify(get_user(int(id_user)))
    else:
        return jsonify(error="This page does not exists."), 404

La fonction analyse la valeur de endpoint afin d’afficher les informations sur l’utilisateur liées à user_id également passé en paramètre. Cependant, nous ne sommes pas autorisés à accéder à l’URI /api/users/<id>, car la fonction vérifie si l’endpoint est défini sur “users”, ce qui semble être réservé aux admins, et renvoie une erreur 403.

Cependant, l’application ne prend pas en compte la partie “sensible à la casse” des endpoints, et il est possible de contourner la condition suivante en utilisant des lettres majuscules.

if endpoint == "users":
    return jsonify(error="This endpoint is for admins only."), 403
return jsonify(get_user(int(id_user)))

Ce qui donne : /api/uSers/1 :

Insecure Direct Object Reference

Maintenant que nous avons accès aux utilisateurs de l’endpoint, nous pouvons lister tous les utilisateurs de l’application. L’objectif est de cibler en particulier les comptes administrateurs, caractérisés par le rôle ADMIN dans la réponse JSON.

Pour ce faire, nous pouvons utiliser l’intruder de Burp pour incrémenter la partie user_id, tout en faisant correspondre ADMIN dans la réponse du serveur.

Une fois l’intruder terminée, nous récupérons la liste des utilisateurs ayant le rôle ADMIN.

Un autre résultat intéressant dans la réponse de l’api est la partie token, qui semble être un UUID.

Password Reset abuse

Ce token retourné est loin d’être inintéressant, car il est utilisé pour réinitialiser le mot de passe.

L’endpoint /api/password-reset attend 2 paramètres : token et password. En visitant la fonction update_password, on remarque qu’il est possible de mettre à jour le mot de passe d’un compte, en supposant que l’on connaisse le token.

@app.route("/api/password-reset", methods=["POST"])
def password_reset():
    json = request.get_json()
    try:
        token = json["token"]
        password = json["password"]
        if update_password(token, password):
            return jsonify(success="The password has been reset.")
        else:
            return jsonify(error="An error has occured.")
    except Exception as e:
        print(e)
        return jsonify(error="Parameter 'token' or 'password' are missing.")

De cette manière, nous pouvons forger la requête suivante pour réinitialiser le mot de passe d’un administrateur :

Une fois le mot de passe mis à jour, il est possible de se connecter au compte compromis et d’obtenir ainsi un token de session valide :

Python Format String

Pour accéder à l’interface d’administration, il faut s’authentifier en fournissant le token précédemment récupéré dans l’en-tête X-Api-Key. Tout d’abord, l’intégrité du token est vérifiée, et la partie “ROLE” doit être “ADMIN”. Ensuite, l’API récupère la variable secret contenue dans le JSON envoyé.

Si nous examinons de plus près la manière dont le contenu transmis est rendu dans le template, nous remarquons qu’une format string est utilisée :

return f"The secret endpoint is : /{self.secret} !"

L’utilisation de cette syntaxe dans la format string est une vulnérabilité, car elle permet d’accéder aux propriétés de l’objet directement dans la format string.

Nous pouvons confirmer la vulnérabilité en envoyant {secret} pour appeler l’objet lui-même.

Ici, nous pouvons voir que la méthode repr (équivalente à toString) est appelée 2 fois, puisque nous avons 2 fois la sortie.

Creusons un peu plus loin en utilisant un debugger python. Pour ce faire, nous allons utiliser les bibliothèques ipdb et rich. Avec ces bibliothèques, nous allons placer un point d’arrêt dans le code, afin de pouvoir inspecter la variable secret et voir ce que nous pouvons en apprendre. Ajoutons les lignes suivantes au code de app.py :

import ipdb, rich

secret = request.get_json()["secret"]
secret = Secret(secret)
ipdb.set_trace()

Lorsque que l’on redémarre l’application, le processus se met en pause et nous propose un prompt.

Nous avons accès à une console de type python, qui nous permet d’effectuer des actions sur le programme. Par exemple, nous pouvons afficher le contenu de la variable secret :

En utilisant la bibliothèque rich, nous pouvons lister toutes les méthodes auxquelles l’objet secret a accès.

Il est donc possible d’appeler toutes ces méthodes, y compris la méthode init pour construire l’objet Secret.

On se rappelle que notre précieux flag est stocké dans les variables globales de l’objet Secret,

Et avec Burp Suite :

File Read

Maintenant que nous avons récupéré l’endpoint secret, nous pouvons y accéder avec la nouvelle fonctionnalité utilisée pour lire les fichiers du système.

filename = urllib.parse.unquote(request.get_json()['filename'])
data = "This file doesn't exist"
bad_chars = ["../", "\\", "."]
is_safe = all(char not in filename for char in bad_chars)

if is_safe:
    filename = urllib.parse.unquote(filename)
    if os.path.isfile('./'+ filename):
        with open(filename) as f:
            data = f.read()
return jsonify(data)

Dans le fichier Dockerfile, nous pouvons voir que le flag est situé à /flag.txt.

COPY flag.txt /flag.txt

Mais avec le filtre, nous ne pouvons pas le lire directement :

Le filtre utilise urllib.parse.unquote() deux fois, donc nous pouvons url-encoder deux fois (n’oubliez pas le point), contourner ce filtre et faire fuiter le flag :

  • Flag : GH{F0rm4t_Str1ng_t0_D4T4_L34K}

Ressources