|
| 1 | +# Parsing helpers for auth negotiation: NTLM challenges and TDS ERROR/INFO on MSSQL LOGIN7. |
| 2 | +# Original NTLM parsing from: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568 |
| 3 | + |
| 4 | +import struct |
| 5 | + |
| 6 | +from impacket import ntlm |
| 7 | +from impacket.smb3 import WIN_VERSIONS |
| 8 | +from impacket.tds import TDS_ERROR_TOKEN, TDS_INFO_TOKEN, TDS_INFO_ERROR |
| 9 | +import contextlib |
| 10 | + |
| 11 | + |
| 12 | +def parse_challenge(challange): |
| 13 | + target_info = { |
| 14 | + "hostname": None, |
| 15 | + "domain": None, |
| 16 | + "os_version": None |
| 17 | + } |
| 18 | + challange = ntlm.NTLMAuthChallenge(challange) |
| 19 | + av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]]) |
| 20 | + if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None: |
| 21 | + with contextlib.suppress(Exception): |
| 22 | + target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le") |
| 23 | + if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None: |
| 24 | + with contextlib.suppress(Exception): |
| 25 | + target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le") |
| 26 | + if "Version" in challange.fields: |
| 27 | + version = challange["Version"] |
| 28 | + if len(version) >= 4: |
| 29 | + major_version = version[0] |
| 30 | + minor_version = version[1] |
| 31 | + product_build = struct.unpack("<H", version[2:4])[0] |
| 32 | + if product_build in WIN_VERSIONS: |
| 33 | + target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}" |
| 34 | + else: |
| 35 | + target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}" |
| 36 | + return target_info |
| 37 | + |
| 38 | + |
| 39 | +def decode_tds_info_error_msgtext(data, offset): |
| 40 | + """Extract MsgText from a TDS ERROR (0xAA) or INFO (0xAB) token at *offset*. |
| 41 | +
|
| 42 | + Official spec: [MS-TDS] Tabular Data Stream Protocol (Microsoft Learn). |
| 43 | + Token layout per MS-TDS 2.2.7.9 (INFO) / 2.2.7.10 (ERROR): |
| 44 | + TokenType BYTE 0xAA | 0xAB |
| 45 | + Length USHORT LE byte count of the remaining fields |
| 46 | + Number LONG LE error / info number |
| 47 | + State BYTE |
| 48 | + Class BYTE severity |
| 49 | + MsgText US_VARCHAR (2-byte LE length prefix + UTF-16LE) |
| 50 | + ... (ServerName, ProcName, LineNumber follow but are unused here) |
| 51 | +
|
| 52 | + The minimum *Length* value for a valid token is 8: Number(4) + State(1) + |
| 53 | + Class(1) + MsgText length prefix(2, may be zero-length string). |
| 54 | +
|
| 55 | + References (Microsoft Learn, MS-TDS): |
| 56 | + INFO: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/284bb815-d083-4ed5-b33a-bdc2492e322b |
| 57 | + ERROR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/9805e9fa-1f8b-4cf8-8f78-8d2602228635 |
| 58 | + Data packet stream tokens: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/f79bb5b8-5919-439a-a696-48064b78b091 |
| 59 | + """ |
| 60 | + remaining = len(data) - offset |
| 61 | + if remaining < 3 or data[offset] not in (TDS_ERROR_TOKEN, TDS_INFO_TOKEN): |
| 62 | + return None |
| 63 | + |
| 64 | + # Length (USHORT LE) after TokenType, see MS-TDS INFO/ERROR links in docstring |
| 65 | + payload_len = int.from_bytes(data[offset + 1 : offset + 3], "little") |
| 66 | + |
| 67 | + if payload_len < 8 or remaining < 3 + payload_len: |
| 68 | + return None |
| 69 | + try: |
| 70 | + token = TDS_INFO_ERROR(data[offset:]) |
| 71 | + text = token["MsgText"].decode("utf-16le").strip() |
| 72 | + except Exception: |
| 73 | + return None |
| 74 | + return text or None |
| 75 | + |
| 76 | + |
| 77 | +def login7_integrated_auth_error_message(packet_data, data_after_login_header): |
| 78 | + """Scan raw LOGIN7 response buffers for the first ERROR/INFO message. |
| 79 | +
|
| 80 | + When a server does not support Integrated Windows Authentication it replies |
| 81 | + to the LOGIN7 NTLMSSP negotiate with a TDS error token instead of an |
| 82 | + NTLMSSP challenge. This helper locates the first ERROR (0xAA) or INFO |
| 83 | + (0xAB) token in either the full packet or the payload after the 3-byte |
| 84 | + LOGIN7 response header and returns its MsgText. |
| 85 | + """ |
| 86 | + token_markers = (TDS_ERROR_TOKEN, TDS_INFO_TOKEN) |
| 87 | + for buf in filter(None, (packet_data, data_after_login_header)): |
| 88 | + for offset in (i for i in range(len(buf)) if buf[i] in token_markers): |
| 89 | + msg = decode_tds_info_error_msgtext(buf, offset) |
| 90 | + if msg: |
| 91 | + return msg |
| 92 | + return None |
0 commit comments