Skip to content

Commit 6584260

Browse files
committed
Resolve merge conflicts and sync fork with upstream
2 parents 4a4fd9e + 67d90e0 commit 6584260

43 files changed

Lines changed: 2130 additions & 1128 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/connection.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import contextlib
66

77
from os.path import isfile
8-
from threading import BoundedSemaphore
8+
from threading import BoundedSemaphore, Lock
99
from functools import wraps
1010
from time import sleep
1111
from ipaddress import ip_address
@@ -23,8 +23,10 @@
2323
from nxc.helpers.pfx import pfx_auth
2424

2525
from impacket.dcerpc.v5 import transport
26+
from impacket.krb5.ccache import CCache
2627

2728
sem = BoundedSemaphore(1)
29+
fail_lock = Lock()
2830
global_failed_logins = 0
2931
user_failed_logins = {}
3032

@@ -315,26 +317,28 @@ def call_modules(self):
315317
def inc_failed_login(self, username):
316318
global global_failed_logins, user_failed_logins
317319

318-
if username not in user_failed_logins:
319-
user_failed_logins[username] = 0
320+
with fail_lock:
321+
if username not in user_failed_logins:
322+
user_failed_logins[username] = 0
320323

321-
user_failed_logins[username] += 1
322-
global_failed_logins += 1
323-
self.failed_logins += 1
324+
user_failed_logins[username] += 1
325+
global_failed_logins += 1
326+
self.failed_logins += 1
324327

325328
def over_fail_limit(self, username):
326329
global global_failed_logins, user_failed_logins
327330

328-
if global_failed_logins == self.args.gfail_limit:
329-
return True
331+
with fail_lock:
332+
if global_failed_logins == self.args.gfail_limit:
333+
return True
330334

331-
if self.failed_logins == self.args.fail_limit:
332-
return True
335+
if self.failed_logins == self.args.fail_limit:
336+
return True
333337

334-
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
335-
return True
338+
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
339+
return True
336340

337-
return False
341+
return False
338342

339343
def query_db_creds(self):
340344
"""Queries the database for credentials to be used for authentication.
@@ -481,8 +485,6 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
481485
- NTLM-hash (/kerberos)
482486
- AES-key
483487
"""
484-
if self.over_fail_limit(username):
485-
return False
486488
if self.args.continue_on_success and owned:
487489
return False
488490

@@ -498,6 +500,8 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
498500
sleep(value)
499501

500502
with sem:
503+
if self.over_fail_limit(username):
504+
return False
501505
if cred_type == "plaintext":
502506
if self.kerberos:
503507
self.logger.debug("Trying to authenticate using Kerberos")
@@ -553,7 +557,7 @@ def login(self):
553557
if self.args.use_kcache:
554558
self.logger.debug("Trying to authenticate using Kerberos cache")
555559
with sem:
556-
username = self.args.username[0] if len(self.args.username) else ""
560+
username = self.args.username[0] if len(self.args.username) else CCache.parseFile()[1]
557561
password = self.args.password[0] if len(self.args.password) else ""
558562
self.kerberos_login(self.domain, username, password, "", "", self.kdcHost, True)
559563
self.logger.info("Successfully authenticated using Kerberos cache")

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/helpers/pfx.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,10 @@ def pfx_auth(self):
495495
req = ini.build_asreq(self.domain, username)
496496
self.logger.info("Requesting TGT")
497497

498+
if not self.kdcHost:
499+
self.logger.fail(f"Could not resolve KDC host for domain {self.domain}. Use --kdcHost to specify the domain controller IP")
500+
return False
501+
498502
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
499503
try:
500504
res = sock.sendrecv(req)

0 commit comments

Comments
 (0)