|
| 1 | +#!/usr/bin/env python |
| 2 | +# Impacket - Collection of Python classes for working with network protocols. |
| 3 | +# |
| 4 | +# Copyright Fortra, LLC and its affiliated companies |
| 5 | +# |
| 6 | +# All rights reserved. |
| 7 | +# |
| 8 | +# This software is provided under a slightly modified version |
| 9 | +# of the Apache Software License. See the accompanying LICENSE file |
| 10 | +# for more information. |
| 11 | +# |
| 12 | +# Description: |
| 13 | +# Creates or deletes VSS (Volume Shadow Copy) snapshots on a remote |
| 14 | +# machine via WMI DCOM. No vssadmin process is spawned on the target. |
| 15 | +# |
| 16 | +# The shadow copy is created using Win32_ShadowCopy.Create() over DCOM, |
| 17 | +# which avoids the process creation telemetry associated with vssadmin. |
| 18 | +# |
| 19 | +# Usage: |
| 20 | +# create_shadowcopy.py [-volume C:\] [[domain/]username[:password]@]<target> |
| 21 | +# create_shadowcopy.py -delete -shadow-id {GUID} [[domain/]username[:password]@]<target> |
| 22 | +# create_shadowcopy.py -list [[domain/]username[:password]@]<target> |
| 23 | +# |
| 24 | +# Author: |
| 25 | +# Black Lantern Security |
| 26 | +# |
| 27 | + |
| 28 | +import sys |
| 29 | +import logging |
| 30 | +import argparse |
| 31 | + |
| 32 | +from impacket.examples import logger |
| 33 | +from impacket.examples.utils import parse_target |
| 34 | +from impacket import version |
| 35 | +from impacket.dcerpc.v5.dcom import wmi |
| 36 | +from impacket.dcerpc.v5.dcom.oaut import NULL |
| 37 | +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION |
| 38 | +from impacket.krb5.keytab import Keytab |
| 39 | + |
| 40 | + |
| 41 | +class ShadowCopy: |
| 42 | + def __init__(self, host, username='', password='', domain='', hashes=None, |
| 43 | + aesKey=None, doKerberos=False, kdcHost=None): |
| 44 | + self.__host = host |
| 45 | + self.__username = username |
| 46 | + self.__password = password |
| 47 | + self.__domain = domain |
| 48 | + self.__lmhash = '' |
| 49 | + self.__nthash = '' |
| 50 | + self.__aesKey = aesKey |
| 51 | + self.__doKerberos = doKerberos |
| 52 | + self.__kdcHost = kdcHost |
| 53 | + if hashes is not None: |
| 54 | + self.__lmhash, self.__nthash = hashes.split(':') |
| 55 | + |
| 56 | + def __connect(self): |
| 57 | + dcom = DCOMConnection(self.__host, self.__username, self.__password, |
| 58 | + self.__domain, self.__lmhash, self.__nthash, |
| 59 | + self.__aesKey, oxidResolver=False, |
| 60 | + doKerberos=self.__doKerberos, |
| 61 | + kdcHost=self.__kdcHost) |
| 62 | + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, |
| 63 | + wmi.IID_IWbemLevel1Login) |
| 64 | + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) |
| 65 | + iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) |
| 66 | + iWbemLevel1Login.RemRelease() |
| 67 | + return dcom, iWbemServices |
| 68 | + |
| 69 | + def create(self, volume): |
| 70 | + dcom, iWbemServices = self.__connect() |
| 71 | + try: |
| 72 | + win32ShadowCopy, _ = iWbemServices.GetObject('Win32_ShadowCopy') |
| 73 | + logging.info('Creating shadow copy for volume: %s' % volume) |
| 74 | + result = win32ShadowCopy.Create(volume, 'ClientAccessible') |
| 75 | + shadowId = result.ShadowID |
| 76 | + logging.info('Shadow copy created: %s' % shadowId) |
| 77 | + |
| 78 | + # Query for the device object path |
| 79 | + query = "SELECT * FROM Win32_ShadowCopy WHERE ID='%s'" % shadowId |
| 80 | + iEnum = iWbemServices.ExecQuery(query) |
| 81 | + device_object = None |
| 82 | + try: |
| 83 | + item = iEnum.Next(0xffffffff, 1)[0] |
| 84 | + props = item.getProperties() |
| 85 | + device_object = props['DeviceObject']['value'] |
| 86 | + except Exception: |
| 87 | + pass |
| 88 | + |
| 89 | + return shadowId, device_object |
| 90 | + finally: |
| 91 | + dcom.disconnect() |
| 92 | + |
| 93 | + def delete(self, shadowId): |
| 94 | + dcom, iWbemServices = self.__connect() |
| 95 | + try: |
| 96 | + wmiPath = 'Win32_ShadowCopy.ID="%s"' % shadowId |
| 97 | + logging.info('Deleting shadow copy: %s' % shadowId) |
| 98 | + iWbemServices.DeleteInstance(wmiPath) |
| 99 | + logging.info('Shadow copy deleted.') |
| 100 | + finally: |
| 101 | + dcom.disconnect() |
| 102 | + |
| 103 | + def list(self): |
| 104 | + dcom, iWbemServices = self.__connect() |
| 105 | + try: |
| 106 | + iEnum = iWbemServices.ExecQuery('SELECT * FROM Win32_ShadowCopy') |
| 107 | + shadows = [] |
| 108 | + while True: |
| 109 | + try: |
| 110 | + item = iEnum.Next(0xffffffff, 1)[0] |
| 111 | + props = item.getProperties() |
| 112 | + shadows.append({ |
| 113 | + 'ID': props['ID']['value'], |
| 114 | + 'DeviceObject': props['DeviceObject']['value'], |
| 115 | + 'VolumeName': props['VolumeName']['value'], |
| 116 | + 'InstallDate': props['InstallDate']['value'], |
| 117 | + }) |
| 118 | + except Exception: |
| 119 | + break |
| 120 | + return shadows |
| 121 | + finally: |
| 122 | + dcom.disconnect() |
| 123 | + |
| 124 | + |
| 125 | +if __name__ == '__main__': |
| 126 | + print(version.BANNER) |
| 127 | + |
| 128 | + parser = argparse.ArgumentParser(add_help=True, |
| 129 | + description='Creates, lists, or deletes VSS shadow copies on a remote ' |
| 130 | + 'machine via WMI DCOM. No vssadmin process is spawned on ' |
| 131 | + 'the target.') |
| 132 | + |
| 133 | + parser.add_argument('target', action='store', |
| 134 | + help='[[domain/]username[:password]@]<targetName or address>') |
| 135 | + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') |
| 136 | + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') |
| 137 | + parser.add_argument('-com-version', action='store', metavar='MAJOR_VERSION:MINOR_VERSION', |
| 138 | + help='DCOM version, format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') |
| 139 | + |
| 140 | + action = parser.add_argument_group('action') |
| 141 | + action.add_argument('-volume', action='store', default='C:\\', |
| 142 | + help='Volume to create the shadow copy for (default: C:\\)') |
| 143 | + action.add_argument('-list', action='store_true', default=False, |
| 144 | + help='List existing shadow copies on the target') |
| 145 | + action.add_argument('-delete', action='store_true', default=False, |
| 146 | + help='Delete a shadow copy (requires -shadow-id)') |
| 147 | + action.add_argument('-shadow-id', action='store', metavar='ID', |
| 148 | + help='Shadow copy ID for deletion (e.g. {GUID})') |
| 149 | + |
| 150 | + group = parser.add_argument_group('authentication') |
| 151 | + group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', |
| 152 | + help='NTLM hashes, format is LMHASH:NTHASH') |
| 153 | + group.add_argument('-no-pass', action='store_true', |
| 154 | + help='Don\'t ask for password (useful for -k)') |
| 155 | + group.add_argument('-k', action='store_true', |
| 156 | + help='Use Kerberos authentication. Grabs credentials from ccache file ' |
| 157 | + '(KRB5CCNAME) based on target parameters. If valid credentials ' |
| 158 | + 'cannot be found, it will use the ones specified in the command line') |
| 159 | + group.add_argument('-aesKey', action='store', metavar='hex key', |
| 160 | + help='AES key to use for Kerberos Authentication (128 or 256 bits)') |
| 161 | + group.add_argument('-dc-ip', action='store', metavar='ip address', |
| 162 | + help='IP Address of the domain controller. If omitted it will use the ' |
| 163 | + 'domain part (FQDN) specified in the target parameter') |
| 164 | + group.add_argument('-target-ip', action='store', metavar='ip address', |
| 165 | + help='IP Address of the target machine. If omitted it will use whatever ' |
| 166 | + 'was specified as target. This is useful when target is the NetBIOS ' |
| 167 | + 'name and you cannot resolve it') |
| 168 | + group.add_argument('-keytab', action='store', |
| 169 | + help='Read keys for SPN from keytab file') |
| 170 | + |
| 171 | + if len(sys.argv) == 1: |
| 172 | + parser.print_help() |
| 173 | + sys.exit(1) |
| 174 | + |
| 175 | + options = parser.parse_args() |
| 176 | + |
| 177 | + logger.init(options.ts, options.debug) |
| 178 | + |
| 179 | + if options.debug is True: |
| 180 | + logging.getLogger().setLevel(logging.DEBUG) |
| 181 | + else: |
| 182 | + logging.getLogger().setLevel(logging.INFO) |
| 183 | + |
| 184 | + if options.com_version is not None: |
| 185 | + try: |
| 186 | + major_version, minor_version = options.com_version.split('.') |
| 187 | + COMVERSION.set_default_version(int(major_version), int(minor_version)) |
| 188 | + except Exception: |
| 189 | + logging.error('Wrong COMVERSION format, use dot separated integers e.g. "5.7"') |
| 190 | + sys.exit(1) |
| 191 | + |
| 192 | + if options.delete and not options.shadow_id: |
| 193 | + logging.error('-delete requires -shadow-id') |
| 194 | + sys.exit(1) |
| 195 | + |
| 196 | + domain, username, password, address = parse_target(options.target) |
| 197 | + |
| 198 | + if options.target_ip is None: |
| 199 | + options.target_ip = address |
| 200 | + |
| 201 | + if domain is None: |
| 202 | + domain = '' |
| 203 | + |
| 204 | + if password == '' and username != '' and options.hashes is None \ |
| 205 | + and options.no_pass is False and options.aesKey is None: |
| 206 | + from getpass import getpass |
| 207 | + password = getpass('Password:') |
| 208 | + |
| 209 | + if options.aesKey is not None: |
| 210 | + options.k = True |
| 211 | + |
| 212 | + if options.keytab is not None: |
| 213 | + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) |
| 214 | + options.k = True |
| 215 | + |
| 216 | + try: |
| 217 | + sc = ShadowCopy(options.target_ip, username, password, domain, |
| 218 | + options.hashes, options.aesKey, options.k, options.dc_ip) |
| 219 | + |
| 220 | + if options.list: |
| 221 | + shadows = sc.list() |
| 222 | + if not shadows: |
| 223 | + logging.info('No shadow copies found.') |
| 224 | + else: |
| 225 | + print('') |
| 226 | + for s in shadows: |
| 227 | + print(' Shadow ID: %s' % s['ID']) |
| 228 | + print(' Device Object: %s' % s['DeviceObject']) |
| 229 | + print(' Volume Name: %s' % s['VolumeName']) |
| 230 | + print(' Install Date: %s' % s['InstallDate']) |
| 231 | + print('') |
| 232 | + |
| 233 | + elif options.delete: |
| 234 | + sc.delete(options.shadow_id) |
| 235 | + |
| 236 | + else: |
| 237 | + shadowId, deviceObject = sc.create(options.volume) |
| 238 | + print('') |
| 239 | + print('Shadow ID: %s' % shadowId) |
| 240 | + print('Device Object: %s' % deviceObject) |
| 241 | + print('') |
| 242 | + print('Use this device path with Read-RawNTFS to access files:') |
| 243 | + print(' Read-RawNTFS -DevicePath "%s" -FileName "ntds.dit" -Output .\\ntds.dit' % deviceObject) |
| 244 | + print('') |
| 245 | + print('To delete this shadow copy:') |
| 246 | + print(' create_shadowcopy.py -delete -shadow-id "%s" %s' % (shadowId, options.target)) |
| 247 | + |
| 248 | + except Exception as e: |
| 249 | + if logging.getLogger().level == logging.DEBUG: |
| 250 | + import traceback |
| 251 | + traceback.print_exc() |
| 252 | + logging.error(str(e)) |
| 253 | + sys.exit(1) |
0 commit comments