Skip to content

Commit b6bed5e

Browse files
committed
Add create_shadowcopy.py for VSS shadow copy management via WMI DCOM
Standalone impacket script to create, list, and delete VSS shadow copies on a remote machine using Win32_ShadowCopy over DCOM. No vssadmin process is spawned on the target, avoiding process creation telemetry.
1 parent 7b59a1a commit b6bed5e

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

examples/create_shadowcopy.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)