From c70123eda920ae4d62502667dcf60a63dcc125e0 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 12 Jul 2026 20:55:51 +0200 Subject: [PATCH 1/6] Redact SAM/LSA/NTDS dump output in audit mode Audit mode already masked login credentials but not those coming from --sam, --lsa and --ntds. Reuses the same mechanism (audit_mode) to keep only the identifier (username, secret name, GMSA ID) and mask the rest of the line shown on screen. The database and export files still keep the full data. --- nxc/config.py | 14 ++++++++++++++ nxc/protocols/smb.py | 12 ++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/nxc/config.py b/nxc/config.py index 477bdaf5c2..1a6f15c8c9 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -48,3 +48,17 @@ def process_secret(text): reveal = text[:reveal_chars_of_pwd] return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8) + + +def process_secret_dump(line, sep=":"): + """ + Redacts the credential material of a dumped secret line (SAM/LSA/NTDS) when audit_mode is + enabled, keeping only the identifier (username, secret name, etc.) before the first separator. + """ + if not audit_mode: + return line + if sep not in line: + return process_secret(line) + identifier, _, _ = line.partition(sep) + mask = audit_mode if len(audit_mode) > 1 else audit_mode * 8 + return f"{identifier}{sep}{mask}" diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..93040bed74 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -54,7 +54,7 @@ from impacket.dcerpc.v5 import tsts as TSTS -from nxc.config import process_secret, host_info_colors, check_guest_account +from nxc.config import process_secret, process_secret_dump, host_info_colors, check_guest_account from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter @@ -2102,7 +2102,7 @@ def sam(self): host_id = self.db.get_hosts(filter_term=self.host)[0][0] def add_sam_hash(sam_hash, host_id): - self.logger.highlight(sam_hash) + self.logger.highlight(process_secret_dump(sam_hash)) if "_history" in sam_hash: return username, _, lmhash, nthash, _, _, _ = sam_hash.split(":") @@ -2422,7 +2422,7 @@ def lsa(self): def add_lsa_secret(secret): add_lsa_secret.secrets += 1 - self.logger.highlight(secret) + self.logger.highlight(process_secret_dump(secret)) if "_SC_GMSA_{84A78B8C" in secret: gmsa_id = secret.split("_")[4].split(":")[0] data = bytes.fromhex(secret.split("_")[4].split(":")[1]) @@ -2432,7 +2432,7 @@ 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}") + self.logger.highlight(process_secret_dump(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}", sep="NTLM: ")) add_lsa_secret.secrets = 0 @@ -2494,10 +2494,10 @@ def add_hash(secret_type, secret, host_id): if self.args.enabled: if "Enabled" in secret: secret = " ".join(secret.split(" ")[:-1]) - self.logger.highlight(secret) + self.logger.highlight(process_secret_dump(secret)) else: secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret - self.logger.highlight(secret) + self.logger.highlight(process_secret_dump(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: From 84223582c2d66692acab444ceb6fc93c8bc64f1e Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 12 Jul 2026 21:07:19 +0200 Subject: [PATCH 2/6] Keep username/RID visible in NTDS dump redaction Adds a keep parameter to process_secret_dump so the NTDS callback can preserve domain\username:rid while still masking the LM/NT hashes in audit mode. --- nxc/config.py | 11 ++++++----- nxc/protocols/smb.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nxc/config.py b/nxc/config.py index 1a6f15c8c9..dd22e503c1 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -50,15 +50,16 @@ def process_secret(text): return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8) -def process_secret_dump(line, sep=":"): +def process_secret_dump(line, sep=":", keep=1): """ Redacts the credential material of a dumped secret line (SAM/LSA/NTDS) when audit_mode is - enabled, keeping only the identifier (username, secret name, etc.) before the first separator. + enabled, keeping only the first `keep` fields (username, RID, secret name, etc.) before + masking the rest of the line. """ if not audit_mode: return line - if sep not in line: + parts = line.split(sep) + if len(parts) <= keep: return process_secret(line) - identifier, _, _ = line.partition(sep) mask = audit_mode if len(audit_mode) > 1 else audit_mode * 8 - return f"{identifier}{sep}{mask}" + return sep.join([*parts[:keep], mask]) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 93040bed74..3e08caac99 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2494,10 +2494,10 @@ def add_hash(secret_type, secret, host_id): if self.args.enabled: if "Enabled" in secret: secret = " ".join(secret.split(" ")[:-1]) - self.logger.highlight(process_secret_dump(secret)) + self.logger.highlight(process_secret_dump(secret, keep=2)) else: secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret - self.logger.highlight(process_secret_dump(secret)) + self.logger.highlight(process_secret_dump(secret, keep=2)) # 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: From aba3086b3f584e64c82316aa1c07be82e835a233 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 12 Jul 2026 21:24:59 +0200 Subject: [PATCH 3/6] Redact Kerberoasting/AS-REP roast hashes in audit mode Reuses process_secret_dump on the highlighted hashcat-format hash lines, keeping the identifying part (user@domain for AS-REP, user/realm/SPN for Kerberoasting) visible while masking the crackable checksum/data. --- nxc/protocols/ldap.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index fd034fa427..72a3442550 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -38,7 +38,7 @@ from impacket.smbconnection import SessionError from impacket.ntlm import getNTLMSSPType1 -from nxc.config import process_secret, host_info_colors +from nxc.config import process_secret, process_secret_dump, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument @@ -321,7 +321,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if self.username and self.password == "" and self.args.asreproast: hash_tgt = KerberosAttacks(self).get_tgt_asroast(self.username) if hash_tgt: - self.logger.highlight(f"{hash_tgt}") + self.logger.highlight(process_secret_dump(hash_tgt)) with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_tgt}\n") return False @@ -446,7 +446,7 @@ def plaintext_login(self, domain, username, password): if self.username and self.password == "" and self.args.asreproast: hash_tgt = KerberosAttacks(self).get_tgt_asroast(self.username) if hash_tgt: - self.logger.highlight(f"{hash_tgt}") + self.logger.highlight(process_secret_dump(hash_tgt)) with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_tgt}\n") return False @@ -542,7 +542,7 @@ def hash_login(self, domain, username, ntlm_hash): if self.username and self.hash == "" and self.args.asreproast: hash_tgt = KerberosAttacks(self).get_tgt_asroast(self.username) if hash_tgt: - self.logger.highlight(f"{hash_tgt}") + self.logger.highlight(process_secret_dump(hash_tgt)) with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_tgt}\n") return False @@ -969,7 +969,7 @@ def asreproast(self): for user in resp_parsed: hash_TGT = KerberosAttacks(self).get_tgt_asroast(user["sAMAccountName"]) if hash_TGT: - self.logger.highlight(f"{hash_TGT}") + self.logger.highlight(process_secret_dump(hash_TGT)) with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_TGT}\n") @@ -1071,7 +1071,7 @@ def kerberoasting(self): pwdLastSet = "" if str(user.get("pwdLastSet", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["pwdLastSet"])))) lastLogon = "" if str(user.get("lastLogon", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["lastLogon"])))) self.logger.display(f"sAMAccountName: {user['sAMAccountName']}, memberOf: {user.get('memberOf', [])}, pwdLastSet: {pwdLastSet}, lastLogon: {lastLogon}") - self.logger.highlight(out) + self.logger.highlight(process_secret_dump(out, sep="*", keep=2)) if self.args.kerberoasting: with open(self.args.kerberoasting, "a+") as hash_kerberoasting: hash_kerberoasting.write(out + "\n") From f15c355d41e2e39306bb7eea8d2c1e09aa7961bb Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 12 Jul 2026 21:48:40 +0200 Subject: [PATCH 4/6] Redact GMSA NTLM/AES hashes in audit mode Same masking as the LSA GMSA output: --gmsa, --gmsa-convert-id and --gmsa-decrypt-lsa now mask the derived NTLM, AES128 and AES256 keys via process_secret while keeping the account name and read-permission list visible. --- nxc/protocols/ldap.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 72a3442550..dce4c62043 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1339,10 +1339,10 @@ def gmsa(self): aes128 = aes256 = "" if "msDS-ManagedPassword" in acc: rc4, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"]) - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {rc4:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {process_secret(rc4):<36} PrincipalsAllowedToReadPassword: {principal_with_read}") if aes128 and aes256: - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {aes128}") - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}") + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {process_secret(aes128)}") + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {process_secret(aes256)}") def gmsa_compute_secrets(self, password_data: bytes, sAMAccountName: str): """Generate RC4, AES128, and AES256 keys for a GMSA account based on the provided password data and username.""" @@ -1407,12 +1407,12 @@ def gmsa_decrypt_lsa(self): # Compute the password and keys data = bytes.fromhex(gmsa_pass) rc4, aes128, aes256 = self.gmsa_compute_secrets(data, sAMAccountName) - self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {rc4}") + self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {process_secret(rc4)}") if not sAMAccountName: self.logger.fail("Could not find the GMSA account associated with the provided ID.") else: - self.logger.highlight(f"Account: {sAMAccountName:<20} aes128-cts-hmac-sha1-96: {aes128}") - self.logger.highlight(f"Account: {sAMAccountName:<20} aes256-cts-hmac-sha1-96: {aes256}") + self.logger.highlight(f"Account: {sAMAccountName:<20} aes128-cts-hmac-sha1-96: {process_secret(aes128)}") + self.logger.highlight(f"Account: {sAMAccountName:<20} aes256-cts-hmac-sha1-96: {process_secret(aes256)}") else: self.logger.fail("The provided string does not appear to be a valid GMSA LSA secret.") From 3225d221e4f4e8ae92fe65859a1c57c8d4601fa6 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Mon, 13 Jul 2026 03:50:29 +0200 Subject: [PATCH 5/6] Redact --dpapi secrets in audit mode Masks password/token/cookie_value in the credential, browser, vault and Firefox DPAPI callbacks via process_secret, keeping the identifying fields (winuser, url/target/resource, username) visible on screen and in the export file. The database still stores the full unredacted secrets. --- nxc/protocols/smb.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 3e08caac99..209ff92f27 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2267,7 +2267,7 @@ def dpapi(self): # Collect User and Machine Credentials Manager secrets def credential_callback(credential): tag = "CREDENTIAL" - line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" + line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{process_secret(credential.password)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2305,7 +2305,7 @@ def credential_callback(credential): def browser_callback(secret): if isinstance(secret, LoginData): secret_url = secret.url + " -" if secret.url != "" else "-" - line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" + line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{process_secret(secret.password)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2318,7 +2318,7 @@ def browser_callback(secret): secret.url, ) elif isinstance(secret, GoogleRefreshToken): - line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" + line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{process_secret(secret.token)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2331,7 +2331,7 @@ def browser_callback(secret): "Google Refresh Token", ) elif isinstance(secret, Cookie): - line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" + line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{process_secret(secret.cookie_value)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2346,7 +2346,7 @@ def vault_callback(secret): tag = "IEX" if secret.type == "Internet Explorer": resource = secret.resource + " -" if secret.resource != "" else "-" - line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" + line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{process_secret(secret.password)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2370,7 +2370,7 @@ def firefox_callback(secret): tag = "FIREFOX" if isinstance(secret, FirefoxData): url = secret.url + " -" if secret.url != "" else "-" - line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" + line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{process_secret(secret.password)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") @@ -2383,7 +2383,7 @@ def firefox_callback(secret): secret.url, ) elif isinstance(secret, FirefoxCookie): - line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" + line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{process_secret(secret.cookie_value)}" self.logger.highlight(line) if self.output_file: self.output_file.write(line + "\n") From 06732e3b0754ca15710116d9721d9ce4778da910 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Wed, 15 Jul 2026 23:21:23 +0200 Subject: [PATCH 6/6] Redact --sccm secrets in audit mode Masks the NAA account password, task sequence secret and collection variable value via process_secret, keeping the identifying fields (username, variable name, tag) visible. The database still stores the full unredacted secrets. --- nxc/protocols/smb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 209ff92f27..382f90d9ad 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2185,7 +2185,7 @@ def sccm(self): def sccm_callback(secret): if isinstance(secret, SCCMCred): tag = "NAA Account" - self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}") + self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{process_secret(secret.password.decode('latin-1'))}") self.db.add_dpapi_secrets( target.address, f"SCCM - {tag}", @@ -2196,7 +2196,7 @@ def sccm_callback(secret): ) elif isinstance(secret, SCCMSecret): tag = "Task sequences secret" - self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}") + self.logger.highlight(f"[{tag}] {process_secret(secret.secret.decode('latin-1'))}") self.db.add_dpapi_secrets( target.address, f"SCCM - {tag}", @@ -2207,7 +2207,7 @@ def sccm_callback(secret): ) elif isinstance(secret, SCCMCollection): tag = "Collection Variable" - self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}") + self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{process_secret(secret.value.decode('latin-1'))}") self.db.add_dpapi_secrets( target.address, f"SCCM - {tag}",