Writeup for Lost Pyramid (Web) - CSAW CTF (2024) 💜
Description
A massive sandstorm revealed this pyramid that has been lost (J)ust over 3300 years.. I'm interested in (W)here the (T)reasure could be?
Source Code
from flask import Flask, request, render_template, jsonify, make_response, redirect, url_for, render_template_stringimport jwtimport datetimeimport osapp =Flask(__name__)# Load keyswithopen('private_key.pem', 'rb')as f: PRIVATE_KEY = f.read()withopen('public_key.pub', 'rb')as f: PUBLICKEY = f.read()KINGSDAY = os.getenv("KINGSDAY", "TEST_TEST")current_date = datetime.datetime.now()current_date = current_date.strftime("%d_%m_%Y")@app.route('/entrance', methods=['GET'])defentrance(): payload ={"ROLE":"commoner","CURRENT_DATE":f"{current_date}_AD","exp": datetime.datetime.now(datetime.timezone.utc)+ datetime.timedelta(days=(365*3000))} token = jwt.encode(payload, PRIVATE_KEY, algorithm="EdDSA") response =make_response(render_template('pyramid.html')) response.set_cookie('pyramid', token)return response@app.route('/hallway', methods=['GET'])defhallway():returnrender_template('hallway.html')@app.route('/scarab_room', methods=['GET', 'POST'])defscarab_room():try:if request.method =='POST': name = request.form.get('name')if name: kings_safelist = ['{','}','𓁹','𓆣','𓀀','𓀁','𓀂','𓀃','𓀄','𓀅','𓀆','𓀇','𓀈','𓀉','𓀊', '𓀐', '𓀑', '𓀒', '𓀓', '𓀔', '𓀕', '𓀖', '𓀗', '𓀘', '𓀙', '𓀚', '𓀛', '𓀜', '𓀝', '𓀞', '𓀟',
'𓀠', '𓀡', '𓀢', '𓀣', '𓀤', '𓀥', '𓀦', '𓀧', '𓀨', '𓀩', '𓀪', '𓀫', '𓀬', '𓀭', '𓀮', '𓀯',
'𓀰','𓀱','𓀲','𓀳','𓀴','𓀵','𓀶','𓀷','𓀸','𓀹','𓀺','𓀻'] name =''.join([char for char in name if char.isalnum() or char in kings_safelist])returnrender_template_string(''' <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Lost Pyramid</title> <style> body { margin: 0; height: 100vh; background-image: url('{{ url_for('static', filename='scarab_room.webp') }}'); background-size: cover; background-position: center; background-repeat: no-repeat; font-family: Arial, sans-serif; color: white; position: relative; } .return-link { position: absolute; top: 10px; right: 10px; font-family: 'Noto Sans Egyptian Hieroglyphs', sans-serif; font-size: 32px; color: gold; text-decoration: none; border: 2px solid gold; padding: 5px 10px; border-radius: 5px; background-color: rgba(0, 0, 0, 0.7); } .return-link:hover { background-color: rgba(0, 0, 0, 0.9); } h1 { color: gold; } </style> </head> <body> <a href="{{ url_for('hallway') }}" class="return-link">RETURN</a><div data-gb-custom-block data-tag="if"> <h1>𓁹𓁹𓁹 Welcome to the Scarab Room, '''+ name +''' 𓁹𓁹𓁹</h1></div> </body> </html> ''', name=name, **globals())exceptExceptionas e:passreturnrender_template('scarab_room.html')@app.route('/osiris_hall', methods=['GET'])defosiris_hall():returnrender_template('osiris_hall.html')@app.route('/anubis_chamber', methods=['GET'])defanubis_chamber():returnrender_template('anubis_chamber.html')@app.route('/')defhome():returnredirect(url_for('entrance'))@app.route('/kings_lair', methods=['GET'])defkings_lair(): token = request.cookies.get('pyramid')ifnot token:returnjsonify({"error": "Token is required"}),400try: decoded = jwt.decode(token, PUBLICKEY, algorithms=jwt.algorithms.get_default_algorithms())if decoded.get("CURRENT_DATE")== KINGSDAY and decoded.get("ROLE")=="royalty":returnrender_template('kings_lair.html')else:returnjsonify({"error": "Access Denied: King said he does not way to see you today."}),403except jwt.ExpiredSignatureError:returnjsonify({"error": "Access has expired"}),401except jwt.InvalidTokenError as e:print(e)returnjsonify({"error": "Invalid Access"}),401if__name__=='__main__': app.run(host ='0.0.0.0', port =8050)
Solution
Looking at the code, we need to ensure the CURRENT_DATE claim in the JWT is set to KINGSDAYand the ROLE is set to royalty.
Note that KINGSDAY is set as environment variable so even if we could easily tamper with our JWT, we don't know what it is the correct date on the server-side.
jwt_tool eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJST0xFIjoiY29tbW9uZXIiLCJDVVJSRU5UX0RBVEUiOiIwN18wOV8yMDI0X0FEIiwiZXhwIjo5NjMzMzcxNDI5OX0.53yJHr1ZxEYzRIrX2GEDao3kTbAY-W3y-9vOHZvRCmYtD49ty-EIo7KyjpwPEEmz-FxxUq2rynETCKiW_6ZIBQ -I -pc ROLE -pv royalty -pc CURRENT_DATE -pv 03_07_1341_BC -X k -pk pubkey
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJST0xFIjoicm95YWx0eSIsIkNVUlJFTlRfREFURSI6IjAzXzA3XzEzNDFfQkMiLCJleHAiOjk2MzMzNzE0Mjk5fQ.c5OwTPXb7qLz-R0mAhBj03jauzQEcnRBcor9KVyW8Q8
It doesn't work! I tried various formats for the public key, but all failed. I assumed I must need the PRIVATE_KEY but couldn't use _ due to the filter list. I was confident there was some unicode issue; otherwise, why include all those symbols in the filter list?
After the CTF, I found out that key confusion was the correct approach (signing with the public key and switching to symmetric); I must have just had the wrong format for the public key. Overall, it's still a pretty cool challenge, but I think the public key should have been in a usable format, and the filter list was an unneeded distraction.