Writeup for Log Me In (Web) - CSAW CTF (2024) 💜
Description
I (definitely did not) have found this challenge in the OSIRIS recruit repository
Source Code
from flask import make_response, session, Blueprint, request, jsonify, render_template, redirect, send_from_directoryfrom pathlib import Pathfrom hashlib import sha256from utils import is_alphanumericfrom models import Account, dbfrom utils import decode, encodeflag = (Path(__file__).parent /"flag.txt").read_text()pagebp =Blueprint('pagebp', __name__)@pagebp.route('/')defindex():returnsend_from_directory("static", 'index.html')@pagebp.route('/login', methods=["GET", "POST"])deflogin():if request.method !='POST':returnsend_from_directory('static', 'login.html') username = request.form.get('username') password =sha256(request.form.get('password').strip().encode()).hexdigest()ifnot username ornot password:return"Missing Login Field",400ifnotis_alphanumeric(username)orlen(username)>50:return"Username not Alphanumeric or longer than 50 chars",403# check if the username already exists in the DB user = Account.query.filter_by(username=username).first()ifnot user or user.password != password:return"Login failed!",403 user ={'username':user.username,'displays':user.displayname,'uid':user.uid} token =encode(dict(user))if token ==None:return"Error while logging in!",500 response =make_response(jsonify({'message': 'Login successful'})) response.set_cookie('info', token, max_age=3600, httponly=True)return response@pagebp.route('/register', methods=['GET', 'POST'])defregister():if request.method !='POST':returnsend_from_directory('static', 'register.html') username = request.form.get('username') password =sha256(request.form.get('password').strip().encode()).hexdigest() displayname = request.form.get('displayname')ifnot username ornot password ornot displayname:return"Missing Registration Field",400ifnotis_alphanumeric(username)orlen(username)>50:return"Username not Alphanumeric or it is longer than 50 chars",403ifnotis_alphanumeric(displayname)orlen(displayname)>50:return"Displayname not Alphanumeric or it is longer than 50 chars",403# check if the username already exists in the DB user = Account.query.filter_by(username=username).first()if user:return"Username already taken!",403 acc =Account( username=username, password=password, displayname=displayname, uid=1 )try:# Add the new account to the session and commit it db.session.add(acc) db.session.commit()returnjsonify({'message': 'Account created successfully'}),201exceptExceptionas e: db.session.rollback()# Roll back the session on errorreturnjsonify({'error': str(e)}),500@pagebp.route('/user')defuser(): cookie = request.cookies.get('info', None) name='hello' msg='world'if cookie ==None:returnrender_template("user.html", display_name='Not Logged in!', special_message='Nah') userinfo =decode(cookie)if userinfo ==None:returnrender_template("user.html", display_name='Error...', special_message='Nah') name = userinfo['displays'] msg = flag if userinfo['uid']==0else"No special message at this time..."returnrender_template("user.html", display_name=name, special_message=msg)@pagebp.route('/logout')deflogout(): session.clear() response =make_response(redirect('/')) response.set_cookie('info', '', expires=0)return response
Solution
To get the flag, we need to visit the /user endpoint with our UID set to zero.
msg = flag if userinfo['uid']==0else"No special message at this time..."
We can register an account and log in; notice how the UID is set?
user ={'username':user.username,'displays':user.displayname,'uid':user.uid}token =encode(dict(user))
It uses a custom encode function, imported from utils.py
defis_alphanumeric(text): pattern =r'^[a-zA-Z0-9]+$'if re.match(pattern, text):returnTrueelse:returnFalsedefLOG(*args,**kwargs):print(*args, **kwargs, flush=True)# Some cryptographic utilitiesdefencode(status:dict) ->str:try: plaintext = json.dumps(status).encode() out =b''for i,j inzip(plaintext, os.environ['ENCRYPT_KEY'].encode()): out +=bytes([i^j])returnbytes.hex(out)exceptExceptionas s:LOG(s)returnNonedefdecode(inp:str) ->dict:try: token =bytes.fromhex(inp) out =''for i,j inzip(token, os.environ['ENCRYPT_KEY'].encode()): out +=chr(i ^ j) user = json.loads(out)return userexceptExceptionas s:LOG(s)returnNone
The JSON object is XORd with the key. Sounds like an OTP issue? If we encode two different user objects (c1 and c2) and then XOR them together, we should recover the key!
My first attempt at this failed; the username/display name probably needed to be longer (hinted at by the 50-char length limits). The problem was finding 50 char names that hadn't already been taken lol.