From 6e45d431c141ea69679e4b4e500d07efc2547fba Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Sun, 12 Mar 2023 12:41:02 -0400 Subject: [PATCH] feat(smb): add functionality to query SAMR for local groups if DC query fails; closes #687 --- cme/protocols/smb.py | 14 ++ cme/protocols/smb/database.py | 6 +- cme/protocols/smb/samrfunc.py | 271 ++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 cme/protocols/smb/samrfunc.py diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index c99a67a8..3f3b9a09 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -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): diff --git a/cme/protocols/smb/database.py b/cme/protocols/smb/database.py index b6039acc..e27dac6f 100755 --- a/cme/protocols/smb/database.py +++ b/cme/protocols/smb/database.py @@ -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() diff --git a/cme/protocols/smb/samrfunc.py b/cme/protocols/smb/samrfunc.py new file mode 100644 index 00000000..7813b752 --- /dev/null +++ b/cme/protocols/smb/samrfunc.py @@ -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]@]') + + 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}")