Skip to content

Error-based Blind SQLi

The query output isn’t reflected, the response body is identical for true and false conditions, and timing is unreliable - but the application returns a distinguishable error when the backend query fails. Force an error only when a condition is true, and the error itself becomes the boolean oracle.

-- Oracle: divide by zero only when the condition is TRUE → DB error
' AND (SELECT CASE WHEN (<condition>) THEN TO_CHAR(1/0) ELSE NULL END FROM dual) IS NULL-- -

If <condition> is true the query errors (HTTP 500, a custom error page, a stack trace); if false it returns normally. Everything else works like boolean blind - binary-search each character - except the signal is “errored vs. didn’t” instead of “content changed”.

The trailing IS NULL matters: it turns the scalar subquery into a valid boolean predicate for the AND. On the false branch the subquery returns NULL, NULL IS NULL is true, and the query runs normally; on the true branch the engine must evaluate 1/0 and raises. Without it, AND <string-or-null> is a type error that fails on both branches, giving you no oracle.

Symptom Class
Errors printed with query output UNION-based / error-based extraction
Response differs on true vs. false condition Boolean blind
Response only differs in timing Time-based blind
Response identical, but a condition-triggered error changes it This page
No feedback of any kind Out-of-band

This is the method PortSwigger’s “conditional errors” lab tests. Reach for it when boolean content differences don’t exist but you can still provoke a visible error state, and before falling back to slow time-based extraction.

Step 1 - Confirm the injection is interpreted as SQL

Section titled “Step 1 - Confirm the injection is interpreted as SQL”

A single quote may already throw an error. Prove the error is SQL (not generic input rejection) by submitting syntactically valid subqueries and watching the error appear and disappear:

' → error (unbalanced quote)
'' → no error
'||(SELECT '')||' → may still error depending on DBMS
'||(SELECT '' FROM dual)||' → no error on Oracle (needs a table)

Pair a true/false condition with an expression that is only evaluated (and only errors) when the condition holds. Division by zero is the classic trigger. Use 1=1 to prove the error fires and 1=2 to prove it stops before plugging in a real condition.

' AND (SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE NULL END FROM dual) IS NULL-- -

Swap 1=1 for 1=2 and the error must disappear. If it fires on both, the trigger is being evaluated independently of the condition - see the first failure mode below.

Replace the constant condition with a substring comparison, exactly as in boolean blind - keep the same conditional-error wrapper, only the condition changes:

' AND (SELECT CASE WHEN
(SUBSTR((SELECT password FROM users WHERE username='administrator'),1,1)='a')
THEN TO_CHAR(1/0) ELSE NULL END FROM dual) IS NULL-- -

Error present → the character matches. Binary-search with >/< on ASCII()/ORD() to cut requests per character from ~94 to ~7, same algorithm as the boolean page. The data-dependent condition also sidesteps the PostgreSQL constant-folding problem, since the planner can’t evaluate a per-row comparison ahead of time.

To sqlmap this is a boolean-blind oracle - the error state is just its “true” response signature - so use --technique=B, not E. (sqlmap’s E is error-based extraction, which scrapes data out of printed error-message text, a different technique covered under UNION-based.) Point it at a reliable true/false matcher:

Terminal window
sqlmap -u "https://<TARGET>/page" --cookie="TrackingId=xyz*" \
--technique=B --string="Internal Server Error" \
--dbms=oracle --dump -T users -C username,password

--string (or --code=500) tells sqlmap what the “true”/errored response looks like.

For manual control you can reuse the boolean-blind ffuf and bash loops, but changing the match filter alone is not enough: those payloads compare a substring directly and produce a content difference, never an error. Swap the injected payload for the conditional-error wrapper above (so the comparison sits inside the CASE), then match on the error - e.g. ffuf ... -mc 500, or -mr "error string" when the error page returns 200.

  • The error fires on both 1=1 and 1=2. The trigger is being evaluated independently of the condition - on PostgreSQL this is constant folding (see the Step 2 caution; make the divisor depend on the CASE). On other engines, make sure the erroring expression is only reachable through the true branch, not computed alongside it.
  • The app returns 200 with a generic error page. That is not necessarily a dead end: if the error page differs from the normal response at all - different text, different length - that difference is your oracle, and you’re really doing boolean blind with the error page as the “true” state. Only fall back to time-based when the errored and successful responses are genuinely indistinguishable.
  • WAF blocks 1/0 or CASE. See Bypasses; a subquery returning multiple rows ((SELECT table_name FROM information_schema.tables)) or a failing CAST are alternative error triggers.
  • Capture one request/response pair showing the errored vs. clean state - it’s clean evidence that the condition controlled the backend.
  • This technique leaks nothing in the response body, so it often slips past response-diffing detection while still being noisy in DB error logs. Note that asymmetry in the report.
  • Error oracle confirmed → automate with sqlmap --technique=B --string= / --code=500, or the boolean-blind scripts with the conditional-error payload matched on status code
  • Backend fingerprinted as Oracle via FROM dual → use the DBMS cheatsheet for Oracle-specific schema queries (all_tables, all_tab_columns)
  • No error oracle after all → Time-based blind, then Out-of-band if timing is also unreliable
  • Extracted an admin hash → crack offline, then pivot to MSSQL / WinRM / SSH on the same host
  • Hardening this class out of an app → SQLi defense