Skip to content

Commit 6ada3ea

Browse files
committed
rename ntlm_parser to negotiate_parser, add TDS LOGIN7 helpers
1 parent 7147aa2 commit 6ada3ea

7 files changed

Lines changed: 98 additions & 65 deletions

File tree

netexec.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ a = Analysis(
4949
'nxc.helpers.bloodhound',
5050
'nxc.helpers.even6_parser',
5151
'nxc.helpers.msada_guids',
52-
'nxc.helpers.ntlm_parser',
52+
'nxc.helpers.negotiate_parser',
5353
'paramiko',
5454
'pefile',
5555
'pypsrp.client',

nxc/helpers/negotiate_parser.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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

nxc/helpers/ntlm_parser.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

nxc/protocols/ldap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
4848
from nxc.protocols.ldap.kerberos import KerberosAttacks
4949
from nxc.parsers.ldap_results import parse_result_attributes
50-
from nxc.helpers.ntlm_parser import parse_challenge
50+
from nxc.helpers.negotiate_parser import parse_challenge
5151
from nxc.paths import CONFIG_PATH
5252

5353
ldap_error_status = {

nxc/protocols/mssql.py

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from nxc.helpers.misc import gen_random_string
1010
from nxc.logger import NXCAdapter
1111
from nxc.helpers.bloodhound import add_user_bh
12-
from nxc.helpers.ntlm_parser import parse_challenge
12+
from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message
1313
from nxc.helpers.powershell import create_ps_command
1414
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
1515

@@ -78,31 +78,6 @@ def wrapper(self, *args, **kwargs):
7878
return func(self, *args, **kwargs)
7979
return wrapper
8080

81-
@staticmethod
82-
def decode_tds_info_error_msgtext(data, offset):
83-
remaining = len(data) - offset
84-
if remaining < 3 or data[offset] not in (TDS_ERROR_TOKEN, TDS_INFO_TOKEN):
85-
return None
86-
payload_len = int.from_bytes(data[offset + 1 : offset + 3], "little")
87-
if payload_len < 8 or remaining < 3 + payload_len:
88-
return None
89-
try:
90-
token = tds.TDS_INFO_ERROR(data[offset:])
91-
text = token["MsgText"].decode("utf-16le").strip()
92-
except Exception:
93-
return None
94-
return text or None
95-
96-
@staticmethod
97-
def login7_integrated_auth_error_message(packet_data, data_after_login_header):
98-
token_markers = (TDS_ERROR_TOKEN, TDS_INFO_TOKEN)
99-
for buf in filter(None, (packet_data, data_after_login_header)):
100-
for offset in (i for i in range(len(buf)) if buf[i] in token_markers):
101-
msg = mssql.decode_tds_info_error_msgtext(buf, offset)
102-
if msg:
103-
return msg
104-
return None
105-
10681
def check_if_admin(self):
10782
self.admin_privs = False
10883
try:
@@ -162,7 +137,7 @@ def enum_host_info(self):
162137
return False
163138
else:
164139
if not challenge.startswith(b"NTLMSSP\x00"):
165-
error_msg = self.login7_integrated_auth_error_message(login_response, challenge)
140+
error_msg = login7_integrated_auth_error_message(login_response, challenge)
166141
detail = f": {error_msg}" if error_msg else ""
167142
self.logger.fail(f"Server does not support Integrated Windows Authentication{detail}")
168143
else:

nxc/protocols/winrm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from nxc.helpers.bloodhound import add_user_bh
2323
from nxc.helpers.logger import highlight
2424
from nxc.helpers.misc import gen_random_string
25-
from nxc.helpers.ntlm_parser import parse_challenge
25+
from nxc.helpers.negotiate_parser import parse_challenge
2626
from nxc.logger import NXCAdapter
2727
from nxc.paths import TMP_PATH
2828

nxc/protocols/wmi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from io import StringIO
33

4-
from nxc.helpers.ntlm_parser import parse_challenge
4+
from nxc.helpers.negotiate_parser import parse_challenge
55
from nxc.config import process_secret
66
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
77
from nxc.logger import NXCAdapter

0 commit comments

Comments
 (0)