From d8182357954c4ecddcdc69d9ee92d65dc83fe6f5 Mon Sep 17 00:00:00 2001 From: Neoreo Date: Fri, 27 Feb 2026 12:20:37 +0100 Subject: [PATCH 01/16] Enhancement : Compute gMSA aesKeys automatically when using --gmsa --- nxc/protocols/ldap.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d96b996d41..aefa5185c8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -26,6 +26,7 @@ SAM_MACHINE_ACCOUNT, ) from impacket.krb5 import constants +from impacket.krb5.crypto import string_to_key from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal, KerberosException @@ -1379,6 +1380,7 @@ def gmsa(self): # Get the password passwd = "" + aes256 = aes128 = None if "msDS-ManagedPassword" in acc: blob = MSDS_MANAGEDPASSWORD_BLOB() blob.fromString(acc["msDS-ManagedPassword"]) @@ -1386,7 +1388,17 @@ def gmsa(self): ntlm_hash = MD4.new() ntlm_hash.update(currentPassword) passwd = hexlify(ntlm_hash.digest()).decode("utf-8") + # Compute Kerberos AES keys + password = currentPassword.decode("utf-16-le", "replace").encode("utf-8") + sam = acc["sAMAccountName"] + salt = f"{self.domain.upper()}host{sam[:-1].lower()}.{self.domain.lower()}" + aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() + aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") + if aes256: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}") + if aes128: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {aes128}") return True def decipher_gmsa_name(self, domain_name=None, account_name=None): @@ -1456,6 +1468,14 @@ def gmsa_decrypt_lsa(self): ntlm_hash.update(currentPassword) passwd = hexlify(ntlm_hash.digest()).decode("utf-8") self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}") + # Compute Kerberos AES keys + password = currentPassword.decode("utf-16-le", "replace").encode("utf-8") + sam_no_dollar = gmsa_id[:-1] if gmsa_id.endswith("$") else gmsa_id + salt = f"{self.domain.upper()}host{sam_no_dollar.lower()}.{self.domain.lower()}" + aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() + aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() + self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") + self.logger.highlight(f"Account: {gmsa_id:<20} aes128-cts-hmac-sha1-96: {aes128}") else: self.logger.fail("No string provided :'(") From fa7706b807d6b676cdde2dce505ff604ebc86781 Mon Sep 17 00:00:00 2001 From: Neoreo Date: Sat, 28 Feb 2026 11:00:46 +0100 Subject: [PATCH 02/16] Factorize code --- nxc/protocols/ldap.py | 50 ++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index aefa5185c8..568cf997cb 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1382,25 +1382,28 @@ def gmsa(self): passwd = "" aes256 = aes128 = None if "msDS-ManagedPassword" in acc: - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(acc["msDS-ManagedPassword"]) - currentPassword = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(currentPassword) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") - # Compute Kerberos AES keys - password = currentPassword.decode("utf-16-le", "replace").encode("utf-8") - sam = acc["sAMAccountName"] - salt = f"{self.domain.upper()}host{sam[:-1].lower()}.{self.domain.lower()}" - aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() - aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") - if aes256: - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}") - if aes128: + passwd, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"]) + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") 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}") return True + def gmsa_compute_secrets(self, data, sam): + # Compute NT Hash + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(data) + current_password = blob["CurrentPassword"][:-2] + ntlm_hash = MD4.new() + ntlm_hash.update(current_password) + passwd = hexlify(ntlm_hash.digest()).decode("utf-8") + # Compute Kerberos Keys + password = current_password.decode("utf-16-le", "replace").encode("utf-8") + sam_no_dollar = sam[:-1] if sam.endswith("$") else sam + salt = f"{self.domain.upper()}host{sam_no_dollar.lower()}.{self.domain.lower()}" + aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() + aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() + return passwd, aes128, aes256 + def decipher_gmsa_name(self, domain_name=None, account_name=None): # https://aadinternals.com/post/gmsa/ gmsa_account_name = (domain_name + account_name).upper() @@ -1459,21 +1462,10 @@ def gmsa_decrypt_lsa(self): if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id: gmsa_id = acc["sAMAccountName"] break - # convert to ntlm + # Compute the password and keys data = bytes.fromhex(gmsa_pass) - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(data) - currentPassword = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(currentPassword) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") + passwd, aes128, aes256 = self.gmsa_compute_secrets(data, gmsa_id) self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}") - # Compute Kerberos AES keys - password = currentPassword.decode("utf-16-le", "replace").encode("utf-8") - sam_no_dollar = gmsa_id[:-1] if gmsa_id.endswith("$") else gmsa_id - salt = f"{self.domain.upper()}host{sam_no_dollar.lower()}.{self.domain.lower()}" - aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() - aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") self.logger.highlight(f"Account: {gmsa_id:<20} aes128-cts-hmac-sha1-96: {aes128}") else: From f5bbf07a542f00e21b9535cc5e905f2bed890dcd Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 5 May 2026 14:42:38 -0400 Subject: [PATCH 03/16] Use generate_kerberos_keys --- nxc/protocols/ldap.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 3e6ff5f6ba..d11d30644f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -28,7 +28,7 @@ SAM_MACHINE_ACCOUNT, ) from impacket.krb5 import constants -from impacket.krb5.crypto import string_to_key +from impacket.krb5.crypto import generate_kerberos_keys, string_to_key from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal, KerberosException @@ -1330,30 +1330,21 @@ def gmsa(self): principal_with_read = resp_parsed[0]["sAMAccountName"] # Get the password - passwd = "" - aes256 = aes128 = None + rc4 = "" + aes128 = aes256 = "" if "msDS-ManagedPassword" in acc: - passwd, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"]) - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(acc["msDS-ManagedPassword"]) + currentPassword = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() + + keys = generate_kerberos_keys(hex_pass=currentPassword, user=acc["sAMAccountName"], domain=self.targetDomain) + rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode() + aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode() + aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode() + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {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}") - return True - - def gmsa_compute_secrets(self, data, sam): - # Compute NT Hash - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(data) - current_password = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(current_password) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") - # Compute Kerberos Keys - password = current_password.decode("utf-16-le", "replace").encode("utf-8") - sam_no_dollar = sam[:-1] if sam.endswith("$") else sam - salt = f"{self.domain.upper()}host{sam_no_dollar.lower()}.{self.domain.lower()}" - aes256 = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents).decode() - aes128 = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents).decode() - return passwd, aes128, aes256 def decipher_gmsa_name(self, domain_name=None, account_name=None): # https://aadinternals.com/post/gmsa/ From 4951135148ca311a1ee7bd88d24c80fc922472f8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 5 May 2026 15:25:07 -0400 Subject: [PATCH 04/16] Remove unused import --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d11d30644f..d0fdba0538 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -28,7 +28,7 @@ SAM_MACHINE_ACCOUNT, ) from impacket.krb5 import constants -from impacket.krb5.crypto import generate_kerberos_keys, string_to_key +from impacket.krb5.crypto import generate_kerberos_keys from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal, KerberosException From 48691de278a127e06df834aca83955f7fe7d8be3 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 5 May 2026 15:25:23 -0400 Subject: [PATCH 05/16] Functionalize key generation --- nxc/protocols/ldap.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d0fdba0538..291678be28 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1333,19 +1333,24 @@ def gmsa(self): rc4 = "" aes128 = aes256 = "" if "msDS-ManagedPassword" in acc: - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(acc["msDS-ManagedPassword"]) - currentPassword = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() - - keys = generate_kerberos_keys(hex_pass=currentPassword, user=acc["sAMAccountName"], domain=self.targetDomain) - rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode() - aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode() - aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode() + 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}") 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}") + 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.""" + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(password_data) + currentPassword = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() + + keys = generate_kerberos_keys(hex_pass=currentPassword, user=sAMAccountName, domain=self.targetDomain) + rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode() + aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode() + aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode() + return rc4, aes128, aes256 + def decipher_gmsa_name(self, domain_name=None, account_name=None): # https://aadinternals.com/post/gmsa/ gmsa_account_name = (domain_name + account_name).upper() @@ -1406,8 +1411,8 @@ def gmsa_decrypt_lsa(self): break # Compute the password and keys data = bytes.fromhex(gmsa_pass) - passwd, aes128, aes256 = self.gmsa_compute_secrets(data, gmsa_id) - self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}") + rc4, aes128, aes256 = self.gmsa_compute_secrets(data, gmsa_id) + self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {rc4}") self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") self.logger.highlight(f"Account: {gmsa_id:<20} aes128-cts-hmac-sha1-96: {aes128}") else: From 324ff9e8fe642b8d954709bc1e54aef63e6e67c1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 5 May 2026 15:30:15 -0400 Subject: [PATCH 06/16] Remove unused import --- nxc/protocols/ldap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 291678be28..2bc01e7247 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -14,7 +14,6 @@ from dns import resolver from dateutil.relativedelta import relativedelta as rd -from Cryptodome.Hash import MD4 from OpenSSL.SSL import SysCallError from bloodhound.ad.authentication import ADAuthentication from bloodhound.ad.domain import AD From d022f14f7e246ff17cd71139f76b6b1930e421c5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 5 May 2026 15:30:22 -0400 Subject: [PATCH 07/16] Formatting --- nxc/protocols/ldap.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 2bc01e7247..7b1272bf42 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1393,12 +1393,10 @@ def gmsa_decrypt_lsa(self): if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") # getting the gmsa account - search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( + gmsa_accounts = self.search( searchBase=self.baseDN, - searchFilter=search_filter, + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", attributes=["sAMAccountName"], - sizeLimit=0, ) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) if gmsa_accounts_parsed: From 21ea460d91c9450b53f4608edd953f95049ac656 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:06:12 -0400 Subject: [PATCH 08/16] Remove duplicate search base --- nxc/protocols/ldap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7b1272bf42..c7fa2574af 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1394,7 +1394,6 @@ def gmsa_decrypt_lsa(self): gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") # getting the gmsa account gmsa_accounts = self.search( - searchBase=self.baseDN, searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", attributes=["sAMAccountName"], ) From 178df416c884a44d61ff89d7daad3cf4e7daf44e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:06:24 -0400 Subject: [PATCH 09/16] Switch order of displaying kerberos keys --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index c7fa2574af..74c7fef699 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1409,8 +1409,8 @@ def gmsa_decrypt_lsa(self): data = bytes.fromhex(gmsa_pass) rc4, aes128, aes256 = self.gmsa_compute_secrets(data, gmsa_id) self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {rc4}") - self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") self.logger.highlight(f"Account: {gmsa_id:<20} aes128-cts-hmac-sha1-96: {aes128}") + self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") else: self.logger.fail("No string provided :'(") From 358052235adc40ec3f4ff9903cad29faae4fe2bb Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:11:50 -0400 Subject: [PATCH 10/16] If we don't have a sAMAccountName skip kerberos keys --- nxc/protocols/ldap.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 74c7fef699..6b2fc7200f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1398,19 +1398,23 @@ def gmsa_decrypt_lsa(self): attributes=["sAMAccountName"], ) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + sAMAccountName = "" if gmsa_accounts_parsed: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") for acc in gmsa_accounts_parsed: if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id: - gmsa_id = acc["sAMAccountName"] + sAMAccountName = acc["sAMAccountName"] break # Compute the password and keys data = bytes.fromhex(gmsa_pass) - rc4, aes128, aes256 = self.gmsa_compute_secrets(data, gmsa_id) - self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {rc4}") - self.logger.highlight(f"Account: {gmsa_id:<20} aes128-cts-hmac-sha1-96: {aes128}") - self.logger.highlight(f"Account: {gmsa_id:<20} aes256-cts-hmac-sha1-96: {aes256}") + rc4, aes128, aes256 = self.gmsa_compute_secrets(data, sAMAccountName) + self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {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}") else: self.logger.fail("No string provided :'(") From 8d5c81c36ce0f03a33021df907ff07bad7ee306c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:13:47 -0400 Subject: [PATCH 11/16] Formatting --- nxc/protocols/ldap.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 6b2fc7200f..19fb43e20a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1370,12 +1370,9 @@ def gmsa_convert_id(self): self.logger.fail("Length of the gmsa id not correct :'(") else: # getting the gmsa account - search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, + gmsa_accounts = self.search( + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", attributes=["sAMAccountName"], - sizeLimit=0, ) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) if gmsa_accounts_parsed: @@ -1403,7 +1400,7 @@ def gmsa_decrypt_lsa(self): self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id: sAMAccountName = acc["sAMAccountName"] break # Compute the password and keys From a0c62e24132ba2c916be1580f52407839f96d235 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:13:51 -0400 Subject: [PATCH 12/16] Formatting --- nxc/protocols/ldap.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 19fb43e20a..84134008b8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1375,13 +1375,12 @@ def gmsa_convert_id(self): attributes=["sAMAccountName"], ) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) - if gmsa_accounts_parsed: - self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") + self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") - for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == self.args.gmsa_convert_id: - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") - break + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") + break else: self.logger.fail("No string provided :'(") From d9603d5c8b58940ede6b027ccc82d39f834cb64e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:15:49 -0400 Subject: [PATCH 13/16] Formatting --- nxc/protocols/ldap.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 84134008b8..1665245a05 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1365,24 +1365,21 @@ def decipher_gmsa_name(self, domain_name=None, account_name=None): return str_hash def gmsa_convert_id(self): - if self.args.gmsa_convert_id: - if len(self.args.gmsa_convert_id) != 64: - self.logger.fail("Length of the gmsa id not correct :'(") - else: - # getting the gmsa account - gmsa_accounts = self.search( - searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", - attributes=["sAMAccountName"], - ) - gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) - self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") - - for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id: - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") - break + if len(self.args.gmsa_convert_id) != 64: + self.logger.fail("Length of the gmsa id not correct :'(") else: - self.logger.fail("No string provided :'(") + # getting the gmsa account + gmsa_accounts = self.search( + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", + attributes=["sAMAccountName"], + ) + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") + + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") + break def gmsa_decrypt_lsa(self): if self.args.gmsa_decrypt_lsa: From 028e774e8ea684f4506d0c14a00bc3ad275e3ae7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:16:14 -0400 Subject: [PATCH 14/16] Formatting --- nxc/protocols/ldap.py | 53 ++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 1665245a05..05762e041f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1382,34 +1382,31 @@ def gmsa_convert_id(self): break def gmsa_decrypt_lsa(self): - if self.args.gmsa_decrypt_lsa: - if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: - gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") - # getting the gmsa account - gmsa_accounts = self.search( - searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", - attributes=["sAMAccountName"], - ) - gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) - sAMAccountName = "" - if gmsa_accounts_parsed: - self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") - - for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id: - sAMAccountName = acc["sAMAccountName"] - break - # 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}") - 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}") - else: - self.logger.fail("No string provided :'(") + if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: + gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") + # getting the gmsa account + gmsa_accounts = self.search( + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", + attributes=["sAMAccountName"], + ) + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + sAMAccountName = "" + if gmsa_accounts_parsed: + self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") + + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id: + sAMAccountName = acc["sAMAccountName"] + break + # 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}") + 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}") def pso(self): """ From d8ea63da8322d88f2d0776eff8f0cc42753c339a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 09:16:59 -0400 Subject: [PATCH 15/16] Add error message if gmsa string is invalid --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 05762e041f..883cd0a4ea 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1407,6 +1407,8 @@ def gmsa_decrypt_lsa(self): 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}") + else: + self.logger.fail("The provided string does not appear to be a valid GMSA LSA secret.") def pso(self): """ From a23d0fa3b88c04adcb468db72ef15b2665be2fbe Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 7 May 2026 10:01:49 -0400 Subject: [PATCH 16/16] Pythonic --- nxc/protocols/ldap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 883cd0a4ea..b9ba0ce3c1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1342,9 +1342,9 @@ 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.""" blob = MSDS_MANAGEDPASSWORD_BLOB() blob.fromString(password_data) - currentPassword = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() + current_password = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() - keys = generate_kerberos_keys(hex_pass=currentPassword, user=sAMAccountName, domain=self.targetDomain) + keys = generate_kerberos_keys(hex_pass=current_password, user=sAMAccountName, domain=self.targetDomain) rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode() aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode() aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode()