"""NTLMSSP Type 3 (Authenticate) message parser. Standalone module shared between any honeypot template that wants to land NTLM credentials in the universal :class:`Credential` table. Currently consumed by the SMB and RDP-NLA templates. The parser is intentionally narrow: only :func:`parse_type3` is public, and it reads a single Type 3 buffer (the bytes starting with the ``NTLMSSP\\0`` signature). Callers handle SPNEGO unwrapping, SMB SessionSetup framing, RDP/CredSSP TSRequest parsing, etc. Reference: MS-NLMP §2.2.1.3 (AUTHENTICATE_MESSAGE). Cred-shape mapping for the universal Credential model: - ``principal`` = ``"DOMAIN\\username"`` when domain present, else bare username. Both decoded UTF-16-LE when NEGOTIATE_UNICODE is set in the message flags (it always is in modern clients). - ``secret_kind`` = ``"ntlmssp_v2"`` when the NtChallengeResponse is ≥ 24 bytes (NTLMv2 carries variable-length blob ≥ 16+8 bytes), ``"ntlmssp_v1"`` for the legacy 24-byte fixed response. - ``secret_b64`` = base64 of the entire NtChallengeResponse bytes. This is the canonical "hashcat -m 5600" (NTLMv2) or "-m 5500" (NTLMv1) input. """ from __future__ import annotations import base64 import struct from typing import Optional NTLMSSP_SIG = b"NTLMSSP\x00" NEGOTIATE_UNICODE = 0x00000001 def find_ntlmssp(buf: bytes) -> int: """Return the offset of the NTLMSSP signature in ``buf`` or -1. Useful for callers that have a SPNEGO-wrapped or SMB-embedded blob and want to skip straight to the inner Type 1/2/3 message without walking the outer ASN.1. """ return buf.find(NTLMSSP_SIG) def _read_field(buf: bytes, off: int) -> tuple[int, int, int]: """Read an NTLMSSP field record: (Len, MaxLen, BufferOffset).""" if off + 8 > len(buf): return 0, 0, 0 f_len, f_max, f_off = struct.unpack_from(" bytes: end = off + length if off < 0 or end > len(buf) or length < 0: return b"" return buf[off:end] def _decode_str(raw: bytes, unicode: bool) -> str: if unicode: return raw.decode("utf-16-le", errors="replace") return raw.decode("ascii", errors="replace") def parse_type3(blob: bytes) -> Optional[dict]: """Parse an NTLMSSP Type 3 (AUTHENTICATE_MESSAGE) buffer. Returns a dict with the universal credential SD shape ready to spread into a ``_log(...)`` call:: { "username": "alice", # service-specific identity "domain": "ACME", # domain (may be empty) "principal": "ACME\\\\alice", # hoisted column "secret_kind": "ntlmssp_v2", # or _v1 "secret_printable": "", # NT response in hex "secret_b64": "", # NT response, lossless } Returns ``None`` when ``blob`` is malformed or not a Type 3. """ if len(blob) < 32 or not blob.startswith(NTLMSSP_SIG): return None msg_type = struct.unpack_from("