Merge pull request #58 from trustedsec/pipal

Pipal
This commit is contained in:
Justin Bollinger
2026-01-26 22:13:10 -05:00
committed by GitHub
4 changed files with 200 additions and 9 deletions
+68 -9
View File
@@ -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
@@ -1650,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)
@@ -1764,6 +1818,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 +1860,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(
+9
View File
@@ -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()
+80
View File
@@ -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
+43
View File
@@ -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