"""Pure helpers that format captured authentication material for hashcat. Supported hashcat modes: 5500 NetNTLMv1 5600 NetNTLMv2 18200 Kerberos AS-REP (etype 23) 13100 Kerberos TGS-REP (etype 23) """ from __future__ import annotations import base64 from typing import Optional def _hex(s: Optional[str]) -> str: """Normalize a hexadecimal string and remove separators.""" if not s: return "" return "".join(c for c in str(s).lower() if c in "0123456789abcdef") # --------------------------------------------------------------------------- # # NTLM / NetNTLM # --------------------------------------------------------------------------- # def build_netntlmv2(user, domain, server_challenge, nt_response) -> Optional[dict]: """NetNTLMv2 (hashcat -m 5600). Format: user::domain:serverchallenge:NTproofstr:blob The NT response contains a 16-byte proof followed by the remaining blob. """ chal = _hex(server_challenge) nt = _hex(nt_response) if not user or len(chal) != 16 or len(nt) < 32: return None ntproof, blob = nt[:32], nt[32:] line = f"{user}::{domain or ''}:{chal}:{ntproof}:{blob}" return {"hashcat": line, "hashcat_mode": 5600, "format": "NetNTLMv2"} def build_netntlmv1(user, host, server_challenge, lm_response, nt_response) -> Optional[dict]: """NetNTLMv1 (hashcat -m 5500). Format: user::host:LMresponse:NTresponse:serverchallenge """ chal = _hex(server_challenge) nt = _hex(nt_response) lm = _hex(lm_response) or nt if not user or len(chal) != 16 or len(nt) < 32: return None line = f"{user}::{host or ''}:{lm}:{nt}:{chal}" return {"hashcat": line, "hashcat_mode": 5500, "format": "NetNTLMv1"} # --------------------------------------------------------------------------- # # Kerberos # --------------------------------------------------------------------------- # def build_krb5_asrep(user, realm, etype, cipher) -> Optional[dict]: """Kerberos AS-REP Roasting (hashcat -m 18200, etype 23). Format: $krb5asrep$$user@REALM:$ cipher = checksum (16 Byte) + edata. """ ci = _hex(cipher) if not user or len(ci) <= 32: return None et = str(etype or 23) checksum, edata = ci[:32], ci[32:] realm_u = (realm or "").upper() line = f"$krb5asrep${et}${user}@{realm_u}:{checksum}${edata}" mode = 18200 if et == "23" else None return {"hashcat": line, "hashcat_mode": mode, "format": f"krb5asrep(etype={et})"} def build_krb5_tgsrep(user, realm, spn, etype, cipher) -> Optional[dict]: """Kerberoasting / TGS-REP (hashcat -m 13100, etype 23). Format: $krb5tgs$$*user$realm$spn*$$ """ ci = _hex(cipher) if not user or len(ci) <= 32: return None et = str(etype or 23) checksum, edata = ci[:32], ci[32:] realm_u = (realm or "").upper() line = f"$krb5tgs${et}$*{user}${realm_u}${spn or ''}*${checksum}${edata}" mode = 13100 if et == "23" else None return {"hashcat": line, "hashcat_mode": mode, "format": f"krb5tgs(etype={et})"} # --------------------------------------------------------------------------- # # SMTP AUTH is clear text with Base64 encoding only. # --------------------------------------------------------------------------- # def _b64(token: str) -> Optional[str]: try: return base64.b64decode(str(token).strip() + "===").decode("utf-8", "ignore") except Exception: return None def decode_smtp_auth_plain(token: str) -> Optional[tuple[Optional[str], Optional[str]]]: """Decode an AUTH PLAIN token into username and password.""" raw = _b64(token) if raw is None: return None parts = raw.split("\x00") if len(parts) >= 3: return parts[1] or None, parts[2] or None if len(parts) == 2: return parts[0] or None, parts[1] or None return None def decode_smtp_auth_login(user_token: Optional[str], pass_token: Optional[str]) -> tuple[Optional[str], Optional[str]]: """Decode the separately encoded AUTH LOGIN username and password.""" user = _b64(user_token) if user_token else None pw = _b64(pass_token) if pass_token else None return user, pw