Log Me In
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_directory
from pathlib import Path
from hashlib import sha256
from utils import is_alphanumeric
from models import Account, db
from utils import decode, encode
flag = (Path(__file__).parent / "flag.txt").read_text()
pagebp = Blueprint('pagebp', __name__)
@pagebp.route('/')
def index():
return send_from_directory("static", 'index.html')
@pagebp.route('/login', methods=["GET", "POST"])
def login():
if request.method != 'POST':
return send_from_directory('static', 'login.html')
username = request.form.get('username')
password = sha256(request.form.get('password').strip().encode()).hexdigest()
if not username or not password:
return "Missing Login Field", 400
if not is_alphanumeric(username) or len(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()
if not 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'])
def register():
if request.method != 'POST':
return send_from_directory('static', 'register.html')
username = request.form.get('username')
password = sha256(request.form.get('password').strip().encode()).hexdigest()
displayname = request.form.get('displayname')
if not username or not password or not displayname:
return "Missing Registration Field", 400
if not is_alphanumeric(username) or len(username) > 50:
return "Username not Alphanumeric or it is longer than 50 chars", 403
if not is_alphanumeric(displayname) or len(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()
return jsonify({'message': 'Account created successfully'}), 201
except Exception as e:
db.session.rollback() # Roll back the session on error
return jsonify({'error': str(e)}), 500
@pagebp.route('/user')
def user():
cookie = request.cookies.get('info', None)
name='hello'
msg='world'
if cookie == None:
return render_template("user.html", display_name='Not Logged in!', special_message='Nah')
userinfo = decode(cookie)
if userinfo == None:
return render_template("user.html", display_name='Error...', special_message='Nah')
name = userinfo['displays']
msg = flag if userinfo['uid'] == 0 else "No special message at this time..."
return render_template("user.html", display_name=name, special_message=msg)
@pagebp.route('/logout')
def logout():
session.clear()
response = make_response(redirect('/'))
response.set_cookie('info', '', expires=0)
return responseSolution
To get the flag, we need to visit the /user endpoint with our UID set to zero.
msg = flag if userinfo['uid'] == 0 else "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
def is_alphanumeric(text):
pattern = r'^[a-zA-Z0-9]+$'
if re.match(pattern, text):
return True
else:
return False
def LOG(*args, **kwargs):
print(*args, **kwargs, flush=True)
# Some cryptographic utilities
def encode(status: dict) -> str:
try:
plaintext = json.dumps(status).encode()
out = b''
for i,j in zip(plaintext, os.environ['ENCRYPT_KEY'].encode()):
out += bytes([i^j])
return bytes.hex(out)
except Exception as s:
LOG(s)
return None
def decode(inp: str) -> dict:
try:
token = bytes.fromhex(inp)
out = ''
for i,j in zip(token, os.environ['ENCRYPT_KEY'].encode()):
out += chr(i ^ j)
user = json.loads(out)
return user
except Exception as s:
LOG(s)
return NoneThe 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.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB = 48674c3731025651282f614a4d5437132579332603202236367628351513723226782c30060a3939302a351b0e313339000f0b28190738107417743b0209702d535e551417281f1c2114361540494e6b36767573360e340e02122a25181b251370220a05280c0d0a083923112904280f3b247604247231760a25071523360c733330114a55604c0f02724d6e7027
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAA = 48674c3731025651282f614a4d543410267a30250023213535752b3616107131257b2f3305093a3a332936180d32303a030c082b1a043b1377147738010a732e535e551417281f1c2114361540494e6b35757670350d370d011129261b182610732109062b0f0e090b3a20122a072b0c38277507277132750926041620350f703033114a55604c0f02724d6e7027I used CyberChef to recover the key:
3E9DTp80EJCpmvvRd8rgBacww7itTR3sg9mqGKxxqktZOprxANJiXFyQ5V5zCH2oqru6sAllMuOfbsnIw742wOuOCSkdYZdR1cKDiMLKIxbPhEiNze7Ee3p7KdFTbwM2qr3fuB9ffPwN@ZLast step is to generate a new (signed) cookie with the UID set to zero:
48674c3731025651282f614a4f3737132579332603202236367628351513723226782c30060a3939302a351b0e313339000f0b28190738107417743b020a704d5d50115f0031000d34066d5c40322f0836767573360e340e02122a25181b251370220a05280c0d0a083923112904280f3b247604247231760a25071523350f105d50460f116003561bUpdate the cookie in the browser, then visit the /user endpoint and you will receive the flag!
Flag: csawctf{S3NS1T1V3_D4T4_ST0R3D_CL13NTS1D3D_B4D_B4D}
Last updated