Skip to content

Commit b1e9d63

Browse files
authored
Merge pull request Pennyw0rth#1053 from XiaoliChan/list-snapshot
[ShadowCopy] Add `list-snapshots` function for SMB & WMI
2 parents 182c898 + 1d672fe commit b1e9d63

10 files changed

Lines changed: 122 additions & 106 deletions

File tree

nxc/modules/enum_dns.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ def options(self, context, module_options):
2929
def on_admin_login(self, context, connection):
3030
if not self.domains:
3131
domains = []
32-
output = connection.wmi("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
32+
output = connection.wmi_query("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
3333
domains = [result["Name"]["value"] for result in output] if output else []
3434
context.log.success(f"Domains retrieved: {domains}")
3535
else:
3636
domains = [self.domains]
3737
data = ""
3838

3939
for domain in domains:
40-
output = connection.wmi(
41-
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
40+
output = connection.wmi_query(
41+
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = '{domain}'",
4242
"root\\microsoftdns",
4343
)
4444

nxc/modules/get_netconnections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def options(self, context, module_options):
2222

2323
def on_admin_login(self, context, connection):
2424
data = []
25-
cards = connection.wmi("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
25+
cards = connection.wmi_query("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
2626
if cards:
2727
for c in cards:
2828
if c["IPAddress"].get("value"):

nxc/modules/wcc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ def check_laps(self):
399399
return success, reasons
400400

401401
def check_last_successful_update(self):
402-
records = self.connection.wmi(wmi_query="Select TimeGenerated FROM Win32_ReliabilityRecords Where EventIdentifier=19", namespace="root\\cimv2")
403-
if isinstance(records, bool) or len(records) == 0:
402+
records = self.connection.wmi_query(wql="Select TimeGenerated FROM Win32_ReliabilityRecords Where EventIdentifier=19", namespace="root\\cimv2")
403+
if not records:
404404
return False, ["No update found"]
405405
most_recent_update_date = records[0]["TimeGenerated"]["value"]
406406
most_recent_update_date = most_recent_update_date.split(".")[0]

nxc/protocols/smb.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,10 +1718,10 @@ def pass_pol(self):
17181718
return PassPolDump(self).dump()
17191719

17201720
@requires_admin
1721-
def wmi(self, wmi_query=None, namespace=None):
1721+
def wmi_query(self, wql=None, namespace=None, callback_func=None):
17221722
records = []
1723-
if not wmi_query:
1724-
wmi_query = self.args.wmi.strip("\n")
1723+
if not wql:
1724+
wql = self.args.wmi_query.strip("\n")
17251725

17261726
if not namespace:
17271727
namespace = self.args.wmi_namespace
@@ -1742,28 +1742,29 @@ def wmi(self, wmi_query=None, namespace=None):
17421742
iWbemLevel1Login = IWbemLevel1Login(iInterface)
17431743
iWbemServices = iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
17441744
iWbemLevel1Login.RemRelease()
1745-
iEnumWbemClassObject = iWbemServices.ExecQuery(wmi_query)
1745+
iEnumWbemClassObject = iWbemServices.ExecQuery(wql)
17461746
except Exception as e:
17471747
self.logger.fail(f"Execute WQL error: {e}")
17481748
if "iWbemLevel1Login" in locals():
17491749
dcom.disconnect()
17501750
else:
1751-
self.logger.info(f"Executing WQL syntax: {wmi_query}")
1752-
while True:
1753-
try:
1754-
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
1755-
record = wmi_results.getProperties()
1756-
records.append(record)
1757-
for k, v in record.items():
1758-
if k != "TimeGenerated": # from the wcc module, but this is a small hack to get it to stop spamming - TODO: add in method to disable output for this function
1759-
self.logger.highlight(f"{k} => {v['value']}")
1760-
except Exception as e:
1761-
if str(e).find("S_FALSE") < 0:
1762-
raise e
1763-
else:
1764-
break
1751+
self.logger.info(f"Executing WQL syntax: {wql}")
1752+
try:
1753+
if not callback_func:
1754+
while True:
1755+
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
1756+
record = wmi_results.getProperties()
1757+
records.append(record)
1758+
for k, v in record.items():
1759+
if k != "TimeGenerated": # from the wcc module, but this is a small hack to get it to stop spamming - TODO: add in method to disable output for this function
1760+
self.logger.highlight(f"{k} => {v['value']}")
1761+
else:
1762+
callback_func(iEnumWbemClassObject, records)
1763+
except Exception as e:
1764+
if str(e).find("S_FALSE") < 0:
1765+
self.logger.debug(e)
17651766
dcom.disconnect()
1766-
return records if records else False
1767+
return records
17671768

17681769
def spider(
17691770
self,
@@ -2235,6 +2236,20 @@ def firefox_callback(secret):
22352236
if self.output_file:
22362237
self.output_file.close()
22372238

2239+
@requires_admin
2240+
def list_snapshots(self):
2241+
drive = self.args.list_snapshots
2242+
2243+
self.logger.info(f"Retrieving volume shadow copies of drive {drive}.")
2244+
snapshots = self.conn.listSnapshots(self.conn.connectTree(drive), "/")
2245+
if not snapshots:
2246+
self.logger.info("No volume shadow copies found.")
2247+
return
2248+
self.logger.highlight(f"{'Drive':<8}{'Shadow Copies GMT SMB PATH':<26}")
2249+
self.logger.highlight(f"{'------':<8}{'--------------------------':<26}")
2250+
for i in snapshots:
2251+
self.logger.highlight(f"{drive:<8}{i:<26}")
2252+
22382253
@requires_admin
22392254
def lsa(self):
22402255
try:

nxc/protocols/smb/proto_args.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def proto_args(parser, parents):
4747
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems")
4848
cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
4949
cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
50+
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
5051

5152
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration")
5253
mapping_enum_group.add_argument("--shares", type=str, nargs="?", const="", help="Enumerate shares and access, filter on specified argument (read ; write ; read,write)")
@@ -72,8 +73,8 @@ def proto_args(parser, parents):
7273
mapping_enum_group.add_argument("--taskkill", type=str, help="Kills a specific PID or a proces name's PID's")
7374

7475
wmi_group = smb_parser.add_argument_group("WMI Queries")
75-
wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
76-
wmi_group.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace")
76+
wmi_group.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query")
77+
wmi_group.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace (default: %(default)s)")
7778

7879
spidering_group = smb_parser.add_argument_group("Spidering Shares")
7980
spidering_group.add_argument("--spider", metavar="SHARE", type=str, help="share to spider")

nxc/protocols/wmi.py

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def __init__(self, args, db, host):
4545
"0000052B": "STATUS_WRONG_PASSWORD",
4646
"00000721": "RPC_S_SEC_PKG_ERROR"
4747
}
48+
self.iWbemLevel1Login = None
49+
self.dcom_conn = None
4850

4951
connection.__init__(self, args, db, host)
5052

@@ -58,6 +60,14 @@ def proto_logger(self):
5860
}
5961
)
6062

63+
# Redefine disconnect function.
64+
def disconnect(self):
65+
if self.conn:
66+
self.conn.disconnect()
67+
if self.dcom_conn:
68+
self.dcom_conn.disconnect()
69+
return
70+
6171
def create_conn_obj(self):
6272
connection_target = fr"ncacn_ip_tcp:{self.remoteName}[{self.port!s}]"
6373
self.logger.debug(f"Creating WMI connection object to {connection_target}")
@@ -145,18 +155,15 @@ def print_host_info(self):
145155

146156
def check_if_admin(self):
147157
try:
148-
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
149-
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
158+
self.dcom_conn = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
159+
iInterface = self.dcom_conn.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
150160
flag, self.stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.rpc_timeout)
151161
except Exception as e:
152162
self.logger.debug(f"Received error while checking admin: {e}")
153-
if "dcom" in locals():
154-
dcom.disconnect()
155163
if "access_denied" not in str(e).lower():
156164
self.logger.fail(str(e))
157165
else:
158166
if not flag or not self.stringBinding:
159-
dcom.disconnect()
160167
error_msg = f'Check admin error: dcom initialization failed with stringbinding: "{self.stringBinding}", please try "--rpc-timeout" option. (probably is admin)'
161168

162169
if not self.stringBinding:
@@ -165,16 +172,14 @@ def check_if_admin(self):
165172
self.logger.fail(error_msg) if not flag else self.logger.debug(error_msg)
166173
else:
167174
try:
168-
iWbemLevel1Login = IWbemLevel1Login(iInterface)
169-
iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
175+
self.iWbemLevel1Login = IWbemLevel1Login(iInterface)
176+
_ = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
177+
self.iWbemLevel1Login.RemRelease()
170178
except Exception as e:
171-
dcom.disconnect()
172-
173179
if "access_denied" not in str(e).lower():
174180
self.logger.fail(str(e))
175181
return False
176182
else:
177-
dcom.disconnect()
178183
self.logger.extra["protocol"] = "WMI"
179184
self.admin_privs = True
180185

@@ -360,48 +365,60 @@ def hash_login(self, domain, username, ntlm_hash):
360365
self.logger.success(out)
361366
return True
362367

363-
# It's very complex to use wmi from rpctansport "convert" to dcom, so let we use dcom directly.
364368
@requires_admin
365-
def wmi(self, wql=None, namespace=None):
366-
"""Execute WQL syntax via WMI
367-
368-
This is done via the --wmi flag
369-
"""
369+
def wmi_query(self, wql=None, namespace=None, callback_func=None):
370370
records = []
371371
if not wql:
372-
wql = self.args.wmi.strip("\n")
372+
wql = self.args.wmi_query.strip("\n")
373373

374374
if not namespace:
375375
namespace = self.args.wmi_namespace
376376

377377
try:
378-
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
379-
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
380-
iWbemLevel1Login = IWbemLevel1Login(iInterface)
381-
iWbemServices = iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
382-
iWbemLevel1Login.RemRelease()
378+
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
379+
self.iWbemLevel1Login.RemRelease()
383380
iEnumWbemClassObject = iWbemServices.ExecQuery(wql)
384381
except Exception as e:
385-
dcom.disconnect()
386382
self.logger.debug(str(e))
387383
self.logger.fail(f"Execute WQL error: {e}")
388384
return False
389385
else:
390386
self.logger.info(f"Executing WQL syntax: {wql}")
391387
try:
392-
while True:
393-
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
394-
record = wmi_results.getProperties()
395-
records.append(record)
396-
for k, v in record.items():
397-
self.logger.highlight(f"{k} => {v['value']}")
388+
if not callback_func:
389+
while True:
390+
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
391+
record = wmi_results.getProperties()
392+
records.append(record)
393+
for k, v in record.items():
394+
self.logger.highlight(f"{k} => {v['value']}")
395+
else:
396+
callback_func(iEnumWbemClassObject, records)
398397
except Exception as e:
399398
if str(e).find("S_FALSE") < 0:
400399
self.logger.debug(e)
400+
return records
401401

402-
dcom.disconnect()
402+
def list_snapshots(self):
403+
drive = self.args.list_snapshots
404+
self.logger.info(f"Retrieving volume shadow copies of drive {drive}.")
405+
wql = "select ID, DeviceObject, ClientAccessible, InstallDate from win32_shadowcopy"
403406

404-
return records
407+
def callback_func(iEnumWbemClassObject, records):
408+
while True:
409+
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
410+
record = dict(wmi_results.getProperties())
411+
records.append(record)
412+
413+
snapshots = self.wmi_query(wql=wql, namespace="root\\cimv2", callback_func=callback_func)
414+
if not snapshots:
415+
self.logger.info("No volume shadow copies found.")
416+
return
417+
418+
self.logger.highlight(f"{'Drive':<8}{'Shadow Copy ID':<40}{'ClientAccessible':<18}{'InstallDate':<27}{'Device Object':<50}")
419+
self.logger.highlight(f"{'------':<8}{'--------------':<40}{'----------------':<18}{'-----------':<27}{'-------------':<50}")
420+
for record in snapshots:
421+
self.logger.highlight(f"{drive:<8}{record['ID']['value']:<40}{record['ClientAccessible']['value']:<18}{record['InstallDate']['value']:<27}{record['DeviceObject']['value']:<50}")
405422

406423
@requires_admin
407424
def execute(self, command=None, get_output=False, use_powershell=False):
@@ -422,14 +439,23 @@ def execute(self, command=None, get_output=False, use_powershell=False):
422439
return ""
423440

424441
if self.args.exec_method == "wmiexec":
425-
exec_method = wmiexec.WMIEXEC(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
426-
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
427-
442+
exec_method = wmiexec.WMIEXEC(
443+
self.remoteName,
444+
self.iWbemLevel1Login,
445+
self.logger,
446+
self.args.exec_timeout,
447+
self.args.codec
448+
)
428449
elif self.args.exec_method == "wmiexec-event":
429-
exec_method = wmiexec_event.WMIEXEC_EVENT(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
430-
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
450+
exec_method = wmiexec_event.WMIEXEC_EVENT(
451+
self.remoteName,
452+
self.iWbemLevel1Login,
453+
self.logger,
454+
self.args.exec_timeout,
455+
self.args.codec
456+
)
457+
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
431458

432-
self.conn.disconnect()
433459
if self.args.execute and get_output:
434460
self.logger.success(f'Executed command: "{command}" via {self.args.exec_method}')
435461
buf = StringIO(output).readlines()

nxc/protocols/wmi/proto_args.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ def proto_args(parser, parents):
99
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", default=None, type=str, help="Domain to authenticate to")
1010
dgroup.add_argument("--local-auth", action="store_true", help="Authenticate locally to each target")
1111

12+
cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering")
13+
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
14+
1215
egroup = wmi_parser.add_argument_group("Mapping/Enumeration")
13-
egroup.add_argument("--wmi", metavar="QUERY", dest="wmi", type=str, help="Issues the specified WMI query")
14-
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: root\\cimv2)")
16+
egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query")
17+
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: %(default)s)")
1518

1619
cgroup = wmi_parser.add_argument_group("Command Execution")
1720
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")

nxc/protocols/wmi/wmiexec.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,22 @@
1818
import base64
1919
from nxc.helpers.misc import gen_random_string
2020
from impacket.dcerpc.v5.dtypes import NULL
21-
from impacket.dcerpc.v5.dcomrt import DCOMConnection
22-
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
2321

2422

2523
class WMIEXEC:
26-
def __init__(self, target, username, password, domain, lmhash, nthash, doKerberos, kdcHost, remoteHost, aesKey, logger, exec_timeout, codec):
24+
def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec):
2725
self.__target = target
28-
self.__username = username
29-
self.__password = password
30-
self.__domain = domain
31-
self.__lmhash = lmhash
32-
self.__nthash = nthash
33-
self.__doKerberos = doKerberos
34-
self.__kdcHost = kdcHost
35-
self.__remoteHost = remoteHost
36-
self.__aesKey = aesKey
26+
self.__iWbemLevel1Login = iWbemLevel1Login
3727
self.logger = logger
3828
self.__exec_timeout = exec_timeout
3929
self.__registry_Path = ""
4030
self.__outputBuffer = ""
41-
4231
self.__shell = "cmd.exe /Q /c "
4332
self.__pwd = "C:\\"
4433
self.__codec = codec
4534

46-
self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, aesKey=self.__aesKey, remoteHost=self.__remoteHost)
47-
iInterface = self.__dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
48-
iWbemLevel1Login = IWbemLevel1Login(iInterface)
49-
self.__iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
50-
iWbemLevel1Login.RemRelease()
35+
self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
36+
self.__iWbemLevel1Login.RemRelease()
5137
self.__win32Process, _ = self.__iWbemServices.GetObject("Win32_Process")
5238

5339
def execute(self, command, output=False, use_powershell=False):
@@ -65,8 +51,6 @@ def execute(self, command, output=False, use_powershell=False):
6551
command = self.__shell + command
6652
self.execute_remote(command)
6753

68-
self.__dcom.disconnect()
69-
7054
return self.__outputBuffer
7155

7256
def execute_remote(self, command):

0 commit comments

Comments
 (0)