diff --git a/config.json.example b/config.json.example index 43770ef..7b9bffc 100644 --- a/config.json.example +++ b/config.json.example @@ -16,5 +16,7 @@ "pipalPath": "/path/to/pipal", "pipal_count" : 10, "bandrelmaxruntime": 300, - "bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19" + "bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19", + "hashview_url": "http://localhost:8443", + "hashview_api_key": "" } \ No newline at end of file diff --git a/hashcat-utils/bin/combinator.arm.bin b/hashcat-utils/bin/combinator.arm.bin new file mode 100755 index 0000000..e0bfe36 Binary files /dev/null and b/hashcat-utils/bin/combinator.arm.bin differ diff --git a/hashcat-utils/bin/expander.arm.bin b/hashcat-utils/bin/expander.arm.bin new file mode 100755 index 0000000..ffb55c8 Binary files /dev/null and b/hashcat-utils/bin/expander.arm.bin differ diff --git a/hate_crack.py b/hate_crack.py index 2301229..7b247a6 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -1,8 +1,10 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Methodology provided by Martin Bos (pure_hate) - https://www.trustedsec.com/team/martin-bos/ # Original script created by Larry Spohn (spoonman) - https://www.trustedsec.com/team/larry-spohn/ # Python refactoring and general fixing, Justin Bollinger (bandrel) - https://www.trustedsec.com/team/justin-bollinger/ +# Hashview integration by Justin Bollinger (bandrel) and Claude Sonnet 4.5 +# special thanks to hans for all his hard work on hashview and creating APIs for us to use import subprocess import sys @@ -13,10 +15,17 @@ import json import binascii import shutil +try: + import requests + REQUESTS_AVAILABLE = True +except ImportError: + REQUESTS_AVAILABLE = False + # python2/3 compatability try: input = raw_input except NameError: + pass hate_path = os.path.dirname(os.path.realpath(__file__)) @@ -108,6 +117,18 @@ except KeyError as e: print('{0} is not defined in config.json using defaults from config.json.example'.format(e)) hcatGoodMeasureBaseList = default_config[e.args[0]] +try: + hashview_url = config_parser['hashview_url'] +except KeyError as e: + print('{0} is not defined in config.json using defaults from config.json.example'.format(e)) + hashview_url = default_config.get('hashview_url', 'https://localhost:8443') + +try: + hashview_api_key = config_parser['hashview_api_key'] +except KeyError as e: + print('{0} is not defined in config.json using defaults from config.json.example'.format(e)) + hashview_api_key = default_config.get('hashview_api_key', '') + hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" @@ -299,7 +320,7 @@ def hcatQuickDictionary(hcatHashType, hcatHashFile, hcatChains, wordlists): def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime): global hcatMaskCount global hcatProcess - hcatProcess = subprocess.Popen( + subprocess.Popen( "cat {hash_file}.out | cut -d : -f 2 > {hash_file}.working".format( hash_file=hcatHashFile), shell=True).wait() hcatProcess = subprocess.Popen( @@ -350,7 +371,7 @@ def hcatFingerprint(hcatHashType, hcatHashFile): crackedAfter = 0 while crackedBefore != crackedAfter: crackedBefore = lineCount(hcatHashFile + ".out") - hcatProcess = subprocess.Popen("cat {hash_file}.out | cut -d : -f 2 > {hash_file}.working".format( + subprocess.Popen("cat {hash_file}.out | cut -d : -f 2 > {hash_file}.working".format( hash_file=hcatHashFile), shell=True).wait() hcatProcess = subprocess.Popen( "{hate_path}/hashcat-utils/bin/{expander_bin} < {hash_file}.working | sort -u > {hash_file}.expanded".format( @@ -428,7 +449,7 @@ def hcatHybrid(hcatHashType, hcatHashFile): hcatProcess.kill() hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} -o {hash_file}.out -a 6 -1 ?s?d {wordlist} ?1?1?1 " + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 6 -1 ?s?d {wordlist} ?1?1?1 " "{tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, @@ -444,7 +465,7 @@ def hcatHybrid(hcatHashType, hcatHashFile): hcatProcess.kill() hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} -o {hash_file}.out -a 6 -1 ?s?d {wordlist} " + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 6 -1 ?s?d {wordlist} " "?1?1?1?1 {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, @@ -460,7 +481,7 @@ def hcatHybrid(hcatHashType, hcatHashFile): hcatProcess.kill() hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} -o {hash_file}.out -a 7 -1 ?s?d ?1?1 {wordlist} " + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 7 -1 ?s?d ?1?1 {wordlist} " "{tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, @@ -476,7 +497,7 @@ def hcatHybrid(hcatHashType, hcatHashFile): hcatProcess.kill() hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} -o {hash_file}.out -a 7 -1 ?s?d ?1?1?1 {wordlist} " + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 7 -1 ?s?d ?1?1?1 {wordlist} " "{tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, @@ -492,7 +513,7 @@ def hcatHybrid(hcatHashType, hcatHashFile): hcatProcess.kill() hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} -o {hash_file}.out -a 7 -1 ?s?d ?1?1?1?1 {wordlist} " + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 7 -1 ?s?d ?1?1?1?1 {wordlist} " "{tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, @@ -530,10 +551,14 @@ def hcatYoloCombination(hcatHashType, hcatHashFile): left=hcatLeft, right=hcatRight, hate_path=hate_path), shell=True) - hcatProcess.wait() + try: + hcatProcess.wait() + except KeyboardInterrupt: + print('Killing PID {0}...'.format(str(hcatProcess.pid))) + hcatProcess.kill() + raise except KeyboardInterrupt: - print('Killing PID {0}...'.format(str(hcatProcess.pid))) - hcatProcess.kill() + pass # Bandrel methodlogy def hcatBandrel(hcatHashType, hcatHashFile): @@ -625,10 +650,14 @@ def hcatMiddleCombinator(hcatHashType, hcatHashFile): middle_mask=masks[x], hate_path=hate_path), shell=True) - hcatProcess.wait() + try: + hcatProcess.wait() + except KeyboardInterrupt: + print('Killing PID {0}...'.format(str(hcatProcess.pid))) + hcatProcess.kill() + raise except KeyboardInterrupt: - print('Killing PID {0}...'.format(str(hcatProcess.pid))) - hcatProcess.kill() + pass # Middle thorough Combinator Attack def hcatThoroughCombinator(hcatHashType, hcatHashFile): @@ -646,20 +675,20 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): new_masks.append('$'+mask) masks = new_masks + hcatProcess = subprocess.Popen( + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 1 {left} " + "{right} {tuning} --potfile-path={hate_path}/hashcat.pot".format( + hcatBin=hcatBin, + hash_type=hcatHashType, + hash_file=hcatHashFile, + session_name=os.path.basename(hcatHashFile), + left=hcatThoroughBaseList, + right=hcatThoroughBaseList, + word_lists=hcatWordlists, + tuning=hcatTuning, + hate_path=hate_path), + shell=True) try: - hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -a 1 {left} " - "{right} {tuning} --potfile-path={hate_path}/hashcat.pot".format( - hcatBin=hcatBin, - hash_type=hcatHashType, - hash_file=hcatHashFile, - session_name=os.path.basename(hcatHashFile), - left=hcatThoroughBaseList, - right=hcatThoroughBaseList, - word_lists=hcatWordlists, - tuning=hcatTuning, - hate_path=hate_path), - shell=True) hcatProcess.wait() except KeyboardInterrupt: print('Killing PID {0}...'.format(str(hcatProcess.pid))) @@ -681,10 +710,14 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): middle_mask=masks[x], hate_path=hate_path), shell=True) - hcatProcess.wait() + try: + hcatProcess.wait() + except KeyboardInterrupt: + print('Killing PID {0}...'.format(str(hcatProcess.pid))) + hcatProcess.kill() + raise except KeyboardInterrupt: - print('Killing PID {0}...'.format(str(hcatProcess.pid))) - hcatProcess.kill() + pass try: for x in range(len(masks)): hcatProcess = subprocess.Popen( @@ -701,10 +734,14 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): end_mask=masks[x], hate_path=hate_path), shell=True) - hcatProcess.wait() + try: + hcatProcess.wait() + except KeyboardInterrupt: + print('Killing PID {0}...'.format(str(hcatProcess.pid))) + hcatProcess.kill() + raise except KeyboardInterrupt: - print('Killing PID {0}...'.format(str(hcatProcess.pid))) - hcatProcess.kill() + pass try: for x in range(len(masks)): hcatProcess = subprocess.Popen( @@ -821,7 +858,7 @@ def hcatLMtoNT(): except KeyboardInterrupt: hcatProcess.kill() - hcatProcess = subprocess.Popen("cat {hash_file}.lm.cracked | cut -d : -f 2 > {hash_file}.working".format( + subprocess.Popen("cat {hash_file}.lm.cracked | cut -d : -f 2 > {hash_file}.working".format( hash_file=hcatHashFile), shell=True).wait() converted = convert_hex("{hash_file}.working".format(hash_file=hcatHashFile)) with open("{hash_file}.working".format(hash_file=hcatHashFile),mode='w') as working: @@ -917,7 +954,6 @@ def check_potfile(): else: print("No hashes found in POT file.") - # creating the combined output for pwdformat + cleartext def combine_ntlm_output(): hashes = {} @@ -963,6 +999,514 @@ def cleanup(): #incase someone mashes the Control+C it will still cleanup cleanup() +# Hashview Integration +class HashviewAPI: + """Upload files to Hashview API""" + + FILE_FORMATS = { + 'pwdump': 0, + 'netntlm': 1, + 'kerberos': 2, + 'shadow': 3, + 'user:hash': 4, + 'hash_only': 5, + } + + def __init__(self, base_url, api_key): + self.base_url = base_url.rstrip('/') + self.api_key = api_key + self.agent_uuid = None + self.session = requests.Session() + self.session.cookies.set('uuid', api_key) + # Disable SSL certificate verification for self-signed certificates + self.session.verify = False + # Suppress SSL warnings + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def use_agent_mode(self): + """Switch to using agent UUID for operations that require it""" + if not self.agent_uuid: + raise Exception("No agent registered. Call register_agent() first.") + + # Switch session to use agent UUID + self.session = self.agent_session + + def upload_wordlist(self, file_path, wordlist_name=None): + if wordlist_name is None: + wordlist_name = os.path.basename(file_path) + + with open(file_path, 'rb') as f: + file_content = f.read() + + url = f"{self.base_url}/v1/wordlists/add/{wordlist_name}" + headers = {'Content-Type': 'text/plain'} + + print(f"Uploading wordlist: {os.path.basename(file_path)} -> {wordlist_name}") + response = self.session.post(url, data=file_content, headers=headers) + response.raise_for_status() + return response.json() + + def upload_hashfile(self, file_path, customer_id, hash_type, file_format=5, hashfile_name=None): + if hashfile_name is None: + hashfile_name = os.path.basename(file_path) + + with open(file_path, 'rb') as f: + file_content = f.read() + + url = ( + f"{self.base_url}/v1/hashfiles/upload/" + f"{customer_id}/{file_format}/{hash_type}/{hashfile_name}" + ) + + headers = {'Content-Type': 'text/plain'} + + print(f"Uploading hashfile: {os.path.basename(file_path)} -> {hashfile_name}") + response = self.session.post(url, data=file_content, headers=headers) + response.raise_for_status() + return response.json() + + def upload_cracked_hashes(self, file_path, hash_type=1000): + # Read and convert file - API expects hex-encoded plaintext (uppercase) + print(f"Importing cracked hashes: {os.path.basename(file_path)}") + print(f" Converting plaintext to hex encoding...") + + converted_lines = [] + line_count = 0 + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + for line in f: + line = line.strip() + if not line or ':' not in line: + continue + + parts = line.split(':', 1) + if len(parts) != 2: + continue + + hash_value = parts[0].strip() + plaintext = parts[1].strip() + + # Convert plaintext to hex (uppercase as API expects) + plaintext_hex = plaintext.encode('utf-8').hex().upper() + converted_lines.append(f"{hash_value}:{plaintext_hex}") + line_count += 1 + + converted_content = '\n'.join(converted_lines) + + print(f" Processed {line_count} hash:plaintext pairs") + + # Use the actual endpoint from api_routes.py + url = f"{self.base_url}/v1/uploadCrackFile/{hash_type}" + + # API expects JSON with 'file' field + payload = { + "file": converted_content + } + headers = {'Content-Type': 'application/json'} + + print(f"\n === REQUEST DETAILS ===") + print(f" URL: {url}") + print(f" Method: POST") + print(f" Headers: {headers}") + print(f" Cookies: {dict(self.session.cookies)}") + print(f" Hash type: {hash_type}") + print(f" Payload preview (first 500 chars):") + print(converted_content[:500]) + print(f" Uploading...") + + response = self.session.post(url, json=payload, headers=headers) + + # Debug: print response details + print(f"\n === RESPONSE DETAILS ===") + print(f" Status code: {response.status_code}") + print(f" Response headers: {dict(response.headers)}") + print(f" Response content: {response.text[:500]}") + + response.raise_for_status() + + # Check if response is JSON error + try: + json_response = response.json() + if 'type' in json_response and json_response['type'] == 'Error': + raise Exception(f"Hashview API Error: {json_response.get('msg', 'Unknown error')}") + return json_response + except (json.JSONDecodeError, ValueError) as e: + # Not valid JSON + raise Exception(f"Invalid API response: {response.text[:200]}") + + def list_customers(self): + url = f"{self.base_url}/v1/customers" + + print("Fetching customer list...") + response = self.session.get(url) + response.raise_for_status() + data = response.json() + + # Parse the 'users' JSON string into a list + if 'users' in data: + customers = json.loads(data['users']) + return {'customers': customers} + + return data + + def create_customer(self, name, description=""): + url = f"{self.base_url}/v1/customers/add" + headers = {'Content-Type': 'application/json'} + data = {"name": name, "description": description} + + print(f"Creating customer: {name}") + response = self.session.post(url, json=data, headers=headers) + response.raise_for_status() + return response.json() + + def create_job(self, name, hashfile_id, customer_id, limit_recovered=False, notify_email=True): + url = f"{self.base_url}/v1/jobs/add" + headers = {'Content-Type': 'application/json'} + data = { + "name": name, + "hashfile_id": hashfile_id, + "customer_id": customer_id, + "limit_recovered": limit_recovered, + "notify_email": notify_email, + "notify_pushover": False + } + + print(f"Creating job: {name}") + response = self.session.post(url, json=data, headers=headers) + response.raise_for_status() + return response.json() + + def start_job(self, job_id): + url = f"{self.base_url}/v1/jobs/start/{job_id}" + + print(f"Starting job ID: {job_id}") + response = self.session.post(url) + response.raise_for_status() + return response.json() + + def list_jobs(self, customer_id=None): + # The API doesn't have a filter by customer endpoint, get all jobs + url = f"{self.base_url}/v1/jobs" + + if customer_id: + print(f"Fetching jobs for customer ID {customer_id}...") + else: + print("Fetching all jobs...") + + response = self.session.get(url) + response.raise_for_status() + data = response.json() + + # Parse the response - may return 'jobs' as JSON string + if 'jobs' in data and isinstance(data['jobs'], str): + jobs = json.loads(data['jobs']) + # Filter by customer_id if provided + if customer_id: + jobs = [job for job in jobs if job.get('customer_id') == customer_id] + return {'jobs': jobs} + + return data + + def download_left_hashes(self, customer_id, hashfile_id, output_file=None): + # Use the proper API v1 endpoint for downloading hashfiles (left only) + url = f"{self.base_url}/v1/hashfiles/{hashfile_id}" + + print(f"Downloading left hashes...") + print(f" Customer ID: {customer_id}") + print(f" Hashfile ID: {hashfile_id}") + + response = self.session.get(url) + + # Check if we got HTML (login page) instead of hash data + if response.content.startswith(b'