Skip to content

Log Me In

Challenge

I (definitely did not) have found this challenge in the OSIRIS recruit repository

— nikobelic29

  • Directorystatic/
    • index.html
    • login.html
    • register.html
  • Directorytemplates/
    • user.html
  • Dockerfile
  • app.py
  • db_migration.py
  • flag.txt !!! the goal
  • models.py
  • requirements.txt
  • routes.py
  • run.sh
  • utils.py
app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from models import init_db
from routes import pagebp
app = Flask(__name__,static_folder=".")
#init db
db_host = os.environ['DB_HOST']
db_username = os.environ['DB_USERNAME']
db_pass = os.environ['DB_PASSWORD']
db_port = os.environ['DB_PORT']
db_name = os.environ['DB_NAME']
db_url = f'postgresql://{db_username}:{db_pass}@{db_host}:{db_port}/{db_name}'
app.config["SQLALCHEMY_DATABASE_URI"] = db_url
app.secret_key = os.environ['FLASK_SECRET']
db = SQLAlchemy()
init_db(app)
app.register_blueprint(pagebp)
if __name__ == '__main__':
app.run(host="0.0.0.0", threaded=True, port=1111)
routes.py
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 response
utils.py
import re
from Crypto.Util.Padding import pad, unpad
import json
import os
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 None

Solution

We can see the flag being loaded into the flag variable and rendered in the /user route if our user has a uid of 0.

routes.py
flag = (Path(__file__).parent / "flag.txt").read_text()
#...
@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)

Well, how can we set our uid to 0?

When we register, we get a uid of 1 by default

routes.py
@pagebp.route('/register', methods=['GET', 'POST'])
def register():
# ...
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

When we login, the “info” cookie gets set, which is encoded, and then gets decoded when we visit the /user page.

routes.py
@pagebp.route('/login', methods=["GET", "POST"])
def login():
# ...
response.set_cookie('info', token, max_age=3600, httponly=True)
return response
utils.py
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

The encode function is using the XOR cipher, which is vulnrable to key leaking.

If we know the plaintext (original json) and the ciphertext (cookie), we can extract the key by XORing the plaintext and ciphertext (see link above for an example).

pwn.py
import json
def derive_secret_key(user: dict, cookie: str) -> str:
plaintext = json.dumps(user).encode()
encoded_bytes = bytes.fromhex(cookie)
secret_key = b''.join([bytes([p ^ e]) for p, e in zip(plaintext, encoded_bytes)])
return secret_key.decode()
user = {"username": "12341234", "displays": "12341234", "uid": 1}
cookie = "48674c3731025651282f614a4d544760570c43557155415b57150d1d27225f121e4a4f4b6769494a425f45687c445054616c3f003c644371042b"
secret_key = derive_secret_key(user, cookie)
print(f"Derived secret key: {secret_key}")
def encode(status: dict, key: str) -> str:
plaintext = json.dumps(status).encode()
out = b''
for i,j in zip(plaintext, key.encode()):
out += bytes([i^j])
return bytes.hex(out)
print(encode(user, secret_key))
print(cookie)
admin = {"username": "12341234", "displays": "12341234", "uid": 0}
print(encode(admin, secret_key))

Then, we can just replace our cookie in the devtools and visit the user page.