diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 098a0805be..fbc7ee61e3 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -44,8 +44,34 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.is_mssql = False - - connection.__init__(self, args, db, host) + self.sqlbrowser_enabled = False + self.sqlbrowser_logger = NXCAdapter( + extra={ + "protocol": "SQLBROWSER", + "host": host, + "port": "1434", + "hostname": "None", + } + ) + # --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( @@ -59,6 +85,7 @@ def proto_logger(self): def create_conn_obj(self): try: + # 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: @@ -71,6 +98,7 @@ def create_conn_obj(self): return True def reconnect_mssql(func): + def wrapper(self, *args, **kwargs): with contextlib.suppress(Exception): self.conn.disconnect() @@ -78,6 +106,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: @@ -116,9 +151,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(0) - # Send the NTLMSSP Negotiate or SQL Auth Packet self.conn.sendTDS(tds.TDS_LOGIN7, login.getData()) @@ -139,7 +171,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 @@ -605,3 +638,51 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + 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.sqlbrowser_logger.success(f"#{index} {instance_name} (port:{instance_port}) (np:{instance_np}) (version:{instance_version})") + + 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.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_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 + 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}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..3da9c1ff6f 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,4 +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") return parser diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index c8c06dcdd9..bc9102f170 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -261,6 +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 --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