From dd72a956e6e4c8863f3fc3c01cb081bf869a28a8 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:00:34 +0200 Subject: [PATCH 1/8] Add --trust-keys to smb ntds dump Expose impacket secretsdump's trust key extraction (fortra/impacket#2207) through NetExec's --ntds: dumps Trusted Domain Object secrets and derives inter-realm Kerberos (AES256, AES128) and RC4 keys for each trust direction. Also fixes the account-line status stripping in add_hash(), which blindly dropped the last space-separated word of every secret line (meant to strip the trailing "Enabled"/"Disabled" on account lines) and was truncating the trust key lines, which contain spaces but no status suffix. --- nxc/protocols/smb.py | 9 ++++++++- nxc/protocols/smb/proto_args.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..bab90f7d0c 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2490,13 +2490,18 @@ def add_hash(secret_type, secret, host_id): else: add_hash.nt_lm_secrets += 1 + # NTDS account lines end with a trailing " Enabled"/" Disabled" status word; + # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces + # but have no status suffix, so only strip the last word when it's actually a status. + has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") + # 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 + secret = " ".join(secret.split(" ")[:-1]) if has_status else secret self.logger.highlight(secret) # Filter out computer accounts, history hashes and kerberos keys for adding to db @@ -2548,6 +2553,8 @@ def add_hash(secret_type, secret, host_id): outputFileName=self.output_filename, justUser=self.args.userntds if self.args.userntds else None, printUserStatus=True, + trustKeys=self.args.trust_keys, + domainFQDN=self.domain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5fbb6f93ec..178f7db850 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -42,6 +42,8 @@ def proto_args(parser, parents): 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)") + trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + trust_keys_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") From dc7e0e432d62fc50cb05164c61f12b00fdb38db4 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:33:11 +0200 Subject: [PATCH 2/8] Add --just-trust-keys to smb ntds dump Like --trust-keys but skips account enumeration entirely (implied --just-dc behavior in impacket), emitting only the trust keys. Useful when only the inter-realm keys are needed, for a faster and quieter dump. --- nxc/protocols/smb.py | 1 + nxc/protocols/smb/proto_args.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index bab90f7d0c..7c23f1bfaa 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2554,6 +2554,7 @@ def add_hash(secret_type, secret, host_id): justUser=self.args.userntds if self.args.userntds else None, printUserStatus=True, trustKeys=self.args.trust_keys, + justTrustKeys=self.args.just_trust_keys, domainFQDN=self.domain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 178f7db850..996076a74b 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -43,7 +43,9 @@ def proto_args(parser, parents): enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)") trust_keys_arg.make_required = [ntds_arg] + just_trust_keys_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") From d96e92d8677e4525c41086d4f2a4b1950f79c51b Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:48:52 +0200 Subject: [PATCH 3/8] Fix trust-keys dump summary messages --just-trust-keys reused the regular NTDS dump summary ("NTDS hashes", "added to the database", grep-disabled hint), which is meaningless when only trust keys are dumped (no accounts are enumerated). Show a summary specific to that mode instead. --- nxc/protocols/smb.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 7c23f1bfaa..fbf446d8b9 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2560,14 +2560,20 @@ def add_hash(secret_type, secret, host_id): ) try: - self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") + if self.args.just_trust_keys: + self.logger.success("Dumping the trust keys, this could take a while so go grab a redbull...") + else: + self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() ntds_outfile = f"{self.output_filename}.ntds" - self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database") - 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") + if self.args.just_trust_keys: + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}") + else: + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database") + 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: # if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: # We don't store the resume file if this error happened, since this error is related to lack From 9860c5fded306e3209a9f0f83030fb597d56b5eb Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 14:07:51 +0200 Subject: [PATCH 4/8] Fix trust key lines disappearing with --enabled Trust key lines never contain "Enabled"/"Disabled" (they have no account status), so the --enabled filter silently dropped them entirely. Detect trust key lines and always show them regardless of --enabled. --- nxc/protocols/smb.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index fbf446d8b9..8447c0c8b4 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2494,12 +2494,19 @@ def add_hash(secret_type, secret, host_id): # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces # but have no status suffix, so only strip the last word when it's actually a status. has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") + # Trust keys have no enabled/disabled account status to filter on, so they're + # shown regardless of --enabled, except previous-password keys which are the + # trust equivalent of history and get excluded like --enabled excludes those. + is_trust_key = " (Incoming" in secret or " (Outgoing" in secret + is_current_trust_key = is_trust_key and ", previous)" not in secret # Log the secret based on args if self.args.enabled: if "Enabled" in secret: secret = " ".join(secret.split(" ")[:-1]) self.logger.highlight(secret) + elif is_current_trust_key: + self.logger.highlight(secret) else: secret = " ".join(secret.split(" ")[:-1]) if has_status else secret self.logger.highlight(secret) From 77f6ab4f855c4cc40d2050bbd75fa00c8ffd084d Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 20:04:25 +0200 Subject: [PATCH 5/8] Add e2e tests and update --enabled help text for trust keys --- nxc/protocols/smb/proto_args.py | 2 +- tests/e2e_commands.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 996076a74b..f660d80174 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -38,7 +38,7 @@ def proto_args(parser, parents): # 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)") + enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit), and current trust keys") 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)") diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 99ff30c4d1..06aa1eb9de 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -32,6 +32,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --history netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --kerberos-keys +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --trust-keys +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --just-trust-keys netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm From 59778f5b7083e237cbdbca46e70db6162f591601 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 26 Jul 2026 17:32:06 +0200 Subject: [PATCH 6/8] Use self.targetDomain for trust key salt derivation --- 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 8447c0c8b4..d01be2f3fb 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2562,7 +2562,7 @@ def add_hash(secret_type, secret, host_id): printUserStatus=True, trustKeys=self.args.trust_keys, justTrustKeys=self.args.just_trust_keys, - domainFQDN=self.domain, + domainFQDN=self.targetDomain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) From 9e94f58081f9fc3269b7ad77570adf925d5016eb Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:45:18 +0200 Subject: [PATCH 7/8] simplify the logic and fix regression Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/smb.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d01be2f3fb..a03d939fd1 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2490,25 +2490,13 @@ def add_hash(secret_type, secret, host_id): else: add_hash.nt_lm_secrets += 1 - # NTDS account lines end with a trailing " Enabled"/" Disabled" status word; - # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces - # but have no status suffix, so only strip the last word when it's actually a status. - has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") - # Trust keys have no enabled/disabled account status to filter on, so they're - # shown regardless of --enabled, except previous-password keys which are the - # trust equivalent of history and get excluded like --enabled excludes those. + is_enabled_account = secret.endswith(" (status=Enabled)") is_trust_key = " (Incoming" in secret or " (Outgoing" in secret is_current_trust_key = is_trust_key and ", previous)" not in secret + for status in (" (status=Enabled)", " (status=Disabled)", " (status=N/A)"): + secret = secret.removesuffix(status) - # Log the secret based on args - if self.args.enabled: - if "Enabled" in secret: - secret = " ".join(secret.split(" ")[:-1]) - self.logger.highlight(secret) - elif is_current_trust_key: - self.logger.highlight(secret) - else: - secret = " ".join(secret.split(" ")[:-1]) if has_status else secret + if not self.args.enabled or is_enabled_account or is_current_trust_key: self.logger.highlight(secret) # Filter out computer accounts, history hashes and kerberos keys for adding to db From 978270ad000b9b60c7d0c530dcb8bb410aaabb89 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:49:41 +0200 Subject: [PATCH 8/8] Add default value to --trust-keys argument Set default value for --trust-keys argument to True. Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/smb/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index f660d80174..8086018a34 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -42,7 +42,7 @@ def proto_args(parser, parents): 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)") - trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit), default True") just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)") trust_keys_arg.make_required = [ntds_arg] just_trust_keys_arg.make_required = [ntds_arg]