From 4b27406bb098f276191bf8991d740e0718fa677d Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Wed, 11 Jun 2025 23:01:54 +0200 Subject: [PATCH 01/10] Added support for SQL Browser enumeration --- nxc/protocols/mssql.py | 45 +++++++++++++++++++++++++++++++ nxc/protocols/mssql/proto_args.py | 1 + 2 files changed, 46 insertions(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index dec1be4a28..35f259e482 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -53,6 +53,51 @@ def proto_logger(self): } ) + def proto_flow(self): + self.logger.debug("Kicking off MSSQL proto_flow") + + if self.args.no_sqlbrowser == False: + sqlbrowser_instance_port = self.enum_sqlbrowser() + # If port has not been manually enforced, connect to instance port instead + if sqlbrowser_instance_port != 0 and self.port == 1433: + self.port = sqlbrowser_instance_port + + # Proceed with normal proto_flow + super().proto_flow() + + def enum_sqlbrowser(self): + try: + conn = tds.MSSQL(self.host, self.port, self.remoteName) + + logger = NXCAdapter( + extra={ + "protocol": "SQLBROWSER", + "host": self.host, + "port": "1434", + "hostname": "None" + } + ) + + logger.info("Checking for SQL browser presence") + instances = conn.getInstances() + if len(instances) > 0: + logger.success('SQL Browser is enabled') + for index, instance in enumerate(instances): + logger.success(f"#{index} Instance {instance['InstanceName']} (port:{instance['tcp']}) (clustered:{instance['IsClustered']}) (version:{instance['Version']})") + + if len(instances) == 1: + port = instances[0]['tcp'] + logger.info(f'Redirecting to port {port}') + self.sqlbrowser_redirect = True + return instances[0]['tcp'] + + else: + logger.success('Multiple instances reported, specify port manually using --port ') + + except Exception as e: + self.logger.debug(f"Error connecting to SQLBROWSER service on host: {self.host}, reason: {e}") + return 0 + def create_conn_obj(self): try: self.conn = tds.MSSQL(self.host, self.port, self.remoteName) diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index b810ccea1b..fc9021422a 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -31,4 +31,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--no-sqlbrowser", action="store_true", default=False, help="Do not request SQL browser information (udp 1434)") return parser \ No newline at end of file From 4e50d7644b09c92579bbd805b22949d96f27c2e1 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Wed, 11 Jun 2025 23:15:08 +0200 Subject: [PATCH 02/10] Add e2e for mssql --no-sqlbrowser --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 8b3e29718a..33955c33c6 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -224,6 +224,7 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-brute +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --no-sqlbrowser # Should behave normally ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From 07f867757464dbc4e70d6dc14a785d7e2c464757 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Thu, 12 Jun 2025 00:07:32 +0200 Subject: [PATCH 03/10] Ruff --- nxc/protocols/mssql.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 35f259e482..c8980e7d06 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -56,7 +56,7 @@ def proto_logger(self): def proto_flow(self): self.logger.debug("Kicking off MSSQL proto_flow") - if self.args.no_sqlbrowser == False: + if not self.args.no_sqlbrowser: sqlbrowser_instance_port = self.enum_sqlbrowser() # If port has not been manually enforced, connect to instance port instead if sqlbrowser_instance_port != 0 and self.port == 1433: @@ -64,7 +64,7 @@ def proto_flow(self): # Proceed with normal proto_flow super().proto_flow() - + def enum_sqlbrowser(self): try: conn = tds.MSSQL(self.host, self.port, self.remoteName) @@ -81,18 +81,18 @@ def enum_sqlbrowser(self): logger.info("Checking for SQL browser presence") instances = conn.getInstances() if len(instances) > 0: - logger.success('SQL Browser is enabled') + logger.success("SQL Browser is enabled") for index, instance in enumerate(instances): logger.success(f"#{index} Instance {instance['InstanceName']} (port:{instance['tcp']}) (clustered:{instance['IsClustered']}) (version:{instance['Version']})") if len(instances) == 1: - port = instances[0]['tcp'] - logger.info(f'Redirecting to port {port}') + port = instances[0]["tcp"] + logger.info(f"Redirecting to port {port}") self.sqlbrowser_redirect = True - return instances[0]['tcp'] + return instances[0]["tcp"] else: - logger.success('Multiple instances reported, specify port manually using --port ') + logger.success("Multiple instances reported, specify port manually using --port ") except Exception as e: self.logger.debug(f"Error connecting to SQLBROWSER service on host: {self.host}, reason: {e}") From 4d17f02d6577d5e0f7c28833d1e67f63b9964a83 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Fri, 13 Jun 2025 09:41:21 +0000 Subject: [PATCH 04/10] Moved enumeration to create_conn_obj + namedpipe detection --- nxc/protocols/mssql.py | 70 +++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index c8980e7d06..e6fef11889 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -53,54 +53,54 @@ def proto_logger(self): } ) - def proto_flow(self): - self.logger.debug("Kicking off MSSQL proto_flow") - - if not self.args.no_sqlbrowser: - sqlbrowser_instance_port = self.enum_sqlbrowser() - # If port has not been manually enforced, connect to instance port instead - if sqlbrowser_instance_port != 0 and self.port == 1433: - self.port = sqlbrowser_instance_port + def enum_sqlbrowser(self): + # SQL Browser enumeration - # Proceed with normal proto_flow - super().proto_flow() + self.mssql_instances = self.conn.getInstances() - def enum_sqlbrowser(self): - try: - conn = tds.MSSQL(self.host, self.port, self.remoteName) + if len(self.mssql_instances) > 0: - logger = NXCAdapter( + sqlbrowser_logger = NXCAdapter( extra={ "protocol": "SQLBROWSER", "host": self.host, "port": "1434", - "hostname": "None" + "hostname": self.mssql_instances[0].get("ServerName") } ) - logger.info("Checking for SQL browser presence") - instances = conn.getInstances() - if len(instances) > 0: - logger.success("SQL Browser is enabled") - for index, instance in enumerate(instances): - logger.success(f"#{index} Instance {instance['InstanceName']} (port:{instance['tcp']}) (clustered:{instance['IsClustered']}) (version:{instance['Version']})") - - if len(instances) == 1: - port = instances[0]["tcp"] - logger.info(f"Redirecting to port {port}") - self.sqlbrowser_redirect = True - return instances[0]["tcp"] - - else: - logger.success("Multiple instances reported, specify port manually using --port ") - - except Exception as e: - self.logger.debug(f"Error connecting to SQLBROWSER service on host: {self.host}, reason: {e}") - return 0 + sqlbrowser_logger.debug(self.mssql_instances) + + # Find the first np or tcp instance + valid_instance = None + for index, instance in enumerate(self.mssql_instances): + sqlbrowser_logger.success(f"#{index} {instance.get('InstanceName')} (port:{instance.get('tcp', 'None')}) (np:{instance.get('np', 'None')}) (version:{instance.get('Version')})") + if (not valid_instance and instance.get("np")) or instance.get("tcp"): + valid_instance = instance + + if not valid_instance: + sqlbrowser_logger.fail(f"SQL Browser detected {len(self.mssql_instances)} instances but none of them are exposed to the network.") + return + + # Only fallback when TCP is detected, until an implementation for np is done + if valid_instance.get("tcp"): + port = valid_instance.get("tcp") + sqlbrowser_logger.success(f"Falling back to instance #{self.mssql_instances.index(valid_instance)} on port {port}") + self.port = port + # Reset proto_logger to update the port + self.proto_logger() + self.conn = tds.MSSQL(self.host, self.port, self.remoteName) + + elif valid_instance.get("np"): + pass def create_conn_obj(self): try: self.conn = tds.MSSQL(self.host, self.port, self.remoteName) + + if not self.args.no_sqlbrowser and not self.is_mssql: + self.enum_sqlbrowser() + # Default has not timeout option in tds.MSSQL.connect() function, let rewrite it. af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0] sock = socket.socket(af, socktype, proto) @@ -154,7 +154,7 @@ def enum_host_info(self): login["Length"] = len(login.getData()) # Get number of mssql instance - self.mssql_instances = self.conn.getInstances(0) + self.mssql_instances = self.conn.getInstances() # Send the NTLMSSP Negotiate or SQL Auth Packet self.conn.sendTDS(tds.TDS_LOGIN7, login.getData()) From 95f9d451381aa78ada5f7dfdec0febae8d8d4d5f Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Fri, 13 Jun 2025 09:52:21 +0000 Subject: [PATCH 05/10] Actually, ignore named pipes for now, just report them. --- nxc/protocols/mssql.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index e6fef11889..49732d7592 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -75,24 +75,20 @@ def enum_sqlbrowser(self): valid_instance = None for index, instance in enumerate(self.mssql_instances): sqlbrowser_logger.success(f"#{index} {instance.get('InstanceName')} (port:{instance.get('tcp', 'None')}) (np:{instance.get('np', 'None')}) (version:{instance.get('Version')})") - if (not valid_instance and instance.get("np")) or instance.get("tcp"): + if not valid_instance and instance.get("tcp"): valid_instance = instance if not valid_instance: - sqlbrowser_logger.fail(f"SQL Browser detected {len(self.mssql_instances)} instances but none of them are exposed to the network.") + sqlbrowser_logger.fail(f"SQL Browser detected {len(self.mssql_instances)} instances but none of them is exposed to TCP.") return # Only fallback when TCP is detected, until an implementation for np is done - if valid_instance.get("tcp"): - port = valid_instance.get("tcp") - sqlbrowser_logger.success(f"Falling back to instance #{self.mssql_instances.index(valid_instance)} on port {port}") - self.port = port - # Reset proto_logger to update the port - self.proto_logger() - self.conn = tds.MSSQL(self.host, self.port, self.remoteName) - - elif valid_instance.get("np"): - pass + port = valid_instance.get("tcp") + sqlbrowser_logger.success(f"Falling back to instance #{self.mssql_instances.index(valid_instance)} on port {port}") + self.port = port + # Reset proto_logger to update the port + self.proto_logger() + self.conn = tds.MSSQL(self.host, self.port, self.remoteName) def create_conn_obj(self): try: From a8f6fb981d5b7b91d0dd9ee9dbbed61e31d38602 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Wed, 1 Apr 2026 21:54:53 +0200 Subject: [PATCH 06/10] Improved sql browser enumeration and connection behavior --- nxc/protocols/mssql.py | 130 +++++++++++++++++++----------- nxc/protocols/mssql/proto_args.py | 3 +- 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 1e29fba00e..d13f79369a 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -44,8 +44,12 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.is_mssql = False + self.sqlbrowser_enabled = False connection.__init__(self, args, db, host) + + if self.args.instance is not None: + self.instance_connect(self.args.instance) def proto_logger(self): self.logger = NXCAdapter( @@ -57,60 +61,18 @@ def proto_logger(self): } ) - def enum_sqlbrowser(self): - # SQL Browser enumeration - - self.mssql_instances = self.conn.getInstances() - - if len(self.mssql_instances) > 0: - - sqlbrowser_logger = NXCAdapter( - extra={ - "protocol": "SQLBROWSER", - "host": self.host, - "port": "1434", - "hostname": self.mssql_instances[0].get("ServerName") - } - ) - - sqlbrowser_logger.debug(self.mssql_instances) - - # Find the first np or tcp instance - valid_instance = None - for index, instance in enumerate(self.mssql_instances): - sqlbrowser_logger.success(f"#{index} {instance.get('InstanceName')} (port:{instance.get('tcp', 'None')}) (np:{instance.get('np', 'None')}) (version:{instance.get('Version')})") - if not valid_instance and instance.get("tcp"): - valid_instance = instance - - if not valid_instance: - sqlbrowser_logger.fail(f"SQL Browser detected {len(self.mssql_instances)} instances but none of them is exposed to TCP.") - return - - # Only fallback when TCP is detected, until an implementation for np is done - port = valid_instance.get("tcp") - sqlbrowser_logger.success(f"Falling back to instance #{self.mssql_instances.index(valid_instance)} on port {port}") - self.port = port - # Reset proto_logger to update the port - self.proto_logger() - self.conn = tds.MSSQL(self.host, self.port, self.remoteName) - def create_conn_obj(self): try: + # Connects to default port or --port self.conn = tds.MSSQL(self.host, self.port, self.remoteName) + self.conn.connect(self.args.mssql_timeout) - if not self.args.no_sqlbrowser and not self.is_mssql: - self.enum_sqlbrowser() - - # Default has not timeout option in tds.MSSQL.connect() function, let rewrite it. - af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0] - sock = socket.socket(af, socktype, proto) - sock.settimeout(self.args.mssql_timeout) - sock.connect(sa) - self.conn.socket = sock - if not self.is_mssql: - self.conn.preLogin() except Exception as e: self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}") + + if self.enum_sqlbrowser(): + self.list_instances() + with contextlib.suppress(Exception): self.conn.disconnect() return False @@ -118,6 +80,23 @@ def create_conn_obj(self): self.is_mssql = True return True + def enum_sqlbrowser(self): + self.logger.debug("Trying to enumerate SQL browser") + # Ignore broadcast targets (UDP) + if self.host.endswith(".255"): + self.logger.debug("Target is a broadcast address, skipping SQL browser enumeration") + self.sqlbrowser_enabled = False + return False + else: + self.mssql_instances = self.conn.getInstances(2) + if len(self.mssql_instances) > 0: + self.sqlbrowser_enabled = True + self.logger.display("SQL browser is enabled.") + return True + else: + self.sqlbrowser_enabled = False + return False + def reconnect_mssql(func): def wrapper(self, *args, **kwargs): with contextlib.suppress(Exception): @@ -166,6 +145,8 @@ def enum_host_info(self): # Get number of mssql instance self.mssql_instances = self.conn.getInstances() + if len(self.conn.getInstances(2)) > 0: + self.sqlbrowser_enabled = True # Send the NTLMSSP Negotiate or SQL Auth Packet self.conn.sendTDS(tds.TDS_LOGIN7, login.getData()) @@ -203,7 +184,8 @@ def enum_host_info(self): def print_host_info(self): encryption = colored(f"EncryptionReq:{self.encryption}", host_info_colors[0 if self.encryption else 1], attrs=["bold"]) - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption})") + sql_browser = colored(f"SQLBrowser:{'True' if self.sqlbrowser_enabled else 'False'}", host_info_colors[0 if self.sqlbrowser_enabled else 1], attrs=["bold"]) + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) ({sql_browser})") @reconnect_mssql def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): @@ -653,3 +635,53 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + def list_instances(self, enumerate_index=None): + if self.sqlbrowser_enabled is False: + self.logger.fail("MSSQL browser is not enabled, cannot enumerate...") + return + + self.logger.debug("Enumerating MSSQL browser") + if len(self.mssql_instances) > 0: + # Get information about instances + for index, instance in enumerate(self.mssql_instances): + if enumerate_index is not None and index != enumerate_index: + continue + if self.args.list_instances: + self.log_instance(index, instance) + elif self.args.instance is None or self.args.instance == enumerate_index: + self.instance_connect(index) + else: + self.logger.fail("No instance to enumerate") + + def log_instance(self, index, instance): + instance_name = instance.get("InstanceName") + instance_port = instance.get("tcp", None) + instance_np = instance.get("np", None) + instance_version = instance.get("Version", None) + self.logger.success(f"#{index} {instance_name} (port:{instance_port}) (np:{instance_np}) (version:{instance_version})") + + def instance_connect(self, instance_index): + if not self.mssql_instances: + return + if 0 <= instance_index < len(self.mssql_instances): + instance = self.mssql_instances[instance_index] + self.logger.debug(f'Connecting to instance index: {instance_index} with details: {instance}') + instance_name = instance.get("InstanceName", None) + instance_port = instance.get("tcp", None) + instance_np = instance.get("np", None) + instance_arguments = self.args + # Drop instances and list-instances arguments + instance_arguments.instance = None + instance_arguments.list_instances = False + # Case #1, instance is listening on a port + if instance_port: + instance_arguments.port = instance_port + new_connection = self.__init__(instance_arguments, self.db, self.host) + # Case #2, instance is listening on a named pipe + elif instance_np: + self.logger.fail(f"Named pipe connections are not supported yet, cannot connect to {instance_np}") + pass + + else: + self.logger.fail(f"Invalid instance index. Please choose an index between 0 and {len(self.mssql_instances) - 1}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index ba132bb45e..cfd53a4e71 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,5 +36,6 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") - mapping_enum_group.add_argument("--no-sqlbrowser", action="store_true", default=False, help="Do not request SQL browser information (udp 1434)") + mapping_enum_group.add_argument("--list-instances", action="store_true", default=False, help="Enumerate MSSQL instances via SQL Browser") + mapping_enum_group.add_argument("--instance", type=int, help="Connect to a specific instance number (use --list-instances to enumerate)") return parser From 241e07b6610dcea604b0ac1c00d5ab0913215c14 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Thu, 2 Apr 2026 15:59:38 +0200 Subject: [PATCH 07/10] Changed to --browser and refactored behavior --- nxc/protocols/mssql.py | 160 +++++++++++++++--------------- nxc/protocols/mssql/proto_args.py | 3 +- 2 files changed, 83 insertions(+), 80 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index d13f79369a..2df316db89 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -45,11 +45,34 @@ def __init__(self, args, db, host): self.nthash = "" self.is_mssql = False self.sqlbrowser_enabled = False + self.sqlbrowser_logger = NXCAdapter( + extra={ + "protocol": "SQLBROWSER", + "host": host, + "port": "1434", + "hostname": "None", + } + ) - connection.__init__(self, args, db, host) - - if self.args.instance is not None: - self.instance_connect(self.args.instance) + # --browser + if args.browser: + args.port = 0 # Override port number with dummy one + connection.__init__(self, args, db, host) + self.discover_sqlbrowser() + + if args.browser == "all": + for instance in self.mssql_instances: + self.instance_connect(instance) + else: + try: + index = int(args.browser) + instance = self.mssql_instances[index] + self.instance_connect(instance) + except ValueError: + self.sqlbrowser_logger.fail("Instance argument must be an integer index or 'all'") + return + else: + connection.__init__(self, args, db, host) def proto_logger(self): self.logger = NXCAdapter( @@ -63,41 +86,20 @@ def proto_logger(self): def create_conn_obj(self): try: - # Connects to default port or --port + # TODO: handle named pipes once supported by impacket self.conn = tds.MSSQL(self.host, self.port, self.remoteName) self.conn.connect(self.args.mssql_timeout) - except Exception as e: self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}") - - if self.enum_sqlbrowser(): - self.list_instances() - with contextlib.suppress(Exception): self.conn.disconnect() return False else: self.is_mssql = True return True - - def enum_sqlbrowser(self): - self.logger.debug("Trying to enumerate SQL browser") - # Ignore broadcast targets (UDP) - if self.host.endswith(".255"): - self.logger.debug("Target is a broadcast address, skipping SQL browser enumeration") - self.sqlbrowser_enabled = False - return False - else: - self.mssql_instances = self.conn.getInstances(2) - if len(self.mssql_instances) > 0: - self.sqlbrowser_enabled = True - self.logger.display("SQL browser is enabled.") - return True - else: - self.sqlbrowser_enabled = False - return False - + def reconnect_mssql(func): + def wrapper(self, *args, **kwargs): with contextlib.suppress(Exception): self.conn.disconnect() @@ -105,6 +107,13 @@ def wrapper(self, *args, **kwargs): return func(self, *args, **kwargs) return wrapper + def reconnect_sqlbrowser(func): + def wrapper(self, *args, **kwargs): + with contextlib.suppress(Exception): + self.sqlbrowser_conn.disconnect() + return func(self, *args, **kwargs) + return wrapper + def check_if_admin(self): self.admin_privs = False try: @@ -143,11 +152,6 @@ def enum_host_info(self): login["SSPI"] = auth.getData() login["Length"] = len(login.getData()) - # Get number of mssql instance - self.mssql_instances = self.conn.getInstances() - if len(self.conn.getInstances(2)) > 0: - self.sqlbrowser_enabled = True - # Send the NTLMSSP Negotiate or SQL Auth Packet self.conn.sendTDS(tds.TDS_LOGIN7, login.getData()) @@ -168,7 +172,8 @@ def enum_host_info(self): self.hostname = ntlm_info["hostname"] self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname - self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),) + + self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, ",".join(str(instance for instance in self.mssql_instances))) if self.args.domain: self.domain = self.args.domain @@ -184,8 +189,7 @@ def enum_host_info(self): def print_host_info(self): encryption = colored(f"EncryptionReq:{self.encryption}", host_info_colors[0 if self.encryption else 1], attrs=["bold"]) - sql_browser = colored(f"SQLBrowser:{'True' if self.sqlbrowser_enabled else 'False'}", host_info_colors[0 if self.sqlbrowser_enabled else 1], attrs=["bold"]) - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) ({sql_browser})") + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption})") @reconnect_mssql def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): @@ -636,52 +640,52 @@ def lsa(self): LSA.dumpCachedHashes() LSA.dumpSecrets() - def list_instances(self, enumerate_index=None): - if self.sqlbrowser_enabled is False: - self.logger.fail("MSSQL browser is not enabled, cannot enumerate...") - return - - self.logger.debug("Enumerating MSSQL browser") - if len(self.mssql_instances) > 0: - # Get information about instances - for index, instance in enumerate(self.mssql_instances): - if enumerate_index is not None and index != enumerate_index: - continue - if self.args.list_instances: - self.log_instance(index, instance) - elif self.args.instance is None or self.args.instance == enumerate_index: - self.instance_connect(index) - else: - self.logger.fail("No instance to enumerate") - def log_instance(self, index, instance): instance_name = instance.get("InstanceName") instance_port = instance.get("tcp", None) instance_np = instance.get("np", None) instance_version = instance.get("Version", None) - self.logger.success(f"#{index} {instance_name} (port:{instance_port}) (np:{instance_np}) (version:{instance_version})") + self.sqlbrowser_logger.success(f"#{index} {instance_name} (port:{instance_port}) (np:{instance_np}) (version:{instance_version})") - def instance_connect(self, instance_index): - if not self.mssql_instances: - return - if 0 <= instance_index < len(self.mssql_instances): - instance = self.mssql_instances[instance_index] - self.logger.debug(f'Connecting to instance index: {instance_index} with details: {instance}') - instance_name = instance.get("InstanceName", None) - instance_port = instance.get("tcp", None) - instance_np = instance.get("np", None) - instance_arguments = self.args - # Drop instances and list-instances arguments - instance_arguments.instance = None - instance_arguments.list_instances = False - # Case #1, instance is listening on a port - if instance_port: - instance_arguments.port = instance_port - new_connection = self.__init__(instance_arguments, self.db, self.host) - # Case #2, instance is listening on a named pipe - elif instance_np: - self.logger.fail(f"Named pipe connections are not supported yet, cannot connect to {instance_np}") - pass - + def discover_sqlbrowser(self): + self.sqlbrowser_conn = tds.MSSQL(self.host, 0, self.remoteName) + # No need to start the connection, we just need the tds object + + # Ignore broadcast targets (UDP) + if self.host.endswith(".255"): + self.sqlbrowser_logger.debug("Target is a broadcast address, skipping SQL browser enumeration") + self.sqlbrowser_enabled = False else: - self.logger.fail(f"Invalid instance index. Please choose an index between 0 and {len(self.mssql_instances) - 1}") + self.sqlbrowser_logger.debug('Listing SQL browser instances') + self.mssql_instances = self.sqlbrowser_conn.getInstances(2) + if len(self.mssql_instances) > 0: + self.sqlbrowser_enabled = True + self.sqlbrowser_logger.success("SQL browser is enabled.") + for index, instance in enumerate(self.mssql_instances): + if self.args.browser == "all" or self.args.browser.isdigit() and int(self.args.browser) == index: + self.log_instance(index, instance) + else: + self.sqlbrowser_enabled = False + + self.sqlbrowser_conn.disconnect() + + return self.sqlbrowser_enabled + + @reconnect_sqlbrowser + def instance_connect(self, instance): + self.sqlbrowser_logger.debug(f'instance_connect to {instance}') + instance_name = instance.get("InstanceName", None) + instance_port = instance.get("tcp", None) + instance_np = instance.get("np", None) + instance_arguments = self.args + # Drop instances and list-instances arguments + instance_arguments.instance = None + instance_arguments.browser = False + # Case #1, instance is listening on a port + if instance_port: + instance_arguments.port = instance_port + new_connection = self.__init__(instance_arguments, self.db, self.host) + # Case #2, instance is listening on a named pipe + elif instance_np: + self.sqlbrowser_logger.fail(f"Named pipe connections are not supported yet, cannot connect to {instance_np}") + pass diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index cfd53a4e71..3c9326a39b 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,6 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") - mapping_enum_group.add_argument("--list-instances", action="store_true", default=False, help="Enumerate MSSQL instances via SQL Browser") - mapping_enum_group.add_argument("--instance", type=int, help="Connect to a specific instance number (use --list-instances to enumerate)") + mapping_enum_group.add_argument("--browser", type=str, nargs='?', const="all", help="Enumerate or connect to MSSQL instances via SQL Browser. Use --browser to connect to a specific instance, or --browser all (default) to connect to all instances") return parser From 3ca32022de4de83970f6be5b55cf1c907f8c770e Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Thu, 2 Apr 2026 16:06:37 +0200 Subject: [PATCH 08/10] ruff fix --- nxc/protocols/mssql.py | 17 ++++++++--------- nxc/protocols/mssql/proto_args.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 2df316db89..c85ea5a6b5 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -56,7 +56,7 @@ def __init__(self, args, db, host): # --browser if args.browser: - args.port = 0 # Override port number with dummy one + args.port = 0 # Override port number with dummy one connection.__init__(self, args, db, host) self.discover_sqlbrowser() @@ -70,7 +70,7 @@ def __init__(self, args, db, host): self.instance_connect(instance) except ValueError: self.sqlbrowser_logger.fail("Instance argument must be an integer index or 'all'") - return + return else: connection.__init__(self, args, db, host) @@ -97,7 +97,7 @@ def create_conn_obj(self): else: self.is_mssql = True return True - + def reconnect_mssql(func): def wrapper(self, *args, **kwargs): @@ -650,30 +650,30 @@ def log_instance(self, index, instance): def discover_sqlbrowser(self): self.sqlbrowser_conn = tds.MSSQL(self.host, 0, self.remoteName) # No need to start the connection, we just need the tds object - + # Ignore broadcast targets (UDP) if self.host.endswith(".255"): self.sqlbrowser_logger.debug("Target is a broadcast address, skipping SQL browser enumeration") self.sqlbrowser_enabled = False else: - self.sqlbrowser_logger.debug('Listing SQL browser instances') + self.sqlbrowser_logger.debug("Listing SQL browser instances") self.mssql_instances = self.sqlbrowser_conn.getInstances(2) if len(self.mssql_instances) > 0: self.sqlbrowser_enabled = True self.sqlbrowser_logger.success("SQL browser is enabled.") for index, instance in enumerate(self.mssql_instances): - if self.args.browser == "all" or self.args.browser.isdigit() and int(self.args.browser) == index: + if self.args.browser == "all" or (self.args.browser.isdigit() and int(self.args.browser) == index): self.log_instance(index, instance) else: self.sqlbrowser_enabled = False - + self.sqlbrowser_conn.disconnect() return self.sqlbrowser_enabled @reconnect_sqlbrowser def instance_connect(self, instance): - self.sqlbrowser_logger.debug(f'instance_connect to {instance}') + self.sqlbrowser_logger.debug(f"instance_connect to {instance}") instance_name = instance.get("InstanceName", None) instance_port = instance.get("tcp", None) instance_np = instance.get("np", None) @@ -688,4 +688,3 @@ def instance_connect(self, instance): # Case #2, instance is listening on a named pipe elif instance_np: self.sqlbrowser_logger.fail(f"Named pipe connections are not supported yet, cannot connect to {instance_np}") - pass diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 3c9326a39b..3da9c1ff6f 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,5 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") - mapping_enum_group.add_argument("--browser", type=str, nargs='?', const="all", help="Enumerate or connect to MSSQL instances via SQL Browser. Use --browser to connect to a specific instance, or --browser all (default) to connect to all instances") + mapping_enum_group.add_argument("--browser", type=str, nargs="?", const="all", help="Enumerate or connect to MSSQL instances via SQL Browser. Use --browser to connect to a specific instance, or --browser all (default) to connect to all instances") return parser From cda1be6251bc77c5687dc6ffc7e7a88a8e45d994 Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Thu, 2 Apr 2026 16:08:02 +0200 Subject: [PATCH 09/10] ruff manual fixes --- nxc/protocols/mssql.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index c85ea5a6b5..fbc7ee61e3 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -53,8 +53,7 @@ def __init__(self, args, db, host): "hostname": "None", } ) - - # --browser + # --browser if args.browser: args.port = 0 # Override port number with dummy one connection.__init__(self, args, db, host) @@ -674,7 +673,6 @@ def discover_sqlbrowser(self): @reconnect_sqlbrowser def instance_connect(self, instance): self.sqlbrowser_logger.debug(f"instance_connect to {instance}") - instance_name = instance.get("InstanceName", None) instance_port = instance.get("tcp", None) instance_np = instance.get("np", None) instance_arguments = self.args @@ -684,7 +682,7 @@ def instance_connect(self, instance): # Case #1, instance is listening on a port if instance_port: instance_arguments.port = instance_port - new_connection = self.__init__(instance_arguments, self.db, self.host) + self.__init__(instance_arguments, self.db, self.host) # Case #2, instance is listening on a named pipe elif instance_np: self.sqlbrowser_logger.fail(f"Named pipe connections are not supported yet, cannot connect to {instance_np}") From 73e65c5f58dc2ed54f5defbe8154ac30aa7854ab Mon Sep 17 00:00:00 2001 From: "Alexandre S." Date: Thu, 2 Apr 2026 16:09:33 +0200 Subject: [PATCH 10/10] Updated e2e_commands --- tests/e2e_commands.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index c8ed9d0414..bc9102f170 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -261,7 +261,10 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M aws-cr netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-brute -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --no-sqlbrowser # Should behave normally +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --browser +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --browser all +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --browser 0 +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --browser netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa