feat(smb): add functionality to query SAMR for local groups if DC query fails; closes #687

This commit is contained in:
Marshall Hallenbeck
2023-03-12 12:41:02 -04:00
parent 80bec84be4
commit 6e45d431c1
3 changed files with 290 additions and 1 deletions
+14
View File
@@ -28,6 +28,7 @@ from cme.protocols.smb.mmcexec import MMCEXEC
from cme.protocols.smb.smbspider import SMBSpider
from cme.protocols.smb.passpol import PassPolDump
from cme.protocols.smb.samruser import UserSamrDump
from cme.protocols.smb.samrfunc import SamrFunc
from cme.protocols.ldap.smbldap import LDAPConnect
from cme.helpers.logger import highlight
from cme.helpers.misc import *
@@ -907,6 +908,19 @@ class smb(connection):
break
except Exception as e:
self.logger.error('Error enumerating local groups of {}: {}'.format(self.host, e))
self.logger.info('Trying with SAMRPC protocol')
groups = SamrFunc(self).get_local_groups()
if groups:
self.logger.success('Enumerated local groups')
logging.debug(f"Local groups: {groups}")
for group_name, group_rid in groups.items():
group_id = self.db.add_group(
self.hostname,
group_name,
rid=group_rid
)
logging.debug(f"Added group, returned id: {group_id}")
return groups
def domainfromdsn(self, dsn):
+5 -1
View File
@@ -71,6 +71,7 @@ class database:
"id" integer PRIMARY KEY,
"domain" text,
"name" text,
"rid" text,
"member_count_ad" integer,
"last_query_time" text
)''')
@@ -507,7 +508,7 @@ class database:
return valid
def add_group(self, domain, name, member_count_ad=None):
def add_group(self, domain, name, rid=None, member_count_ad=None):
domain = domain.split('.')[0]
groups = []
@@ -521,6 +522,7 @@ class database:
group_data = {
"domain": domain,
"name": name,
"rid": rid
}
if not results:
@@ -537,6 +539,8 @@ class database:
g_data["domain"] = domain
if name is not None:
g_data["name"] = name
if rid is not None:
g_data["rid"] = rid
if member_count_ad is not None:
g_data["member_count_ad"] = member_count_ad
today = datetime.now()
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Majorly stolen from https://gist.github.com/ropnop/7a41da7aabb8455d0898db362335e139
# Which in turn stole from Impacket :)
# Code refactored and added to by @mjhallenbeck (Marshall-Hallenbeck on GitHub)
import sys
import logging
import argparse
import impacket
from impacket.dcerpc.v5 import transport, lsat, lsad, samr
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
class SamrFunc:
def __init__(self, connection):
self.logger = connection.logger
self.addr = connection.host
self.protocol = connection.args.port
self.username = connection.username
self.password = connection.password
self.domain = connection.domain
self.hash = connection.hash
self.lmhash = ''
self.nthash = ''
self.aesKey = None
self.doKerberos = False
if self.hash is not None:
if self.hash.find(':') != -1:
self.lmhash, self.nthash = self.hash.split(':')
else:
self.nthash = self.hash
if self.password is None:
self.password = ''
self.samr_query = SAMRQuery(
username=self.username,
password=self.password,
remote_name=self.addr,
remote_host=self.addr
)
self.lsa_query = LSAQuery(
username=self.username,
password=self.password,
remote_name=self.addr,
remote_host=self.addr
)
def get_builtin_groups(self):
domains = self.samr_query.get_domains()
if "Builtin" not in domains:
logging.error(f"No Builtin group to query locally on")
return
domain_handle = self.samr_query.get_domain_handle('Builtin')
groups = self.samr_query.get_domain_aliases(domain_handle)
return groups
def get_custom_groups(self):
domains = self.samr_query.get_domains()
custom_groups = {}
for domain in domains:
if domain == "Builtin":
continue
domain_handle = self.samr_query.get_domain_handle(domain)
custom_groups.update(self.samr_query.get_domain_aliases(domain_handle))
print(f"Local Groups: {custom_groups}")
return custom_groups
def get_local_groups(self):
builtin_groups = self.get_builtin_groups()
custom_groups = self.get_custom_groups()
return {**builtin_groups, **custom_groups}
def get_local_users(self):
pass
def get_local_administrators(self):
self.get_builtin_groups()
if "Administrators" in self.groups:
self.logger.success(f"Found Local Administrators group: RID {self.groups['Administrators']}")
domain_handle = self.samr_query.get_domain_handle('Builtin')
self.logger.debug(f"Querying group members")
member_sids = self.samr_query.get_alias_members(domain_handle, self.groups['Administrators'])
member_names = self.lsa_query.lookup_sids(member_sids)
for sid, name in zip(member_sids, member_names):
print(f"{name} - {sid}")
class SAMRQuery:
def __init__(self, username='', password='', domain='', port=445, remote_name='', remote_host=''):
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash = ''
self.__nthash = ''
self.__aesKey = None
self.__port = port
self.__remote_name = remote_name
self.__remote_host = remote_host
self.dce = self.get_dce()
self.server_handle = self.get_server_handle()
def get_transport(self):
string_binding = f"ncacn_np:{self.__port}[\pipe\samr]"
logging.debug(f"Binding to {string_binding}")
# using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr'
rpc_transport = transport.SMBTransport(
self.__remote_host,
self.__port,
r'\samr',
self.__username,
self.__password,
self.__domain,
self.__lmhash,
self.__nthash,
self.__aesKey
#doKerberos=self.doKerberos
)
return rpc_transport
def get_dce(self):
rpc_transport = self.get_transport()
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
except impacket.nmb.NetBIOSError as e:
logging.error(f"NetBIOSError on Connection: {e}")
return
return dce
def get_server_handle(self):
resp = samr.hSamrConnect(self.dce)
return resp['ServerHandle']
def get_domains(self):
resp = samr.hSamrEnumerateDomainsInSamServer(self.dce, self.server_handle)
domains = resp['Buffer']['Buffer']
domain_names = []
for domain in domains:
domain_names.append(domain['Name'])
return domain_names
def get_domain_handle(self, domain_name):
resp = samr.hSamrLookupDomainInSamServer(self.dce, self.server_handle, domain_name)
resp = samr.hSamrOpenDomain(self.dce, serverHandle=self.server_handle, domainId=resp['DomainId'])
return resp['DomainHandle']
def get_domain_aliases(self, domain_handle):
resp = samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)
aliases = {}
for alias in resp['Buffer']['Buffer']:
aliases[alias['Name']] = alias['RelativeId']
return aliases
def get_alias_handle(self, domain_handle, alias_id):
resp = samr.hSamrOpenAlias(self.dce, domain_handle, desiredAccess=MAXIMUM_ALLOWED, aliasId=alias_id)
return resp['AliasHandle']
def get_alias_members(self, domain_handle, alias_id):
alias_handle = self.get_alias_handle(domain_handle, alias_id)
resp = samr.hSamrGetMembersInAlias(self.dce, alias_handle)
member_sids = []
for member in resp['Members']['Sids']:
member_sids.append(member['SidPointer'].formatCanonical())
return member_sids
class LSAQuery:
def __init__(self, username='', password='', domain='', port=445, remote_name='', remote_host=''):
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash = ''
self.__nthash = ''
self.__aesKey = None
self.__port = port
self.__remote_name = remote_name
self.__remote_host = remote_host
self.dce = self.get_dce()
self.policy_handle = self.get_policy_handle()
def get_transport(self):
string_binding = f"ncacn_np:{self.__remote_name}[\\pipe\\lsarpc]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(self.__port)
rpc_transport.setRemoteHost(self.__remote_host)
if hasattr(rpc_transport, 'set_credentials'):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(
self.__username,
self.__password,
self.__domain,
self.__lmhash,
self.__nthash,
self.__aesKey
)
rpc_transport.set_kerberos(False, None)
return rpc_transport
def get_dce(self):
rpc_transport = self.get_transport()
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(lsat.MSRPC_UUID_LSAT)
except impacket.nmb.NetBIOSError as e:
self.logger.error(f"NetBIOSError on Connection: {e}")
return
return dce
def get_policy_handle(self):
resp = lsad.hLsarOpenPolicy2(self.dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)
return resp['PolicyHandle']
def lookup_sids(self, sids):
resp = lsat.hLsarLookupSids(self.dce, self.policy_handle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
names = []
for translated_names in resp['TranslatedNames']['Names']:
names.append(translated_names['Name'])
return names
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
group = parser.add_argument_group('authentication')
group.add_argument('-no-pass', action='store_true', help='don\'t ask for password')
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
options = parser.parse_args()
import re
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
options.target).groups('')
# In case the password contains '@'
if '@' in remote_name:
password = password + '@' + remote_name.rpartition('@')[0]
remote_name = remote_name.rpartition('@')[2]
if domain is None:
domain = ''
if password == '' and username != '' and options.no_pass is False:
from getpass import getpass
password = getpass("Password:")
samr_func = SamrFunc(
username=username,
password=password,
remote_name=remote_name,
remot_host=remote_name
)
builtin_groups = samr_func.get_builtin_groups()
print(f"Built In Groups: {builtin_groups}")