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_string
import jwt
import datetime
import os
app = Flask(__name__)
# Load keys
with open('private_key.pem', 'rb') as f:
PRIVATE_KEY = f.read()
with open('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'])
def entrance():
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'])
def hallway():
return render_template('hallway.html')
@app.route('/scarab_room', methods=['GET', 'POST'])
def scarab_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])
return render_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())
except Exception as e:
pass
return render_template('scarab_room.html')
@app.route('/osiris_hall', methods=['GET'])
def osiris_hall():
return render_template('osiris_hall.html')
@app.route('/anubis_chamber', methods=['GET'])
def anubis_chamber():
return render_template('anubis_chamber.html')
@app.route('/')
def home():
return redirect(url_for('entrance'))
@app.route('/kings_lair', methods=['GET'])
def kings_lair():
token = request.cookies.get('pyramid')
if not token:
return jsonify({"error": "Token is required"}), 400
try:
decoded = jwt.decode(token, PUBLICKEY, algorithms=jwt.algorithms.get_default_algorithms())
if decoded.get("CURRENT_DATE") == KINGSDAY and decoded.get("ROLE") == "royalty":
return render_template('kings_lair.html')
else:
return jsonify({"error": "Access Denied: King said he does not way to see you today."}), 403
except jwt.ExpiredSignatureError:
return jsonify({"error": "Access has expired"}), 401
except jwt.InvalidTokenError as e:
print(e)
return jsonify({"error": "Invalid Access"}), 401
if __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.