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
from flask import Flaskfrom flask_sqlalchemy import SQLAlchemyimport osfrom models import init_dbfrom 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_urlapp.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)
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, 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
import refrom Crypto.Util.Padding import pad, unpadimport jsonimport 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 utilitiesdef 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
.
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
@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.
@pagebp.route('/login', methods=["GET", "POST"])def login(): # ... response.set_cookie('info', token, max_age=3600, httponly=True) return response
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).
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.