From 78056b6d1f178629bfe2a685a0cee632eb065210 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 26 Jan 2026 21:54:39 -0500 Subject: [PATCH 1/3] moved comments back to top --- hate_crack.py | 71 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/hate_crack.py b/hate_crack.py index 066e6d2..1b57576 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -1,3 +1,11 @@ +# 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 + +# Load config before anything that needs hashview_url/hashview_api_key + import sys import os import json @@ -25,13 +33,7 @@ if os.path.isdir(_pkg_dir): if "__spec__" in globals() and __spec__ is not None: __spec__.submodule_search_locations = __path__ -# 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 -# Load config before anything that needs hashview_url/hashview_api_key hate_path = os.path.dirname(os.path.realpath(__file__)) if not os.path.isfile(hate_path + '/config.json'): print('Initializing config.json from config.json.example') @@ -103,6 +105,7 @@ from hate_crack.api import ( download_hashes_from_hashview, download_hashmob_wordlists, download_weakpass_torrent, + extract_with_7z, ) from hate_crack.cli import ( add_common_args, @@ -216,6 +219,38 @@ def verify_wordlist_dir(directory, wordlist): print('Exiting. Please check configuration and try again.') return None +def cleanup_wordlist_artifacts(): + wordlists_dir = hcatWordlists or os.path.join(hate_path, "wordlists") + if not os.path.isabs(wordlists_dir): + wordlists_dir = os.path.join(hate_path, wordlists_dir) + targets = [hate_path, os.getcwd()] + if wordlists_dir not in targets: + targets.append(wordlists_dir) + + for base in targets: + if not os.path.isdir(base): + continue + for name in os.listdir(base): + path = os.path.join(base, name) + if name.endswith(".out"): + try: + os.remove(path) + except Exception as e: + print(f"[!] Failed to remove output file {path}: {e}") + if base == wordlists_dir and name.endswith(".torrent"): + try: + os.remove(path) + except Exception as e: + print(f"[!] Failed to remove torrent file {path}: {e}") + if base == wordlists_dir and name.endswith(".7z"): + ok = extract_with_7z(path) + if not ok: + try: + os.remove(path) + print(f"[!] Removed failed archive: {path}") + except Exception as e: + print(f"[!] Failed to remove archive {path}: {e}") + if not SKIP_INIT: # hashcat biniary checks for systems that install hashcat binary in different location than the rest of the hashcat files if hcatPath: @@ -1169,15 +1204,28 @@ def check_potfile(): def combine_ntlm_output(): hashes = {} check_potfile() + if not os.path.isfile(hcatHashFile + ".out"): + print("No hashes found in POT file.") + return with open(hcatHashFile + ".out", "r") as hcatCrackedFile: for crackedLine in hcatCrackedFile: - hash, password = crackedLine.split(':') + parts = crackedLine.split(':', 1) + if len(parts) != 2: + continue + hash, password = parts hashes[hash] = password.rstrip() + if not hashes: + print("No hashes found in POT file.") + return with open(hcatHashFileOrig + ".out", "w+") as hcatCombinedHashes: with open(hcatHashFileOrig, "r") as hcatOrigFile: for origLine in hcatOrigFile: - if origLine.split(':')[3] in hashes: - password = hashes[origLine.split(':')[3]] + orig_parts = origLine.split(':') + if len(orig_parts) < 4: + continue + ntlm_hash = orig_parts[3] + if ntlm_hash in hashes: + password = hashes[ntlm_hash] hcatCombinedHashes.write(origLine.strip()+password+'\n') # Cleanup Temp Files @@ -1764,6 +1812,7 @@ def main(): parser.add_argument('--weakpass', action='store_true', help='Download wordlists from Weakpass') parser.add_argument('--rank', type=int, default=-1, help='Only show wordlists with this rank (use 0 to show all, default: >4)') parser.add_argument('--hashmob', action='store_true', help='Download wordlists from Hashmob.net') + parser.add_argument('--cleanup', action='store_true', help='Cleanup .out files, torrents, and extract or remove .7z archives') parser.add_argument('--debug', action='store_true', help='Enable debug mode') # Removed add_common_args(parser) since config items are now only set via config file args = parser.parse_args() @@ -1805,6 +1854,10 @@ def main(): ) sys.exit(0) + if args.cleanup: + cleanup_wordlist_artifacts() + sys.exit(0) + if args.download_all_torrents: try: download_all_weakpass_torrents( From a0d3286d28c83ed4fe147aa9feed75e77c269fa5 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 26 Jan 2026 22:05:19 -0500 Subject: [PATCH 2/3] pipal tests --- hate_crack/api.py | 9 ++++ tests/test_pipal.py | 80 +++++++++++++++++++++++++++++++++ tests/test_pipal_integration.py | 43 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 tests/test_pipal.py create mode 100644 tests/test_pipal_integration.py diff --git a/hate_crack/api.py b/hate_crack/api.py index f33cd6f..724af6b 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1,6 +1,7 @@ import json import os import threading +import time from queue import Queue import shutil from typing import Callable, Tuple @@ -80,6 +81,14 @@ def register_torrent_cleanup(): _TORRENT_CLEANUP_REGISTERED = True def fetch_all_weakpass_wordlists_multithreaded(total_pages=67, threads=10, output_file="weakpass_wordlists.json"): + if os.path.isfile(output_file): + try: + mtime = os.path.getmtime(output_file) + if (time.time() - mtime) < 24 * 60 * 60: + print(f"[i] Using cached wordlist file: {output_file}") + return + except Exception: + pass wordlists = [] lock = threading.Lock() q = Queue() diff --git a/tests/test_pipal.py b/tests/test_pipal.py new file mode 100644 index 0000000..08da3dd --- /dev/null +++ b/tests/test_pipal.py @@ -0,0 +1,80 @@ +import os + + +def _write_executable(path, content): + path.write_text(content) + os.chmod(path, 0o755) + + +def test_pipal_runs_and_parses_basewords(hc_module, tmp_path, capsys): + hc = hc_module + hc.hcatHashType = "0" + hc.pipal_count = 3 + hc.hcatHashFile = str(tmp_path / "hashes") + + out_path = tmp_path / "hashes.out" + out_path.write_text( + "hash1:password123\n" + "hash2:$HEX[70617373313233]\n" + ) + + pipal_stub = tmp_path / "pipal_stub.py" + _write_executable( + pipal_stub, + "#!/usr/bin/env python3\n" + "import sys\n" + "out = None\n" + "if '--output' in sys.argv:\n" + " out = sys.argv[sys.argv.index('--output') + 1]\n" + "if not out:\n" + " out = sys.argv[-1]\n" + "with open(out, 'w') as f:\n" + " f.write('Top 3 base words\\n')\n" + " f.write('pass123 10\\n')\n" + " f.write('letmein 5\\n')\n" + " f.write('welcome 3\\n')\n" + ) + hc.pipalPath = str(pipal_stub) + + result = hc.pipal() + captured = capsys.readouterr() + + assert result == ["pass123", "letmein", "welcome"] + assert "Pipal file is at" in captured.out + + passwords_file = tmp_path / "hashes.passwords" + assert passwords_file.exists() + content = passwords_file.read_text() + assert "password123" in content + assert "pass123" in content + + +def test_pipal_missing_out_returns_empty(hc_module, tmp_path, capsys): + hc = hc_module + hc.hcatHashType = "0" + hc.pipal_count = 3 + hc.hcatHashFile = str(tmp_path / "hashes") + + pipal_stub = tmp_path / "pipal_stub.py" + _write_executable( + pipal_stub, + "#!/usr/bin/env python3\n" + "import sys\n" + "out = None\n" + "if '--output' in sys.argv:\n" + " out = sys.argv[sys.argv.index('--output') + 1]\n" + "if not out:\n" + " out = sys.argv[-1]\n" + "with open(out, 'w') as f:\n" + " f.write('Top 3 base words\\n')\n" + " f.write('pass123 10\\n')\n" + " f.write('letmein 5\\n')\n" + " f.write('welcome 3\\n')\n" + ) + hc.pipalPath = str(pipal_stub) + + result = hc.pipal() + captured = capsys.readouterr() + + assert result == [] + assert "No hashes were cracked" in captured.out diff --git a/tests/test_pipal_integration.py b/tests/test_pipal_integration.py new file mode 100644 index 0000000..6017a4f --- /dev/null +++ b/tests/test_pipal_integration.py @@ -0,0 +1,43 @@ +import os +import subprocess + + +import json + +def test_pipal_executable_and_runs(tmp_path): + # Read pipalPath from config.json + config_path = os.path.join(os.path.dirname(__file__), '..', 'config.json') + with open(config_path, 'r') as f: + config = json.load(f) + pipal_path = config.get('pipalPath') + if not pipal_path or not os.path.isfile(pipal_path): + import pytest + pytest.skip("pipalPath not configured or file missing") + + if not os.access(pipal_path, os.X_OK): + raise AssertionError( + f"pipalPath exists but is not executable: {pipal_path}. " + "Ensure pipal is installed/compiled and has execute permissions." + ) + + input_file = tmp_path / "pipal_input.txt" + input_file.write_text("password\npassword123\nletmein\n") + output_file = tmp_path / "pipal_output.txt" + + result = subprocess.run( + [pipal_path, str(input_file), "-t", "3", "--output", str(output_file)], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + raise AssertionError( + "pipal did not run successfully. " + f"returncode={result.returncode}, stdout={result.stdout}, stderr={result.stderr}" + ) + + if not output_file.exists(): + raise AssertionError("pipal did not produce an output file; it may need to be compiled.") + + content = output_file.read_text() + assert "Top 3 base words" in content From a53719cfe1efb25d29a00c847bda5146c6f0459d Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 26 Jan 2026 22:10:11 -0500 Subject: [PATCH 3/3] pipal print --- hate_crack.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hate_crack.py b/hate_crack.py index 1b57576..0b6a290 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -1698,6 +1698,12 @@ def pipal(): print('Killing PID {0}...'.format(str(pipalProcess.pid))) pipalProcess.kill() print("Pipal file is at " + hcatHashFilePipal + ".pipal\n") + view_choice = input("Would you like to view (cat) the pipal output? (Y/n): ").strip().lower() + if view_choice in ('', 'y', 'yes'): + print("\n--- Pipal Output Start ---\n") + with open(hcatHashFilePipal + ".pipal") as pipalfile: + print(pipalfile.read()) + print("\n--- Pipal Output End ---\n") with open(hcatHashFilePipal + ".pipal") as pipalfile: pipal_content = pipalfile.readlines() raw_pipal = '\n'.join(pipal_content)