Authorization disclaimer. This walkthrough applies only to the intentionally vulnerable PortSwigger Web Security Academy lab linked below. Test only systems you own or have explicit permission to assess.

Overview

This lab contains blind SQL injection in the TrackingId cookie. Successful and unsuccessful queries do not produce useful content differences, but a database exception causes a distinct HTTP error. By triggering that exception only when a tested proposition is true, the error becomes a one-bit oracle that can reveal the lab’s synthetic administrator password.

Lab objective/setup

The objective is to determine the password for the synthetic administrator account and sign in. The back end is Oracle, which matters because a standalone SELECT requires a table such as dual, and the injected expression must be compatible with the surrounding string column.

What I observed

I intercepted a normal request and changed only the TrackingId cookie. Appending one quote caused an internal error, while appending two quotes restored the normal response. That paired test is stronger evidence than an isolated error: the first value disrupts SQL syntax and the second balances it again.

I intercepted a normal request and changed only the TrackingId cookie. Appending one quote caused an internal error, while appending two quotes restored the normal response. That paired test is stronger evidence than an isolated error: the first value disrupts SQL syntax and the second balances it again. `UNION SELECT null` fails, whereas `UNION SELECT null FROM dual` succeeds and identifies Oracle's required dummy table.
I intercepted a normal request and changed only the TrackingId cookie. Appending one quote caused an internal error, while appending two quotes restored the normal response. That paired test is stronger evidence than an isolated error: the first value disrupts SQL syntax and the second balances it again. `UNION SELECT null` fails, whereas `UNION SELECT null FROM dual` succeeds and identifies Oracle's required dummy table.
I intercepted a normal request and changed only the `TrackingId` cookie. Appending one quote caused an internal error, while appending two quotes restored the normal response. That paired test is stronger evidence than an isolated error: the first value disrupts SQL syntax and the second balances it again.
I intercepted a normal request and changed only the `TrackingId` cookie. Appending one quote caused an internal error, while appending two quotes restored the normal response. That paired test is stronger evidence than an isolated error: the first value disrupts SQL syntax and the second balances it again.

Oracle-specific probes then established usable syntax. A UNION SELECT null form failed, whereas the equivalent selection FROM dual succeeded.

Repeater sends `UNION SELECT null FROM dual` in `TrackingId`; the normal response confirms Oracle accepts the dummy-table form that failed without `FROM dual`.
Repeater sends `UNION SELECT null FROM dual` in `TrackingId`; the normal response confirms Oracle accepts the dummy-table form that failed without `FROM dual`.
Oracle-specific probes then established usable syntax. A `UNION SELECT null` form failed, whereas the equivalent selection `FROM dual` succeeded.
Oracle-specific probes then established usable syntax. A `UNION SELECT null` form failed, whereas the equivalent selection `FROM dual` succeeded.

Why it is vulnerable

The useful lab-only payload pattern is:

1
2
3
4
' UNION SELECT CASE WHEN (<condition>)
THEN TO_CHAR(1/0)
ELSE ''
END FROM dual --

When <condition> is true, Oracle evaluates 1/0, raises ZERO_DIVIDE, and the application returns its error response. When it is false, the empty string branch is selected and the request completes normally. TO_CHAR makes the error-producing branch type-compatible with the selected text value.

Repeater sends a TrackingId UNION CASE probe whose true branch evaluates TO_CHAR(1/0) FROM dual; the response shows the Oracle-triggered server error.
Repeater sends a TrackingId UNION CASE probe whose true branch evaluates TO_CHAR(1/0) FROM dual; the response shows the Oracle-triggered server error.
The TrackingId false-condition CASE selects an empty string FROM dual; the Academy shop home page loads normally with no Oracle ZERO_DIVIDE error.
The TrackingId false-condition CASE selects an empty string FROM dual; the Academy shop home page loads normally with no Oracle ZERO_DIVIDE error.

Walkthrough in the authorized PortSwigger lab

With the oracle validated, I tested candidate lengths against the single synthetic administrator row. In Burp Intruder, the candidate integer occupied one payload position in a condition equivalent to LENGTH(password)=N. The response associated with N=20 entered the error state, establishing a 20-character password for this lab instance.

Intruder marks the numeric payload position inside LENGTH(password)=N for the administrator row in the users table.
Intruder marks the numeric payload position inside LENGTH(password)=N for the administrator row in the users table.
Burp Intruder results identify N=20 as the sole TrackingId length candidate producing the Oracle error, so the administrator password contains 20 characters.
Burp Intruder results identify N=20 as the sole TrackingId length candidate producing the Oracle error, so the administrator password contains 20 characters.

Recovering each synthetic character

For each position from 1 through 20, I tested candidate characters using an Oracle expression equivalent to SUBSTR(password, position, 1)='candidate'. Each position had exactly one candidate that triggered the error response. Sorting or filtering Intruder results by status and length made those matches straightforward to identify.

The character-extraction request places payload positions in SUBSTR(password,position,1)=candidate within the Oracle CASE expression.
The character-extraction request places payload positions in SUBSTR(password,position,1)=candidate within the Oracle CASE expression.
Filtered Intruder results show one error-producing character candidate for each password position, yielding the synthetic administrator credential.
Filtered Intruder results show one error-producing character candidate for each password position, yielding the synthetic administrator credential.

The recovered value is ephemeral lab data and is intentionally not reproduced as a reusable credential. Entering the extracted value for administrator completed the exercise.

The recovered value is ephemeral lab data and is intentionally not reproduced as a reusable credential. Entering the extracted value for `administrator` completed the exercise.
The recovered value is ephemeral lab data and is intentionally not reproduced as a reusable credential. Entering the extracted value for `administrator` completed the exercise.

Verification

Three controls support the conclusion: quote balancing changes error behavior predictably; true and false predicates reliably select different HTTP outcomes; and the 20 extracted character tests produce a credential accepted by the lab. Together, these observations distinguish a conditional SQL error channel from random server instability.

Impact and defensive lesson

Suppressing query output does not make SQL injection harmless. Status codes, timing, connection resets, and custom error pages can all leak boolean results. Production applications should use parameterized queries, avoid concatenating cookie values into SQL, return uniform external errors, and monitor repeated near-identical requests. Uniform errors reduce leakage but are only defense in depth; eliminating the injection sink is the primary fix.

Provenance

This article is a publication-quality English rewrite of the author’s notes for the reserved PortSwigger migration item the internal migration record. Figures were retained from that lab session and stored locally under the manifest-reserved image route.

References