diff --git a/config.json.example b/config.json.example index c6ded51..dfc890f 100644 --- a/config.json.example +++ b/config.json.example @@ -3,6 +3,7 @@ "hcatBin": "hashcat", "hcatTuning": "--force --remove", "hcatPotfilePath": "~/.hashcat/hashcat.potfile", + "hcatDebugLogPath": "./hashcat_debug", "hcatWordlists": "/Passwords/wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists", "rules_directory": "/path/to/hashcat/rules", diff --git a/hate_crack/api.py b/hate_crack/api.py index b021b07..e6f058f 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -962,14 +962,16 @@ class HashviewAPI: "combined_file": combined_file, } - def download_found_hashes(self, customer_id, hashfile_id, output_file=None): + def download_found_hashes(self, customer_id, hashfile_id, output_file=None, hash_type=None): import sys + import subprocess url = f"{self.base_url}/v1/hashfiles/{hashfile_id}/found" resp = self.session.get(url, headers=self._auth_headers(), stream=True) resp.raise_for_status() if output_file is None: output_file = f"found_{customer_id}_{hashfile_id}.txt" + total = int(resp.headers.get("content-length", 0)) downloaded = 0 chunk_size = 8192 @@ -992,81 +994,110 @@ class HashviewAPI: if total == 0: print(f"Downloaded {downloaded} bytes.") - # Combine with corresponding left file output if it exists - # Only combine if the output file matches the expected naming pattern + # Split found file into hashes and clears + output_dir = os.path.dirname(os.path.abspath(output_file)) or os.getcwd() + found_hashes_file = os.path.join(output_dir, f"found_hashes_{customer_id}_{hashfile_id}.txt") + found_clears_file = os.path.join(output_dir, f"found_clears_{customer_id}_{hashfile_id}.txt") + + hashes_count = 0 + clears_count = 0 combined_count = 0 combined_file = None - - # Extract customer_id and hashfile_id from the output filename to ensure proper matching - import re - - output_basename = os.path.basename(output_file) - # Match pattern: found_{customer_id}_{hashfile_id}.txt - match = re.match(r"found_(\d+)_(\d+)\.txt$", output_basename) - - if match: - found_customer_id = match.group(1) - found_hashfile_id = match.group(2) - - # Only proceed if the IDs from filename match the actual download IDs - if ( - str(customer_id) == found_customer_id - and str(hashfile_id) == found_hashfile_id - ): - left_base = f"left_{customer_id}_{hashfile_id}.txt" - - # Check for regular format .out file - left_out = left_base + ".out" - if not os.path.exists(left_out): - # Check for pwdump format .nt.txt.out file - left_out = f"left_{customer_id}_{hashfile_id}.nt.txt.out" - - if os.path.exists(left_out): - # Read existing hashes from .out file into a set - found_hashes = set() - with open(left_out, "r", encoding="utf-8", errors="ignore") as f: - for line in f: - line = line.strip() - if line: - found_hashes.add(line) - - original_count = len(found_hashes) - - # Read and add hashes from the downloaded found file - with open(output_file, "r", encoding="utf-8", errors="ignore") as f: - for line in f: - line = line.strip() - if line: - found_hashes.add(line) - - combined_count = len(found_hashes) - original_count - - # Write combined results to the .out file - combined_file = left_out - with open(combined_file, "w", encoding="utf-8") as f: - for line in sorted(found_hashes): - f.write(line + "\n") - - print(f"Combined {combined_count} new hashes from {output_file}") - print( - f"Total unique hashes in {combined_file}: {len(found_hashes)}" - ) + + try: + with open(found_hashes_file, "w", encoding="utf-8") as hf, \ + open(found_clears_file, "w", encoding="utf-8") as cf: + with open(output_file, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + if line: + parts = line.rsplit(":", 1) # Split on last colon + if len(parts) == 2: + hash_part, clear_part = parts + hf.write(hash_part + "\n") + cf.write(clear_part + "\n") + hashes_count += 1 + clears_count += 1 + + print(f"✓ Split found file into {hashes_count} hashes and {clears_count} clears") + + # Run hashcat to combine them into an output file + combined_file = output_file + ".out" + try: + tuning_args = get_hcat_tuning_args() + + # Create temporary outfile for hashcat + temp_outfile = output_file + ".tmp" + + if self.debug: + print(f"[DEBUG] download_found_hashes: hash_type={hash_type}, type={type(hash_type)}") + + # Build command with hash type if provided + cmd = ["hashcat", *tuning_args] + if hash_type: + cmd.extend(["-m", str(hash_type)]) + cmd.extend([ + found_hashes_file, + found_clears_file, + "--outfile", + temp_outfile, + "--outfile-format=1,2", + ]) + + if self.debug: + print(f"[DEBUG] Running command: {' '.join(cmd)}") + + print(f"Running: {' '.join(cmd)}") + + result = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, timeout=300) + + if result.returncode != 0: + print(f"Warning: hashcat exited with code {result.returncode}") + if result.stderr: + print(f" stderr: {result.stderr}") + + # Write the output file + if os.path.exists(temp_outfile): + with open(temp_outfile, "r", encoding="utf-8", errors="ignore") as tmp_f: + with open(combined_file, "w", encoding="utf-8") as out_f: + out_f.write(tmp_f.read()) - # Delete the found file after successful merge + # Count lines in output + with open(combined_file, "r", encoding="utf-8", errors="ignore") as f: + combined_count = len(f.readlines()) + print(f"✓ Created {combined_file} (total lines: {combined_count})") + + # Clean up temp file try: - os.remove(output_file) - print(f"Deleted {output_file} after merge") + os.remove(temp_outfile) + except Exception: + pass + else: + print("Note: No cracked hashes generated") + + except FileNotFoundError: + print("✗ Error: hashcat not found in PATH") + except subprocess.TimeoutExpired: + print("✗ Error: hashcat execution timed out") + except Exception as e: + print(f"✗ Error running hashcat: {e}") + + # Clean up temporary files (keep when debug is enabled) + if not self.debug: + files_to_delete = [found_hashes_file, found_clears_file] + for temp_file in files_to_delete: + try: + if os.path.exists(temp_file): + os.remove(temp_file) except Exception as e: - print(f"Warning: Could not delete {output_file}: {e}") + if self.debug: + print(f"Warning: Could not delete {temp_file}: {e}") else: - print( - f"Skipping combine: customer_id/hashfile_id mismatch (expected {customer_id}/{hashfile_id}, filename has {found_customer_id}/{found_hashfile_id})" - ) - else: - print( - f"Skipping combine: output filename '{output_basename}' doesn't match expected pattern 'found__.txt'" - ) - + print("Debug enabled: keeping split files") + + except Exception as e: + print(f"✗ Error splitting found file: {e}") + return { "output_file": output_file, "size": downloaded, diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index deb104c..5dc08aa 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -327,7 +327,75 @@ def fingerprint_crack(ctx: Any) -> None: def combinator_crack(ctx: Any) -> None: - ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile) + print("\n" + "=" * 60) + print("COMBINATOR ATTACK") + print("=" * 60) + print("This attack combines two wordlists to generate candidates.") + print("Example: wordlist1='password' + wordlist2='123' = 'password123'") + print("=" * 60) + + use_default = ( + input("\nUse default combinator wordlists from config? (Y/n): ").strip().lower() + ) + + if use_default != "n": + print("\nUsing default wordlist(s) from config:") + if isinstance(ctx.hcatCombinationWordlist, list): + for wl in ctx.hcatCombinationWordlist: + print(f" - {wl}") + wordlists = ctx.hcatCombinationWordlist + else: + print(f" - {ctx.hcatCombinationWordlist}") + wordlists = [ctx.hcatCombinationWordlist] + else: + print("\nSelect wordlists for combinator attack.") + print("You need to provide exactly 2 wordlists.") + print("You can enter:") + print(" - Two file paths separated by commas") + print(" - Press TAB to autocomplete file paths") + + selection = ctx.select_file_with_autocomplete( + "Enter 2 wordlist files (comma-separated)", allow_multiple=True + ) + + if not selection: + print("No wordlists selected. Aborting combinator attack.") + return + + if isinstance(selection, str): + wordlists = [selection] + else: + wordlists = selection + + if len(wordlists) < 2: + print("\n[!] Combinator attack requires at least 2 wordlists.") + print("Aborting combinator attack.") + return + + valid_wordlists = [] + for wl in wordlists[:2]: # Only use first 2 + resolved = ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) + if os.path.isfile(resolved): + valid_wordlists.append(resolved) + print(f"✓ Found: {resolved}") + else: + print(f"✗ Not found: {resolved}") + + if len(valid_wordlists) < 2: + print("\nCould not find 2 valid wordlists. Aborting combinator attack.") + return + + wordlists = valid_wordlists + + wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists[:2]] + + print(f"\nStarting combinator attack with 2 wordlists:") + print(f" Wordlist 1: {wordlists[0]}") + print(f" Wordlist 2: {wordlists[1]}") + print(f"Hash type: {ctx.hcatHashType}") + print(f"Hash file: {ctx.hcatHashFile}") + + ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists) def hybrid_crack(ctx: Any) -> None: diff --git a/hate_crack/config.json.example b/hate_crack/config.json.example index 5b7ec14..48df46a 100644 --- a/hate_crack/config.json.example +++ b/hate_crack/config.json.example @@ -3,6 +3,7 @@ "hcatBin": "hashcat", "hcatTuning": "--force --remove", "hcatPotfilePath": "~/.hashcat/hashcat.potfile", + "hcatDebugLogPath": "./hashcat_debug", "hcatWordlists": "/Passwords/wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists", "rules_directory": "/path/to/hashcat/rules", diff --git a/hate_crack/main.py b/hate_crack/main.py index c26aed6..9c87e3d 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -454,6 +454,18 @@ except KeyError as e: ) hcatGoodMeasureBaseList = default_config[e.args[0]] +try: + hcatDebugLogPath = config_parser.get("hcatDebugLogPath", "./hashcat_debug") + # Expand user home directory if present + hcatDebugLogPath = os.path.expanduser(hcatDebugLogPath) +except KeyError as e: + print( + "{0} is not defined in config.json using defaults from config.json.example".format( + e + ) + ) + hcatDebugLogPath = os.path.expanduser(default_config.get("hcatDebugLogPath", "./hashcat_debug")) + hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" hcatPrinceBin = "pp64.bin" @@ -689,6 +701,28 @@ def _debug_cmd(cmd): print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}") +def _add_debug_mode_for_rules(cmd): + """Add debug mode arguments to hashcat command if rules are being used. + + This function detects if rules are present in the command (by looking for -r flags) + and adds --debug-mode=1 and --debug-file= if rules are found. + Debug log path is configurable via hcatDebugLogPath in config.json + """ + if "-r" in cmd: + # Create debug output directory if it doesn't exist + os.makedirs(hcatDebugLogPath, exist_ok=True) + + # Create a debug output filename based on the session ID or hash file + debug_filename = os.path.join(hcatDebugLogPath, "hashcat_debug.log") + if "--session" in cmd: + session_idx = cmd.index("--session") + 1 + if session_idx < len(cmd): + debug_filename = os.path.join(hcatDebugLogPath, f"hashcat_debug_{cmd[session_idx]}.log") + + cmd.extend(["--debug-mode", "4", "--debug-file", debug_filename]) + return cmd + + # Sanitize filename for use as hashcat session name def generate_session_id(): """Sanitize the hashfile name for use as a hashcat session name @@ -932,6 +966,7 @@ def hcatDictionary(hcatHashType, hcatHashFile): cmd.extend(["-r", rule_best66]) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -956,6 +991,7 @@ def hcatDictionary(hcatHashType, hcatHashFile): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -979,6 +1015,7 @@ def hcatDictionary(hcatHashType, hcatHashFile): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -1020,6 +1057,7 @@ def hcatQuickDictionary( cmd.extend(shlex.split(hcatChains)) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd, use_potfile_path=use_potfile_path, potfile_path=potfile_path) + cmd = _add_debug_mode_for_rules(cmd) _debug_cmd(cmd) hcatProcess = subprocess.Popen(cmd) try: @@ -1178,9 +1216,35 @@ def hcatFingerprint( # Combinator Attack -def hcatCombination(hcatHashType, hcatHashFile): +def hcatCombination(hcatHashType, hcatHashFile, wordlists=None): global hcatCombinationCount global hcatProcess + + # Use provided wordlists or fall back to config default + if wordlists is None: + wordlists = hcatCombinationWordlist + + # Ensure wordlists is a list with at least 2 items + if not isinstance(wordlists, list): + wordlists = [wordlists] + + if len(wordlists) < 2: + print("[!] Combinator attack requires at least 2 wordlists.") + return + + # Resolve wordlist paths + resolved_wordlists = [] + for wordlist in wordlists[:2]: # Only use first 2 wordlists + resolved = _resolve_wordlist_path(wordlist, hcatWordlists) + if os.path.isfile(resolved): + resolved_wordlists.append(resolved) + else: + print(f"[!] Wordlist not found: {resolved}") + + if len(resolved_wordlists) < 2: + print("[!] Could not find 2 valid wordlists. Aborting combinator attack.") + return + cmd = [ hcatBin, "-m", @@ -1192,8 +1256,8 @@ def hcatCombination(hcatHashType, hcatHashFile): f"{hcatHashFile}.out", "-a", "1", - hcatCombinationWordlist[0], - hcatCombinationWordlist[1], + resolved_wordlists[0], + resolved_wordlists[1], ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) @@ -1620,6 +1684,7 @@ def hcatPrince(hcatHashType, hcatHashFile): ] hashcat_cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(hashcat_cmd) + hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd) with open(prince_base, "rb") as base: prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE) hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout) @@ -1656,6 +1721,7 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -1739,6 +1805,7 @@ def hcatLMtoNT(): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -1778,6 +1845,7 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) + cmd = _add_debug_mode_for_rules(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -1897,6 +1965,7 @@ def hashview_api(): menu_options.append(("upload_wordlist", "Upload Wordlist")) menu_options.append(("download_wordlist", "Download Wordlist")) menu_options.append(("download_left", "Download Left Hashes (with automatic merge if found)")) + menu_options.append(("download_found", "Download Found Hashes (with automatic split)")) if hcatHashFile: menu_options.append(("upload_hashfile_job", "Upload Hashfile and Create Job")) menu_options.append(("back", "Back to Main Menu")) @@ -2415,6 +2484,152 @@ def hashview_api(): except Exception as e: print(f"\n✗ Error downloading hashes: {str(e)}") + elif option_key == "download_found": + # Download found hashes + try: + while True: + # First, list customers to help user select + customers_result = api_harness.list_customers() + customers = ( + customers_result.get("customers", []) + if isinstance(customers_result, dict) + else customers_result + ) + if customers: + api_harness.display_customers_multicolumn(customers) + else: + print("\nNo customers found.") + + # Select or create customer + customer_input = input( + "\nEnter customer ID or N to create new: " + ).strip() + if customer_input.lower() == "n": + customer_name = input("Enter customer name: ").strip() + if customer_name: + try: + result = api_harness.create_customer(customer_name) + print( + f"\n✓ Success: {result.get('msg', 'Customer created')}" + ) + customer_id = result.get("customer_id") or result.get("id") + if not customer_id: + print("\n✗ Error: Customer ID not returned.") + continue + print(f" Customer ID: {customer_id}") + except Exception as e: + print(f"\n✗ Error creating customer: {str(e)}") + continue + else: + print("\n✗ Error: Customer name cannot be empty.") + continue + else: + try: + customer_id = int(customer_input) + except ValueError: + print( + "\n✗ Error: Invalid ID entered. Please enter a numeric ID or N." + ) + continue + + # List hashfiles for the customer + try: + customer_hashfiles = api_harness.get_customer_hashfiles( + customer_id + ) + + if not customer_hashfiles: + print(f"\nNo hashfiles found for customer ID {customer_id}") + continue + + print("\n" + "=" * 120) + print(f"Hashfiles for Customer ID {customer_id}:") + print("=" * 120) + print(f"{'ID':<10} {'Hash Type':<10} {'Name':<96}") + print("-" * 120) + hashfile_map = {} + for hf in customer_hashfiles: + hf_id = hf.get("id") + hf_name = hf.get("name", "N/A") + hf_type = hf.get("hash_type") or hf.get("hashtype") or "N/A" + if hf_id is None: + continue + # Truncate long names to fit within 120 columns + if len(str(hf_name)) > 96: + hf_name = str(hf_name)[:93] + "..." + if debug_mode: + print(f"[DEBUG] Hashfile {hf_id}: hash_type={hf.get('hash_type')}, hashtype={hf.get('hashtype')}, combined={hf_type}") + print(f"{hf_id:<10} {hf_type:<10} {hf_name:<96}") + hashfile_map[int(hf_id)] = hf_type + print("=" * 120) + print(f"Total: {len(hashfile_map)} hashfile(s)") + except Exception as e: + print(f"\nWarning: Could not list hashfiles: {e}") + continue + + while True: + try: + hashfile_id_input = input("\nEnter hashfile ID: ").strip() + hashfile_id = int(hashfile_id_input) + except ValueError: + print("\n✗ Error: Invalid ID entered. Please enter a numeric ID.") + continue + if hashfile_id not in hashfile_map: + print("\n✗ Error: Hashfile ID not in the list. Please try again.") + continue + break + break + + # Set output filename automatically + output_file = f"found_{customer_id}_{hashfile_id}.txt" + + # Get hash type for hashcat from the hashfile map + selected_hash_type = hashfile_map.get(hashfile_id) + if debug_mode: + print(f"[DEBUG] selected_hash_type from map: {selected_hash_type}") + if not selected_hash_type or selected_hash_type == "N/A": + try: + details = api_harness.get_hashfile_details(hashfile_id) + selected_hash_type = details.get("hashtype") + if debug_mode: + print(f"[DEBUG] selected_hash_type from get_hashfile_details: {selected_hash_type}") + except Exception as e: + if debug_mode: + print(f"[DEBUG] Error fetching hashfile details: {e}") + selected_hash_type = None + + # Download the found hashes + if debug_mode: + print(f"[DEBUG] Calling download_found_hashes with hash_type={selected_hash_type}") + download_result = api_harness.download_found_hashes( + customer_id, hashfile_id, output_file + ) + print(f"\n✓ Success: Downloaded {download_result['size']} bytes") + print(f" File: {download_result['output_file']}") + if selected_hash_type: + print(f" Hash mode: {selected_hash_type}") + + # Ask if user wants to switch to this hashfile + switch = ( + input("\nSwitch to this hashfile for cracking? (Y/n): ") + .strip() + .lower() + ) + if switch != "n": + hcatHashFile = download_result["output_file"] + if selected_hash_type: + hcatHashType = str(selected_hash_type) + else: + hcatHashType = "1000" # Default to NTLM if unavailable + print(f"✓ Switched to hashfile: {hcatHashFile}") + print("\nReturning to main menu to start cracking...") + return # Exit hashview menu and return to main menu + + except ValueError: + print("\n✗ Error: Invalid ID entered. Please enter a numeric ID.") + except Exception as e: + print(f"\n✗ Error downloading found hashes: {str(e)}") + elif option_key == "back": break