#!/usr/bin/env python3 """ Join IPMI dump output with a hashcat potfile to show cracked credentials along with host information. Expected formats: Dump file (from IPMI tool): 10.160.133.27 ADMIN:516a655d81000000...140541444d494e:55bc7c98e16c... Potfile (hashcat): 516a655d81000000...140541444d494e:55bc7c98e16c...:SomePassword Usage: ipmi_show_cracked.py -d ipmi-dump.txt -p ipmi_hashes.potfile """ import argparse import os import sys def debug(msg: str) -> None: print(f"[DEBUG] {msg}", file=sys.stderr) def warn(msg: str) -> None: print(f"[WARN] {msg}", file=sys.stderr) def error(msg: str) -> None: print(f"[ERROR] {msg}", file=sys.stderr) def load_potfile(pot_path: str) -> dict: """ Load a hashcat potfile into a dict mapping: "hash_part1:hash_part2" (lowercased) -> plaintext password """ mapping = {} try: with open(pot_path, "r", encoding="utf-8", errors="ignore") as f: for line_no, line in enumerate(f, start=1): line = line.strip() if not line or line.startswith("#"): continue parts = line.split(":") if len(parts) < 3: warn(f"Unexpected potfile format on line {line_no}, skipping") continue # Everything except the last field is the hash string password = parts[-1] ipmi_hash = ":".join(parts[:-1]).lower() mapping[ipmi_hash] = password debug(f"Loaded {len(mapping)} unique cracked hashes from potfile") except FileNotFoundError: error(f"Potfile not found: {pot_path}") sys.exit(1) except OSError as e: error(f"Error reading potfile '{pot_path}': {e}") sys.exit(1) return mapping def process_dump(dump_path: str, cracked_map: dict) -> None: """ Read the IPMI dump file, join with cracked_map, and print results. """ total_lines = 0 matched = 0 unmatched = 0 try: with open(dump_path, "r", encoding="utf-8", errors="ignore") as f: # Header print("{:<15} {:<10} {:<20} {}".format( "HOST", "USER", "STATUS", "PASSWORD" )) print("-" * 70) for line_no, line in enumerate(f, start=1): line = line.strip() if not line or line.startswith("#"): continue total_lines += 1 # Expect: " ::" try: host, rest = line.split(None, 1) except ValueError: warn(f"Line {line_no}: could not split host and rest, skipping") unmatched += 1 continue rest_parts = rest.split(":") if len(rest_parts) < 3: warn(f"Line {line_no}: unexpected dump format, skipping") unmatched += 1 continue user = rest_parts[0] hash_part1 = rest_parts[1] hash_part2 = rest_parts[2] ipmi_hash = f"{hash_part1}:{hash_part2}".lower() if ipmi_hash in cracked_map: password = cracked_map[ipmi_hash] status = "CRACKED" matched += 1 else: password = "" status = "UNCRACKED" unmatched += 1 print("{:<15} {:<10} {:<20} {}".format( host, user, status, password )) except FileNotFoundError: error(f"Dump file not found: {dump_path}") sys.exit(1) except OSError as e: error(f"Error reading dump file '{dump_path}': {e}") sys.exit(1) debug(f"Processed {total_lines} dump lines") debug(f"Matched (cracked): {matched}") debug(f"Unmatched (uncracked or bad format): {unmatched}") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Join IPMI hash dump with hashcat potfile to show cracked credentials" ) parser.add_argument( "-d", "--dump", help="IPMI hash dump file (with host info)", required=False ) parser.add_argument( "-p", "--pot", help="hashcat potfile containing cracked IPMI hashes", required=False ) args = parser.parse_args() # Enforce required args and show help if missing (per your preference) if not args.dump or not args.pot: print("[ERROR] Both --dump and --pot are required.\n", file=sys.stderr) parser.print_help(sys.stderr) sys.exit(1) return args def main() -> None: args = parse_args() debug(f"Using dump file: {args.dump}") debug(f"Using potfile : {args.pot}") # Quick existence checks for extra robustness for path_label, path_value in (("dump file", args.dump), ("potfile", args.pot)): if not os.path.isfile(path_value): error(f"Specified {path_label} does not exist or is not a file: {path_value}") sys.exit(1) cracked_map = load_potfile(args.pot) process_dump(args.dump, cracked_map) if __name__ == "__main__": main()