François REYNAUD 2023-07-21 17:03:33 +02:00
commit 6d4731cf93
30 changed files with 905 additions and 189 deletions

View File

@ -9,7 +9,7 @@ on:
jobs: jobs:
build: build:
name: CrackMapExec Tests on ${{ matrix.os }} name: CrackMapExec Tests for Py${{ matrix.python-version }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
max-parallel: 4 max-parallel: 4
@ -22,9 +22,14 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install librairies - name: Install poetry
run: | run: |
pip install . pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install librairies with dev group
run: |
poetry install --with dev
- name: Run the e2e test - name: Run the e2e test
run: | run: |
pytest tests poetry run pytest tests

View File

@ -8,10 +8,11 @@ from cme.loaders.protocolloader import ProtocolLoader
from cme.helpers.logger import highlight from cme.helpers.logger import highlight
from termcolor import colored from termcolor import colored
from cme.logger import cme_logger from cme.logger import cme_logger
import importlib.metadata
def gen_cli_args(): def gen_cli_args():
VERSION = "6.0.0" VERSION = importlib.metadata.version("crackmapexec")
CODENAME = "Bane" CODENAME = "Bane"
parser = argparse.ArgumentParser(description=f""" parser = argparse.ArgumentParser(description=f"""
@ -112,7 +113,7 @@ def gen_cli_args():
std_parser = argparse.ArgumentParser(add_help=False) std_parser = argparse.ArgumentParser(add_help=False)
std_parser.add_argument( std_parser.add_argument(
"target", "target",
nargs="+" if not module_parser.parse_known_args()[0].list_modules else "*", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*",
type=str, type=str,
help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)", help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)",
) )

View File

@ -51,8 +51,8 @@ class connection(object):
self.admin_privs = False self.admin_privs = False
self.password = "" self.password = ""
self.username = "" self.username = ""
self.kerberos = True if self.args.kerberos or self.args.use_kcache else False self.kerberos = True if self.args.kerberos or self.args.use_kcache or self.args.aesKey else False
self.aesKey = None if not self.args.aesKey else self.args.aesKey self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.failed_logins = 0 self.failed_logins = 0
@ -236,6 +236,7 @@ class connection(object):
secret.append(secret_single) secret.append(secret_single)
cred_type.append(cred_type_single) cred_type.append(cred_type_single)
if len(secret) != len(data): data = [None] * len(secret)
return domain, username, owned, secret, cred_type, data return domain, username, owned, secret, cred_type, data
def parse_credentials(self): def parse_credentials(self):
@ -324,6 +325,10 @@ class connection(object):
return False return False
if self.args.continue_on_success and owned: if self.args.continue_on_success and owned:
return False return False
# Enforcing FQDN for SMB if not using local authentication. Related issues/PRs: #26, #28, #24, #38
if self.args.protocol == 'smb' and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "":
self.logger.error(f"Domain {domain} for user {username.rstrip()} need to be FQDN ex:domain.local, not domain")
return False
with sem: with sem:
if cred_type == 'plaintext': if cred_type == 'plaintext':

View File

@ -28,6 +28,18 @@ from sys import exit
import logging import logging
import sqlalchemy import sqlalchemy
from rich.progress import Progress from rich.progress import Progress
from sys import platform
# Increase file_limit to prevent error "Too many open files"
if platform != "win32":
import resource
file_limit = list(resource.getrlimit(resource.RLIMIT_NOFILE))
if file_limit[1] > 10000:
file_limit[0] = 10000
else:
file_limit[0] = file_limit[1]
file_limit = tuple(file_limit)
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
try: try:
import librlers import librlers

View File

@ -13,7 +13,7 @@ from impacket.dcerpc.v5.dcomrt import IObjectExporter
class CMEModule: class CMEModule:
name = "ioxidresolver" name = "ioxidresolver"
description = "Thie module helps you to identify hosts that have additional active interfaces" description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb"] supported_protocols = ["smb"]
opsec_safe = True opsec_safe = True
multiple_hosts = False multiple_hosts = False

View File

@ -27,13 +27,17 @@ class CMEModule:
def options(self, context, module_options): def options(self, context, module_options):
""" """
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
BASE_DN The base domain name for the LDAP query
""" """
self.context = context self.context = context
self.regex = re.compile("(https?://.+)") self.regex = re.compile("(https?://.+)")
self.server = None self.server = None
self.base_dn = None
if module_options and "SERVER" in module_options: if module_options and "SERVER" in module_options:
self.server = module_options["SERVER"] self.server = module_options["SERVER"]
if module_options and "BASE_DN" in module_options:
self.base_dn = module_options["BASE_DN"]
def on_login(self, context, connection): def on_login(self, context, connection):
""" """
@ -49,7 +53,7 @@ class CMEModule:
try: try:
sc = ldap.SimplePagedResultsControl() sc = ldap.SimplePagedResultsControl()
base_dn_root = ",".join(connection.ldapConnection._baseDN.split(",")[-2:]) base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
if self.server is None: if self.server is None:
resp = connection.ldapConnection.search( resp = connection.ldapConnection.search(

307
cme/modules/add_computer.py Normal file
View File

@ -0,0 +1,307 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ldap3
from impacket.dcerpc.v5 import samr, epm, transport
class CMEModule:
'''
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
Thanks to the guys at impacket for the original code
'''
name = 'add-computer'
description = 'Adds or deletes a domain computer'
supported_protocols = ['smb']
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
'''
add-computer: Specify add-computer to call the module using smb
NAME: Specify the NAME option to name the Computer to be added
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
DELETE: Specify DELETE to remove a Computer
CHANGEPW: Specify CHANGEPW to modify a Computer password
Usage: cme smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
cme smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
cme smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
'''
self.__baseDN = None
self.__computerGroup = None
self.__method = "SAMR"
self.__noAdd = False
self.__delete = False
self.noLDAPRequired = False
if 'DELETE' in module_options:
self.__delete = True
if 'CHANGEPW' in module_options and ('NAME' not in module_options or 'PASSWORD' not in module_options):
context.log.error('NAME and PASSWORD options are required!')
elif 'CHANGEPW' in module_options:
self.__noAdd = True
if 'NAME' in module_options:
self.__computerName = module_options['NAME']
if self.__computerName[-1] != '$':
self.__computerName += '$'
else:
context.log.error('NAME option is required!')
exit(1)
if 'PASSWORD' in module_options:
self.__computerPassword = module_options['PASSWORD']
elif 'PASSWORD' not in module_options and not self.__delete:
context.log.error('PASSWORD option is required!')
exit(1)
def on_login(self, context, connection):
#Set some variables
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__username = connection.username
self.__password = connection.password
self.__targetIp = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
self.__doKerberos = connection.kerberos
self.__nthash = ""
self.__lmhash = ""
if context.hash and ":" in context.hash[0]:
hashList = context.hash[0].split(":")
self.__nthash = hashList[-1]
self.__lmhash = hashList[0]
elif context.hash and ":" not in context.hash[0]:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
# First try to add via SAMR over SMB
self.doSAMRAdd(context)
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.doLDAPSAdd(connection,context)
else:
exit(1)
def doSAMRAdd(self,context):
if self.__targetIp is not None:
stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
else:
stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.set_dport(self.__port)
if self.__targetIp is not None:
rpctransport.setRemoteHost(self.__targetIp)
rpctransport.setRemoteName(self.__target)
if hasattr(rpctransport, 'set_credentials'):
# This method exists only for selected protocol sequences.
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()
servHandle = None
domainHandle = None
userHandle = None
try:
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target,
samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN )
servHandle = samrConnectResponse['ServerHandle']
samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle)
domains = samrEnumResponse['Buffer']['Buffer']
domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains))
if len(domainsWithoutBuiltin) > 1:
domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains))
if len(domain) != 1:
context.log.highlight(u'{}'.format(
'This domain does not exist: "' + self.__domainNetbios + '"'))
logging.critical("Available domain(s):")
for domain in domains:
logging.error(" * %s" % domain['Name'])
raise Exception()
else:
selectedDomain = domain[0]['Name']
else:
selectedDomain = domainsWithoutBuiltin[0]['Name']
samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain)
domainSID = samrLookupDomainResponse['DomainId']
if logging.getLogger().level == logging.DEBUG:
logging.info("Opening domain %s..." % selectedDomain)
samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID)
domainHandle = samrOpenDomainResponse['DomainHandle']
if self.__noAdd or self.__delete:
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000073:
context.log.highlight(u'{}'.format(
self.__computerName + ' not found in domain ' + selectedDomain))
self.noLDAPRequired = True
raise Exception()
else:
raise
userRID = checkForUser['RelativeIds']['Element'][0]
if self.__delete:
access = samr.DELETE
message = "delete"
else:
access = samr.USER_FORCE_PASSWORD_CHANGE
message = "set the password for"
try:
openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID)
userHandle = openUser['UserHandle']
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000022:
context.log.highlight(u'{}'.format(
self.__username + ' does not have the right to ' + message + " " + self.__computerName))
self.noLDAPRequired = True
raise Exception()
else:
raise
else:
if self.__computerName is not None:
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight(u'{}'.format(
'Computer account already exists with the name: "' + self.__computerName + '"'))
raise Exception()
except samr.DCERPCSessionError as e:
if e.error_code != 0xc0000073:
raise
else:
foundUnused = False
while not foundUnused:
self.__computerName = self.generateComputerName()
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000073:
foundUnused = True
else:
raise
try:
createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,)
self.noLDAPRequired = True
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000022:
context.log.highlight(u'{}'.format(
'The following user does not have the right to create a computer account: "' + self.__username + '"'))
raise Exception()
elif e.error_code == 0xc00002e7:
context.log.highlight(u'{}'.format(
'The following user exceeded their machine account quota: "' + self.__username + '"'))
raise Exception()
else:
raise
userHandle = createUser['UserHandle']
if self.__delete:
samr.hSamrDeleteUser(dce, userHandle)
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
self.noLDAPRequired=True
userHandle = None
else:
samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword)
if self.__noAdd:
context.log.highlight(u'{}'.format(
'Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
self.noLDAPRequired=True
else:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
userRID = checkForUser['RelativeIds']['Element'][0]
openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID)
userHandle = openUser['UserHandle']
req = samr.SAMPR_USER_INFO_BUFFER()
req['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation
req['Control']['UserAccountControl'] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, userHandle, req)
if not self.noLDAPRequired:
context.log.highlight(u'{}'.format(
'Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
self.noLDAPRequired = True
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
finally:
if userHandle is not None:
samr.hSamrCloseHandle(dce, userHandle)
if domainHandle is not None:
samr.hSamrCloseHandle(dce, domainHandle)
if servHandle is not None:
samr.hSamrCloseHandle(dce, servHandle)
dce.disconnect()
def doLDAPSAdd(self, connection, context):
ldap_domain = connection.domain.replace(".", ",dc=")
spns = [
'HOST/%s' % self.__computerName,
'HOST/%s.%s' % (self.__computerName, connection.domain),
'RestrictedKrbHost/%s' % self.__computerName,
'RestrictedKrbHost/%s.%s' % (self.__computerName, connection.domain),
]
ucd = {
'dnsHostName': '%s.%s' % (self.__computerName, connection.domain),
'userAccountControl': 0x1000,
'servicePrincipalName': spns,
'sAMAccountName': self.__computerName,
'unicodePwd': ('"%s"' % self.__computerPassword).encode('utf-16-le')
}
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers='ALL:@SECLEVEL=0')
ldapServer = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
c = Connection(ldapServer, connection.username + '@' + connection.domain, connection.password)
c.bind()
if (self.__delete):
result = c.delete("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain)
if result:
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
elif result == False and c.last_error == "noSuchObject":
context.log.highlight(u'{}'.format('Computer named "' + self.__computerName + '" was not found'))
elif result == False and c.last_error == "insufficientAccessRights":
context.log.highlight(
u'{}'.format('Insufficient Access Rights to delete the Computer "' + self.__computerName + '"'))
else:
context.log.highlight(u'{}'.format(
'Unable to delete the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
else:
result = c.add("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain,
['top', 'person', 'organizationalPerson', 'user', 'computer'], ucd)
if result:
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
context.log.highlight(u'{}'.format('You can try to verify this with the CME command:'))
context.log.highlight(u'{}'.format(
'cme ldap ' + connection.host + ' -u ' + connection.username + ' -p ' + connection.password + ' -M group-mem -o GROUP="Domain Computers"'))
elif result == False and c.last_error == "entryAlreadyExists":
context.log.highlight(u'{}'.format('The Computer account "' + self.__computerName + '" already exists'))
elif not result:
context.log.highlight(u'{}'.format(
'Unable to add the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
c.unbind()

85
cme/modules/comp_desc.py Normal file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import sys
class CMEModule:
'''
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
'''
name = 'comp-desc'
description = 'Retrieves computers containing the specified description'
supported_protocols = ['ldap']
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
'''
comp-desc: Specify comp-desc to call the module
DESC: Specify the DESC option to enter your description text to search for
Usage: cme ldap $DC-IP -u Username -p Password -M comp-desc -o DESC="server"
cme ldap $DC-IP -u Username -p Password -M comp-desc -o DESC="XP"
'''
self.DESC = ''
if 'DESC' in module_options:
self.DESC = module_options['DESC']
else:
context.log.error('DESC option is required!')
exit(1)
def on_login(self, context, connection):
# Building the search filter
searchFilter = "(&(objectCategory=computer)(operatingSystem=*"+self.DESC+"*))"
try:
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=['dNSHostName','operatingSystem'],
sizeLimit=0)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find('sizeLimitExceeded') >= 0:
context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
resp = e.getAnswers()
pass
else:
logging.debug(e)
return False
answers = []
context.log.debug('Total no. of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
dNSHostName = ''
operatingSystem = ''
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'dNSHostName':
dNSHostName = str(attribute['vals'][0])
elif str(attribute['type']) == 'operatingSystem':
operatingSystem = attribute['vals'][0]
if dNSHostName != '' and operatingSystem != '':
answers.append([dNSHostName,operatingSystem])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers) > 0:
context.log.success('Found the following computers: ')
for answer in answers:
try:
IP = socket.gethostbyname(answer[0])
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],IP))
context.log.debug('IP found')
except socket.gaierror as e:
context.log.debug('Missing IP')
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],"No IP Found"))
else:
context.log.success('Unable to find any computers with the description "' + self.DESC + '"')

View File

@ -41,6 +41,7 @@ class CMEModule:
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
doKerberos=connection.kerberos, doKerberos=connection.kerberos,
dcHost=connection.kdcHost, dcHost=connection.kdcHost,
aesKey=connection.aesKey,
) )
if dce is not None: if dce is not None:
@ -103,7 +104,7 @@ class NetrDfsAddRootResponse(NDRCALL):
class TriggerAuth: class TriggerAuth:
def connect(self, username, password, domain, lmhash, nthash, target, doKerberos, dcHost): def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target) rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
if hasattr(rpctransport, "set_credentials"): if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials( rpctransport.set_credentials(
@ -112,6 +113,7 @@ class TriggerAuth:
domain=domain, domain=domain,
lmhash=lmhash, lmhash=lmhash,
nthash=nthash, nthash=nthash,
aesKey=aesKey,
) )
if doKerberos: if doKerberos:

View File

@ -46,6 +46,7 @@ class CMEModule:
connection.domain, connection.domain,
connection.lmhash, connection.lmhash,
connection.nthash, connection.nthash,
connection.aesKey,
) )
dce, rpctransport = lsa.connect() dce, rpctransport = lsa.connect()
policyHandle = lsa.open_policy(dce) policyHandle = lsa.open_policy(dce)
@ -54,7 +55,7 @@ class CMEModule:
for service in product["services"]: for service in product["services"]:
try: try:
lsa.LsarLookupNames(dce, policyHandle, service["name"]) lsa.LsarLookupNames(dce, policyHandle, service["name"])
context.log.display(f"Detected installed service on {connection.host}: {product['name']} {service['description']}") context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
if product["name"] not in results: if product["name"] not in results:
results[product["name"]] = {"services": []} results[product["name"]] = {"services": []}
results[product["name"]]["services"].append(service) results[product["name"]]["services"].append(service)
@ -64,7 +65,7 @@ class CMEModule:
except Exception as e: except Exception as e:
context.log.fail(str(e)) context.log.fail(str(e))
context.log.display(f"Detecting running processes on {connection.host} by enumerating pipes...") context.log.info(f"Detecting running processes on {connection.host} by enumerating pipes...")
try: try:
for f in connection.conn.listPath("IPC$", "\\*"): for f in connection.conn.listPath("IPC$", "\\*"):
fl = f.get_longname() fl = f.get_longname()
@ -124,6 +125,7 @@ class LsaLookupNames:
kdcHost="", kdcHost="",
lmhash="", lmhash="",
nthash="", nthash="",
aesKey="",
): ):
self.domain = domain self.domain = domain
self.username = username self.username = username
@ -133,6 +135,7 @@ class LsaLookupNames:
self.doKerberos = k self.doKerberos = k
self.lmhash = lmhash self.lmhash = lmhash
self.nthash = nthash self.nthash = nthash
self.aesKey = aesKey
self.dcHost = kdcHost self.dcHost = kdcHost
def connect(self, string_binding=None, iface_uuid=None): def connect(self, string_binding=None, iface_uuid=None):
@ -154,7 +157,7 @@ class LsaLookupNames:
# Authenticate if specified # Authenticate if specified
if self.authn and hasattr(rpc_transport, "set_credentials"): if self.authn and hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences. # This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash) rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos: if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost) rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
class CMEModule:
'''
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
'''
name = 'group-mem'
description = 'Retrieves all the members within a Group'
supported_protocols = ['ldap']
opsec_safe = True
multiple_hosts = False
primaryGroupID = ''
answers = []
def options(self, context, module_options):
'''
group-mem: Specify group-mem to call the module
GROUP: Specify the GROUP option to query for that group's members
Usage: cme ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
cme ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain controllers"
'''
self.GROUP = ''
if 'GROUP' in module_options:
self.GROUP = module_options['GROUP']
else:
context.log.error('GROUP option is required!')
exit(1)
def on_login(self, context, connection):
#First look up the SID of the group passed in
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "objectSid"
searchResult = doSearch(self, context, connection, searchFilter, attribute)
#If no SID for the Group is returned exit the program
if searchResult is None:
context.log.success('Unable to find any members of the "' + self.GROUP + '" group')
return True
# Convert the binary SID to a primaryGroupID string to be used further
sidString = connection.sid_to_str(searchResult).split("-")
self.primaryGroupID = sidString[-1]
#Look up the groups DN
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "distinguishedName"
distinguishedName = (doSearch(self, context, connection, searchFilter, attribute)).decode("utf-8")
# Carry out the search
searchFilter = "(|(memberOf="+distinguishedName+")(primaryGroupID="+self.primaryGroupID+"))"
attribute = "sAMAccountName"
searchResult = doSearch(self, context, connection, searchFilter, attribute)
if len(self.answers) > 0:
context.log.success('Found the following members of the ' + self.GROUP + ' group:')
for answer in self.answers:
context.log.highlight(u'{}'.format(answer[0]))
# Carry out an LDAP search for the Group with the supplied Group name
def doSearch(self,context, connection,searchFilter,attributeName):
try:
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=[attributeName],
sizeLimit=0)
context.log.debug('Total no. of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
attributeValue = '';
try:
for attribute in item['attributes']:
if str(attribute['type']) == attributeName:
if attributeName == "objectSid":
attributeValue = bytes(attribute['vals'][0])
return attributeValue;
elif attributeName == "distinguishedName":
attributeValue = bytes(attribute['vals'][0])
return attributeValue;
else:
attributeValue = str(attribute['vals'][0])
if attributeValue is not None:
self.answers.append([attributeValue])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
pass
except Exception as e:
context.log.debug("Exception:", e)
return False

View File

@ -147,7 +147,15 @@ class CMEModule:
self.reset = None self.reset = None
self.reset_dumped = None self.reset_dumped = None
self.method = None self.method = None
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_computers(connection.host)[0][0]
if password is not None:
credential_type = 'plaintext'
else:
credential_type = 'hash'
password = ':'.join(h for h in [lmhash, nthash] if h is not None)
context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id)
def options(self, context, module_options): def options(self, context, module_options):
""" """
METHOD Method to use to dump lsass.exe with lsassy METHOD Method to use to dump lsass.exe with lsassy
@ -222,6 +230,7 @@ class CMEModule:
] ]
) )
credentials_output.append(cred) credentials_output.append(cred)
self.save_credentials(context, connection, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"])
global credentials_data global credentials_data
credentials_data = credentials_output credentials_data = credentials_output

View File

@ -3,7 +3,7 @@
import json import json
from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldapasn1 as ldapasn1_impacket
from cme.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
class CMEModule: class CMEModule:
""" """
@ -49,21 +49,35 @@ class CMEModule:
for computer in results: for computer in results:
msMCSAdmPwd = "" msMCSAdmPwd = ""
sAMAccountName = "" sAMAccountName = ""
values = {str(attr["type"]).lower(): str(attr["vals"][0]) for attr in computer["attributes"]} values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values: if "mslaps-encryptedpassword" in values:
context.log.fail("LAPS password is encrypted and currently CrackMapExec doesn't" " support the decryption...") msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(
bytes(msMCSAdmPwd),
connection.username if connection.username else "",
connection.password if connection.password else "",
connection.domain,
connection.nthash if connection.nthash else "",
connection.kerberos,
connection.kdcHost,
339)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return return
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values: elif "mslaps-password" in values:
r = json.loads(values["mslaps-password"]) r = json.loads(str(values["mslaps-password"]))
laps_computers.append((values["samaccountname"], r["n"], r["p"])) laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "ms-mcs-admpwd" in values: elif "ms-mcs-admpwd" in values:
laps_computers.append((values["samaccountname"], "", values["ms-mcs-admpwd"])) laps_computers.append((str(values["samaccountname"]), "", str(values["ms-mcs-admpwd"])))
else: else:
context.log.fail("No result found with attribute ms-MCS-AdmPwd or" " msLAPS-Password") context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
laps_computers = sorted(laps_computers, key=lambda x: x[0]) laps_computers = sorted(laps_computers, key=lambda x: x[0])
for sAMAccountName, user, msMCSAdmPwd in laps_computers: for sAMAccountName, user, password in laps_computers:
context.log.highlight("Computer: {:<20} User: {:<15} Password: {}".format(sAMAccountName, user, msMCSAdmPwd)) context.log.highlight("Computer:{} User:{:<15} Password:{}".format(sAMAccountName, user, password))
else: else:
context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password !") context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password !")

View File

@ -128,6 +128,8 @@ class CMEModule:
credz_bh = [] credz_bh = []
domain = None domain = None
for cred in credentials: for cred in credentials:
if cred["domain"] == None:
cred["domain"] = ""
domain = cred["domain"] domain = cred["domain"]
if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper(): if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper():
domain = connection.domain # slim shady domain = connection.domain # slim shady

View File

@ -45,6 +45,7 @@ class CMEModule:
domain=connection.domain, domain=connection.domain,
lmhash=connection.lmhash, lmhash=connection.lmhash,
nthash=connection.nthash, nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe=self.pipe, pipe=self.pipe,
do_kerberos=connection.kerberos, do_kerberos=connection.kerberos,
@ -195,6 +196,7 @@ def coerce(
domain, domain,
lmhash, lmhash,
nthash, nthash,
aesKey,
target, target,
pipe, pipe,
do_kerberos, do_kerberos,
@ -232,6 +234,7 @@ def coerce(
domain=domain, domain=domain,
lmhash=lmhash, lmhash=lmhash,
nthash=nthash, nthash=nthash,
aesKey=aesKey,
) )
if target_ip: if target_ip:

View File

@ -56,6 +56,7 @@ class CMEModule:
connection.domain, connection.domain,
connection.lmhash, connection.lmhash,
connection.nthash, connection.nthash,
connection.aesKey,
) )
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)

View File

@ -45,6 +45,7 @@ class CMEModule:
domain=connection.domain, domain=connection.domain,
lmhash=connection.lmhash, lmhash=connection.lmhash,
nthash=connection.nthash, nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe="FssagentRpc", pipe="FssagentRpc",
doKerberos=connection.kerberos, doKerberos=connection.kerberos,
@ -62,6 +63,7 @@ class CMEModule:
domain=connection.domain, domain=connection.domain,
lmhash=connection.lmhash, lmhash=connection.lmhash,
nthash=connection.nthash, nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe="FssagentRpc", pipe="FssagentRpc",
) )
@ -194,6 +196,7 @@ class CoerceAuth:
domain, domain,
lmhash, lmhash,
nthash, nthash,
aesKey,
target, target,
pipe, pipe,
doKerberos, doKerberos,
@ -215,6 +218,7 @@ class CoerceAuth:
domain=domain, domain=domain,
lmhash=lmhash, lmhash=lmhash,
nthash=nthash, nthash=nthash,
aesKey=aesKey,
) )
dce.set_credentials(*rpctransport.get_credentials()) dce.set_credentials(*rpctransport.get_credentials())

View File

@ -27,6 +27,7 @@ class CMEModule:
""" """
self.showservers = True self.showservers = True
self.base_dn = None
if module_options and "SHOWSERVERS" in module_options: if module_options and "SHOWSERVERS" in module_options:
if module_options["SHOWSERVERS"].lower() == "true" or module_options["SHOWSERVERS"] == "1": if module_options["SHOWSERVERS"].lower() == "true" or module_options["SHOWSERVERS"] == "1":
@ -35,6 +36,8 @@ class CMEModule:
self.showservers = False self.showservers = False
else: else:
print("Could not parse showservers option.") print("Could not parse showservers option.")
if module_options and "BASE_DN" in module_options:
self.base_dn = module_options["BASE_DN"]
name = "subnets" name = "subnets"
description = "Retrieves the different Sites and Subnets of an Active Directory" description = "Retrieves the different Sites and Subnets of an Active Directory"
@ -43,16 +46,20 @@ class CMEModule:
multiple_hosts = False multiple_hosts = False
def on_login(self, context, connection): def on_login(self, context, connection):
dn = ",".join(["DC=%s" % part for part in connection.domain.split(".")][-2:]) dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
context.log.display("Getting the Sites and Subnets from domain") context.log.display("Getting the Sites and Subnets from domain")
try:
list_sites = connection.ldapConnection.search( list_sites = connection.ldapConnection.search(
searchBase="CN=Configuration,%s" % dn, searchBase="CN=Configuration,%s" % dn,
searchFilter="(objectClass=site)", searchFilter="(objectClass=site)",
attributes=["distinguishedName", "name", "description"], attributes=["distinguishedName", "name", "description"],
sizeLimit=999, sizeLimit=999,
) )
except LDAPSearchError as e:
context.log.fail(str(e))
exit()
for site in list_sites: for site in list_sites:
if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True: if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True:
continue continue

View File

@ -292,8 +292,7 @@ class ldap(connection):
# Re-connect since we logged off # Re-connect since we logged off
self.create_conn_obj() self.create_conn_obj()
self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
self.output_filename = self.output_filename.replace(":", "-")
def print_host_info(self): def print_host_info(self):
self.logger.debug("Printing host info for LDAP") self.logger.debug("Printing host info for LDAP")
@ -738,17 +737,20 @@ class ldap(connection):
try: try:
if self.ldapConnection: if self.ldapConnection:
self.logger.debug(f"Search Filter={searchFilter}") self.logger.debug(f"Search Filter={searchFilter}")
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
resp = self.ldapConnection.search( resp = self.ldapConnection.search(
searchFilter=searchFilter, searchFilter=searchFilter,
attributes=attributes, attributes=attributes,
sizeLimit=sizeLimit, sizeLimit=sizeLimit,
searchControls=[paged_search_control],
) )
return resp return resp
except ldap_impacket.LDAPSearchError as e: except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0: if e.getErrorString().find("sizeLimitExceeded") >= 0:
# We should never reach this code as we use paged search now
self.logger.fail("sizeLimitExceeded exception caught, giving up and processing the data received") self.logger.fail("sizeLimitExceeded exception caught, giving up and processing the data received")
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
resp = e.getAnswers() resp = e.getAnswers()
pass pass
else: else:

View File

@ -29,7 +29,7 @@ class MSSQLEXEC:
if output: if output:
cme_logger.debug(f"Output is enabled") cme_logger.debug(f"Output is enabled")
for row in command_output: for row in command_output:
cme_logger.display(row) cme_logger.debug(row)
# self.mssql_conn.printReplies() # self.mssql_conn.printReplies()
# self.mssql_conn.colMeta[0]["TypeData"] = 80 * 2 # self.mssql_conn.colMeta[0]["TypeData"] = 80 * 2
# self.mssql_conn.printRows() # self.mssql_conn.printRows()

View File

@ -133,8 +133,7 @@ class rdp(connection):
self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"]) self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"])
self.logger.extra["hostname"] = self.hostname self.logger.extra["hostname"] = self.hostname
self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
self.output_filename = self.output_filename.replace(":", "-")
break break
if self.args.domain: if self.args.domain:

View File

@ -234,8 +234,7 @@ class smb(connection):
pass pass
self.os_arch = self.get_os_arch() self.os_arch = self.get_os_arch()
self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
self.output_filename = self.output_filename.replace(":", "-")
if not self.domain: if not self.domain:
self.domain = self.hostname self.domain = self.hostname
@ -322,7 +321,11 @@ class smb(connection):
self.args.kerberos, self.args.kerberos,
self.args.kdcHost, self.args.kdcHost,
339) 339)
try:
data = d.run() data = d.run()
except Exception as e:
self.logger.fail(str(e))
return
r = loads(data) r = loads(data)
msMCSAdmPwd = r["p"] msMCSAdmPwd = r["p"]
username_laps = r["n"] username_laps = r["n"]
@ -405,8 +408,8 @@ class smb(connection):
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
else: else:
self.plaintext_login(username, password, self.host) self.plaintext_login(self.hostname, username, password)
return return True
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}" out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
self.logger.success(out) self.logger.success(out)
@ -603,7 +606,11 @@ class smb(connection):
) )
self.smbv1 = False self.smbv1 = False
except socket.error as e: except socket.error as e:
# This should not happen anymore!!!
if str(e).find("Too many open files") != -1: if str(e).find("Too many open files") != -1:
if not self.logger:
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
self.proto_logger()
self.logger.fail(f"SMBv3 connection error on {self.host if not kdc else kdc}: {e}") self.logger.fail(f"SMBv3 connection error on {self.host if not kdc else kdc}: {e}")
return False return False
except (Exception, NetBIOSTimeout) as e: except (Exception, NetBIOSTimeout) as e:
@ -626,7 +633,10 @@ class smb(connection):
except: except:
pass pass
else: else:
try:
dce.bind(scmr.MSRPC_UUID_SCMR) dce.bind(scmr.MSRPC_UUID_SCMR)
except:
pass
try: try:
# 0xF003F - SC_MANAGER_ALL_ACCESS # 0xF003F - SC_MANAGER_ALL_ACCESS
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx
@ -689,7 +699,8 @@ class smb(connection):
self.password, self.password,
self.domain, self.domain,
self.conn, self.conn,
self.hash, self.args.share,
self.hash
) )
self.logger.info("Executed command via mmcexec") self.logger.info("Executed command via mmcexec")
break break
@ -709,6 +720,7 @@ class smb(connection):
self.aesKey, self.aesKey,
self.kdcHost, self.kdcHost,
self.hash, self.hash,
self.logger
) # self.args.share) ) # self.args.share)
self.logger.info("Executed command via atexec") self.logger.info("Executed command via atexec")
break break
@ -731,6 +743,7 @@ class smb(connection):
self.kdcHost, self.kdcHost,
self.hash, self.hash,
self.args.share, self.args.share,
self.args.port,
self.logger self.logger
) )
self.logger.info("Executed command via smbexec") self.logger.info("Executed command via smbexec")
@ -1001,7 +1014,7 @@ class smb(connection):
groups = SamrFunc(self).get_local_groups() groups = SamrFunc(self).get_local_groups()
if groups: if groups:
self.logger.success("Enumerated local groups") self.logger.success("Enumerated local groups")
self.logger.display(f"Local groups: {groups}") self.logger.debug(f"Local groups: {groups}")
for group_name, group_rid in groups.items(): for group_name, group_rid in groups.items():
self.logger.highlight(f"rid => {group_rid} => {group_name}") self.logger.highlight(f"rid => {group_rid} => {group_name}")
@ -1274,7 +1287,7 @@ class smb(connection):
if hasattr(rpc_transport, "set_credentials"): if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences. # This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash) rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.kerberos: if self.kerberos:
rpc_transport.set_kerberos(self.kerberos, self.kdcHost) rpc_transport.set_kerberos(self.kerberos, self.kdcHost)

View File

@ -5,7 +5,7 @@ import os
import logging import logging
from impacket.dcerpc.v5 import tsch, transport from impacket.dcerpc.v5 import tsch, transport
from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from cme.helpers.misc import gen_random_string from cme.helpers.misc import gen_random_string
from cme.logger import cme_logger from cme.logger import cme_logger
from time import sleep from time import sleep
@ -23,6 +23,7 @@ class TSCH_EXEC:
aesKey=None, aesKey=None,
kdcHost=None, kdcHost=None,
hashes=None, hashes=None,
logger=cme_logger
): ):
self.__target = target self.__target = target
self.__username = username self.__username = username
@ -36,6 +37,7 @@ class TSCH_EXEC:
self.__aesKey = aesKey self.__aesKey = aesKey
self.__doKerberos = doKerberos self.__doKerberos = doKerberos
self.__kdcHost = kdcHost self.__kdcHost = kdcHost
self.logger = logger
if hashes is not None: if hashes is not None:
# This checks to see if we didn't provide the LM Hash # This checks to see if we didn't provide the LM Hash
@ -147,6 +149,7 @@ class TSCH_EXEC:
dce.set_credentials(*self.__rpctransport.get_credentials()) dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect() dce.connect()
# dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY) # dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS) dce.bind(tsch.MSRPC_UUID_TSCHS)
tmpName = gen_random_string(8) tmpName = gen_random_string(8)
tmpFileName = tmpName + ".tmp" tmpFileName = tmpName + ".tmp"
@ -156,7 +159,11 @@ class TSCH_EXEC:
logging.info(f"Task XML: {xml}") logging.info(f"Task XML: {xml}")
taskCreated = False taskCreated = False
logging.info(f"Creating task \\{tmpName}") logging.info(f"Creating task \\{tmpName}")
try:
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
self.logger.fail(str(e))
return
taskCreated = True taskCreated = True
logging.info(f"Running task \\{tmpName}") logging.info(f"Running task \\{tmpName}")

View File

@ -60,7 +60,7 @@ from impacket.dcerpc.v5.dtypes import NULL
class MMCEXEC: class MMCEXEC:
def __init__(self, host, share_name, username, password, domain, smbconnection, hashes=None): def __init__(self, host, share_name, username, password, domain, smbconnection, share, hashes=None):
self.__host = host self.__host = host
self.__username = username self.__username = username
self.__password = password self.__password = password
@ -76,10 +76,12 @@ class MMCEXEC:
self.__quit = None self.__quit = None
self.__executeShellCommand = None self.__executeShellCommand = None
self.__retOutput = True self.__retOutput = True
self.__share = share
self.__dcom = None
if hashes is not None: if hashes is not None:
self.__lmhash, self.__nthash = hashes.split(":") self.__lmhash, self.__nthash = hashes.split(":")
dcom = DCOMConnection( self.__dcom = DCOMConnection(
self.__host, self.__host,
self.__username, self.__username,
self.__password, self.__password,
@ -90,7 +92,7 @@ class MMCEXEC:
oxidResolver=True, oxidResolver=True,
) )
try: try:
iInterface = dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch) iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
iMMC = IDispatch(iInterface) iMMC = IDispatch(iInterface)
resp = iMMC.GetIDsOfNames(("Document",)) resp = iMMC.GetIDsOfNames(("Document",))
@ -117,20 +119,20 @@ class MMCEXEC:
except Exception as e: except Exception as e:
self.exit() self.exit()
logging.error(str(e)) logging.error(str(e))
dcom.disconnect() self.__dcom.disconnect()
def getInterface(self, interface, resp): def getInterface(self, interface, resp):
# Now let's parse the answer and build an Interface instance # Now let's parse the answer and build an Interface instance
objRefType = OBJREF("".join(resp))["flags"] objRefType = OBJREF(b"".join(resp))["flags"]
objRef = None objRef = None
if objRefType == FLAGS_OBJREF_CUSTOM: if objRefType == FLAGS_OBJREF_CUSTOM:
objRef = OBJREF_CUSTOM("".join(resp)) objRef = OBJREF_CUSTOM(b"".join(resp))
elif objRefType == FLAGS_OBJREF_HANDLER: elif objRefType == FLAGS_OBJREF_HANDLER:
objRef = OBJREF_HANDLER("".join(resp)) objRef = OBJREF_HANDLER(b"".join(resp))
elif objRefType == FLAGS_OBJREF_STANDARD: elif objRefType == FLAGS_OBJREF_STANDARD:
objRef = OBJREF_STANDARD("".join(resp)) objRef = OBJREF_STANDARD(b"".join(resp))
elif objRefType == FLAGS_OBJREF_EXTENDED: elif objRefType == FLAGS_OBJREF_EXTENDED:
objRef = OBJREF_EXTENDED("".join(resp)) objRef = OBJREF_EXTENDED(b"".join(resp))
else: else:
logging.error("Unknown OBJREF Type! 0x%x" % objRefType) logging.error("Unknown OBJREF Type! 0x%x" % objRefType)
@ -150,6 +152,7 @@ class MMCEXEC:
self.__retOutput = output self.__retOutput = output
self.execute_remote(command) self.execute_remote(command)
self.exit() self.exit()
self.__dcom.disconnect()
return self.__outputBuffer return self.__outputBuffer
def exit(self): def exit(self):
@ -163,12 +166,11 @@ class MMCEXEC:
return True return True
def execute_remote(self, data): def execute_remote(self, data):
self.__output = gen_random_string(6) self.__output = "\\Windows\\Temp\\" + gen_random_string(6)
local_ip = self.__smbconnection.getSMBServer().get_socket().getsockname()[0]
command = "/Q /c " + data command = self.__shell + " /Q /c " + data
if self.__retOutput is True: if self.__retOutput is True:
command += " 1> " + f"\\\\{local_ip}\\{self.__share_name}\\{self.__output}" + " 2>&1" command += " 1> " + f"{self.__output}" + " 2>&1"
dispParams = DISPPARAMS(None, False) dispParams = DISPPARAMS(None, False)
dispParams["rgdispidNamedArgs"] = NULL dispParams["rgdispidNamedArgs"] = NULL
@ -203,7 +205,7 @@ class MMCEXEC:
dispParams["rgvarg"].append(arg0) dispParams["rgvarg"].append(arg0)
self.__executeShellCommand[0].Invoke(self.__executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, 0, [], []) self.__executeShellCommand[0].Invoke(self.__executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, 0, [], [])
self.get_output_fileless() self.get_output_remote()
def output_callback(self, data): def output_callback(self, data):
self.__outputBuffer += data self.__outputBuffer += data
@ -219,3 +221,22 @@ class MMCEXEC:
break break
except IOError: except IOError:
sleep(2) sleep(2)
def get_output_remote(self):
if self.__retOutput is False:
self.__outputBuffer = ""
return
while True:
try:
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if str(e).find("STATUS_SHARING_VIOLATION") >= 0:
# Output not finished, let's wait
sleep(2)
pass
else:
pass
self.__smbconnection.deleteFile(self.__share, self.__output)

View File

@ -80,7 +80,7 @@ class PassPolDump:
self.hash = connection.hash self.hash = connection.hash
self.lmhash = "" self.lmhash = ""
self.nthash = "" self.nthash = ""
self.aesKey = None self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos self.doKerberos = connection.kerberos
self.protocols = PassPolDump.KNOWN_PROTOCOLS.keys() self.protocols = PassPolDump.KNOWN_PROTOCOLS.keys()
self.pass_pol = {} self.pass_pol = {}

View File

@ -25,7 +25,7 @@ class SamrFunc:
self.hash = connection.hash self.hash = connection.hash
self.lmhash = "" self.lmhash = ""
self.nthash = "" self.nthash = ""
self.aesKey = (None,) self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos self.doKerberos = connection.kerberos
if self.hash is not None: if self.hash is not None:
@ -40,16 +40,20 @@ class SamrFunc:
self.samr_query = SAMRQuery( self.samr_query = SAMRQuery(
username=self.username, username=self.username,
password=self.password, password=self.password,
domain=self.domain,
remote_name=self.addr, remote_name=self.addr,
remote_host=self.addr, remote_host=self.addr,
kerberos=self.doKerberos, kerberos=self.doKerberos,
aesKey=self.aesKey,
) )
self.lsa_query = LSAQuery( self.lsa_query = LSAQuery(
username=self.username, username=self.username,
password=self.password, password=self.password,
domain=self.domain,
remote_name=self.addr, remote_name=self.addr,
remote_host=self.addr, remote_host=self.addr,
kerberos=self.doKerberos, kerberos=self.doKerberos,
aesKey=self.aesKey,
logger=self.logger logger=self.logger
) )
@ -107,13 +111,14 @@ class SAMRQuery:
remote_name="", remote_name="",
remote_host="", remote_host="",
kerberos=None, kerberos=None,
aesKey="",
): ):
self.__username = username self.__username = username
self.__password = password self.__password = password
self.__domain = domain self.__domain = domain
self.__lmhash = "" self.__lmhash = ""
self.__nthash = "" self.__nthash = ""
self.__aesKey = None self.__aesKey = aesKey
self.__port = port self.__port = port
self.__remote_name = remote_name self.__remote_name = remote_name
self.__remote_host = remote_host self.__remote_host = remote_host
@ -207,6 +212,7 @@ class LSAQuery:
port=445, port=445,
remote_name="", remote_name="",
remote_host="", remote_host="",
aesKey="",
kerberos=None, kerberos=None,
logger=None logger=None
): ):
@ -215,7 +221,7 @@ class LSAQuery:
self.__domain = domain self.__domain = domain
self.__lmhash = "" self.__lmhash = ""
self.__nthash = "" self.__nthash = ""
self.__aesKey = None self.__aesKey = aesKey
self.__port = port self.__port = port
self.__remote_name = remote_name self.__remote_name = remote_name
self.__remote_host = remote_host self.__remote_host = remote_host

View File

@ -24,7 +24,7 @@ class UserSamrDump:
self.hash = connection.hash self.hash = connection.hash
self.lmhash = "" self.lmhash = ""
self.nthash = "" self.nthash = ""
self.aesKey = None self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos self.doKerberos = connection.kerberos
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys() self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
self.users = [] self.users = []

View File

@ -91,8 +91,7 @@ class winrm(connection):
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os) self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
self.output_filename = self.output_filename.replace(":", "-")
def laps_search(self, username, password, ntlm_hash, domain): def laps_search(self, username, password, ntlm_hash, domain):
ldapco = LDAPConnect(self.domain, "389", self.domain) ldapco = LDAPConnect(self.domain, "389", self.domain)

330
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "crackmapexec" name = "crackmapexec"
version = "6.0.0" version = "6.0.1"
description = "A swiss army knife for pentesting networks" description = "A swiss army knife for pentesting networks"
authors = ["Marcello Salvati <byt3bl33d3r@pm.com>", "Martial Puygrenier <mpgn@protonmail.com>"] authors = ["Marcello Salvati <byt3bl33d3r@pm.com>", "Martial Puygrenier <mpgn@protonmail.com>"]
readme = "README.md" readme = "README.md"
@ -50,17 +50,18 @@ asyauth = "~0.0.13"
masky = "^0.2.0" masky = "^0.2.0"
sqlalchemy = "^2.0.4" sqlalchemy = "^2.0.4"
aiosqlite = "^0.18.0" aiosqlite = "^0.18.0"
pytest = "^7.2.2"
pyasn1-modules = "^0.3.0" pyasn1-modules = "^0.3.0"
rich = "^13.3.5" rich = "^13.3.5"
python-libnmap = "^0.7.3" python-libnmap = "^0.7.3"
resource = "^0.2.1"
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
flake8 = "*" flake8 = "*"
pylint = "*" pylint = "*"
shiv = "*" shiv = "*"
black = "^20.8b1" black = "^20.8b1"
pytest = "^7.2.2"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.2.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"