Squ1rrel CTF 2025
emojicrypt
Passwords can be more secure. Weβre taking the first step.
Author: nisala
//app.py
from flask import Flask, request, redirect, url_for, g
import sqlite3
import bcrypt
import random
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__, static_folder='templates')
DATABASE = 'users.db'
EMOJIS = ['π', 'π', 'π', 'π', 'π±', 'π', 'π', 'π', 'π', 'π
', 'π', 'π', 'π', 'π΅', 'π', 'π', 'π', 'π', 'πΆ', 'π©', 'π']
NUMBERS = '0123456789'
database = None
def get_db():
global database
if database is None:
database = sqlite3.connect(DATABASE)
init_db()
return database
def generate_salt():
return 'aa'.join(random.choices(EMOJIS, k=12))
def init_db():
with app.app_context():
db = get_db()
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL
)''')
db.commit()
@app.route('/register', methods=['POST'])
def register():
email = request.form.get('email')
username = request.form.get('username')
if not email or not username:
return "Missing email or username", 400
salt = generate_salt()
random_password = ''.join(random.choice(NUMBERS) for _ in range(32))
password_hash = bcrypt.hashpw((salt + random_password).encode("utf-8"), bcrypt.gensalt()).decode('utf-8')
# TODO: email the password to the user. oopsies!
db = get_db()
cursor = db.cursor()
try:
cursor.execute("INSERT INTO users (email, username, password_hash, salt) VALUES (?, ?, ?, ?)", (email, username, password_hash, salt))
db.commit()
except sqlite3.IntegrityError as e:
print(e)
return "Email or username already exists", 400
return redirect(url_for('index', registered='true'))
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
return "Missing username or password", 400
db = get_db()
cursor = db.cursor()
cursor.execute("SELECT salt, password_hash FROM users WHERE username = ?", (username,))
data = cursor.fetchone()
if data is None:
return redirect(url_for('index', incorrect='true'))
salt, hash = data
if salt and hash and bcrypt.checkpw((salt + password).encode("utf-8"), hash.encode("utf-8")):
return os.environ.get("FLAG")
else:
return redirect(url_for('index', incorrect='true'))
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
if __name__ == '__main__':
app.run(port=8000)We are given 2 file where it happen to be a small website with /login and /register.
Once we register a username and email, the website will create a random password. However, this password is never shared with the user, making it impossible to log in.
The password was 32 random numbers from 0-9.
Vulnerability
The vulnerability occurs at generate_salt(),
This creates 12 emoji with 22 'aa'
However, the maximum length for a bycrpt password was 72. We can bruteforce 2 digits from 00-99
go getter
There's a joke to be made here about Python eating the GOpher. I'll cook on it and get back to you.
Author: kyle
These issues happen when Python and Golang are being used differently.
The application misses rebuilding its JSON structure from whatever was parsed before and forwards the request body to the service request. This is great because it means we can control whatever is sent to the backend service.
The json parser is instructed to bind the value referenced by the key action the the RequestData field Action. So what happens if we have two action fields?
Last updated