def gc_sam_lookup(self, samname): """ This function attempts to resolve the SAM name returned in session enumeration to a user/domain combination by querying the Global Catalog. SharpHound calls this GC Deconflictation. """ output = [] entries = self.resolve_samname(samname) # If an error occurs, return if entries is None: return if len(entries) > 1: # Awww multiple matches, unsure which is the valid one, add them with different weights for entry in entries: domain = ADUtils.ldap2domain(entry['dn']) principal = (u'%s@%s' % (entry['attributes']['sAMAccountName'], domain)).upper() # This is consistent with SharpHound if domain.lower() == self.addomain.domain.lower(): weight = 1 else: weight = 2 output.append((principal, weight)) else: if len(entries) == 0: # This shouldn't even happen, but let's default to the current domain principal = (u'%s@%s' % (samname, self.addomain.domain)).upper() output.append((principal, 2)) else: # One match, best case entry = entries[0] domain = ADUtils.ldap2domain(entry['dn']) principal = (u'%s@%s' % (entry['attributes']['sAMAccountName'], domain)).upper() output.append((principal, 2)) return output
def get_membership(self, member): # First assume it is a user try: resolved_entry = self.addomain.users[member] except KeyError: # Try if it is a group try: resolved_entry = self.addomain.groups[member] except KeyError: # Try if it is a computer try: entry = self.addomain.computers[member] # Computers are stored as raw entries resolved_entry = ADUtils.resolve_ad_entry(entry) except KeyError: use_gc = ADUtils.ldap2domain( member) != self.addomain.domain qobject = self.addomain.objectresolver.resolve_distinguishedname( member, use_gc=use_gc) if qobject is None: return resolved_entry = ADUtils.resolve_ad_entry(qobject) # Store it in the cache if resolved_entry['type'] == 'user': self.addomain.users[member] = resolved_entry if resolved_entry['type'] == 'group': self.addomain.groups[member] = resolved_entry # Computers are stored as raw entries if resolved_entry['type'] == 'computer': self.addomain.computers[member] = qobject return { "MemberName": resolved_entry['principal'], "MemberType": resolved_entry['type'].capitalize() }
def write_membership(self, resolved_entry, membership, out): if membership in self.addomain.groups: parent = self.addomain.groups[membership] pd = ADUtils.ldap2domain(membership) pr = ADUtils.resolve_ad_entry(parent) out.write(u'%s,%s,%s\n' % (pr['principal'], resolved_entry['principal'], resolved_entry['type'])) else: # This could be a group in a different domain parent = self.addomain.objectresolver.resolve_group(membership) if not parent: logging.warning('Warning: Unknown group %s', membership) return self.addomain.groups[membership] = parent pd = ADUtils.ldap2domain(membership) pr = ADUtils.resolve_ad_entry(parent) out.write(u'%s,%s,%s\n' % (pr['principal'], resolved_entry['principal'], resolved_entry['type']))
def rpc_get_loggedon(self): """ Query logged on users via RPC. Requires admin privs """ binding = r'ncacn_np:%s[\PIPE\wkssvc]' % self.addr loggedonusers = set() dce = self.dce_rpc_connect(binding, wkst.MSRPC_UUID_WKST) if dce is None: logging.warning('Connection failed: %s', binding) return try: # 1 means more detail, including the domain resp = wkst.hNetrWkstaUserEnum(dce, 1) for record in resp['UserInfo']['WkstaUserInfo']['Level1'][ 'Buffer']: # Skip computer accounts if record['wkui1_username'][-2] == '$': continue # Skip sessions for local accounts if record['wkui1_logon_domain'][:-1].upper( ) == self.samname.upper(): continue domain = record['wkui1_logon_domain'][:-1].upper() domain_entry = self.ad.get_domain_by_name(domain) if domain_entry is not None: domain = ADUtils.ldap2domain( domain_entry['attributes']['distinguishedName']) logging.debug( 'Found logged on user at %s: %s@%s' % (self.hostname, record['wkui1_username'][:-1], domain)) loggedonusers.add((record['wkui1_username'][:-1], domain)) except DCERPCException as e: if 'rpc_s_access_denied' in str(e): logging.debug( 'Access denied while enumerating LoggedOn on %s, probably no admin privs', self.hostname) else: logging.debug('Exception connecting to RPC: %s', e) except Exception as e: if 'connection reset' in str(e): logging.debug('Connection was reset: %s', e) else: raise e dce.disconnect() return list(loggedonusers)
def get_membership(self, member): """ Attempt to resolve the membership (DN) of a group to an object """ # First assume it is a user try: resolved_entry = self.addomain.users[member] except KeyError: # Try if it is a group try: resolved_entry = self.addomain.groups[member] except KeyError: # Try if it is a computer try: entry = self.addomain.computers[member] # Computers are stored as raw entries resolved_entry = ADUtils.resolve_ad_entry(entry) except KeyError: use_gc = ADUtils.ldap2domain( member) != self.addomain.domain qobject = self.addomain.objectresolver.resolve_distinguishedname( member, use_gc=use_gc) if qobject is None: return None resolved_entry = ADUtils.resolve_ad_entry(qobject) # Store it in the cache if resolved_entry['type'] == 'User': self.addomain.users[member] = resolved_entry if resolved_entry['type'] == 'Group': self.addomain.groups[member] = resolved_entry # Computers are stored as raw entries if resolved_entry['type'] == 'Computer': self.addomain.computers[member] = qobject return { "ObjectIdentifier": resolved_entry['objectid'], "ObjectType": resolved_entry['type'].capitalize() }
def fromLDAP(identifier, sid=None): dns_name = ADUtils.ldap2domain(identifier) return ADDomain(name=dns_name, sid=sid, distinguishedname=identifier)
def get_root_domain(self): return ADUtils.ldap2domain( self.ldap.server.info.other['configurationNamingContext'][0])
def rpc_resolve_sids(self): """ Resolve any remaining unknown SIDs for local administrator accounts. """ # If all sids were already cached, we can just return if len(self.admin_sids) == 0: return binding = r'ncacn_np:%s[\PIPE\lsarpc]' % self.addr dce = self.dce_rpc_connect(binding, lsat.MSRPC_UUID_LSAT) if dce is None: logging.warning('Connection failed') return try: resp = lsat.hLsarOpenPolicy2(dce, lsat.POLICY_LOOKUP_NAMES | MAXIMUM_ALLOWED) except Exception as e: if str(e).find('Broken pipe') >= 0: return else: raise policyHandle = resp['PolicyHandle'] # We could look up the SIDs all at once, but if not all SIDs are mapped, we don't know which # ones were resolved and which not, making it impossible to map them in the cache. # Therefor we use more SAMR calls at the start, but after a while most SIDs will be reliable # in our cache and this function doesn't even need to get called anymore. for sid_string in self.admin_sids: try: resp = lsat.hLsarLookupSids(dce, policyHandle, [sid_string], lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta) except DCERPCException as e: if str(e).find('STATUS_NONE_MAPPED') >= 0: logging.warning('SID %s lookup failed, return status: STATUS_NONE_MAPPED', sid_string) # Try next SID continue elif str(e).find('STATUS_SOME_NOT_MAPPED') >= 0: # Not all could be resolved, work with the ones that could resp = e.get_packet() else: raise domains = [] for entry in resp['ReferencedDomains']['Domains']: domains.append(entry['Name']) for entry in resp['TranslatedNames']['Names']: domain = domains[entry['DomainIndex']] domain_entry = self.ad.get_domain_by_name(domain) if domain_entry is not None: domain = ADUtils.ldap2domain(domain_entry['attributes']['distinguishedName']) # TODO: what if it isn't? Should we fall back to LDAP? if entry['Name'] != '': resolved_entry = ADUtils.resolve_sid_entry(entry, domain) logging.debug('Resolved SID to name: %s', resolved_entry['principal']) self.admins.append({'Name': resolved_entry['principal'], 'Type': resolved_entry['type'].capitalize()}) # Add it to our cache self.ad.sidcache.put(sid_string, resolved_entry) else: logging.warning('Resolved name is empty [%s]', entry) dce.disconnect()
def process_computer(self, hostname, samname, objectsid, entry, results_q): """ Processes a single computer, pushes the results of the computer to the given queue. """ logging.debug('Querying computer: %s', hostname) c = ADComputer(hostname=hostname, samname=samname, ad=self.addomain, addc=self.addc, objectsid=objectsid) c.primarygroup = self.get_primary_membership(entry) if c.try_connect() == True: try: if 'session' in self.collect: sessions = c.rpc_get_sessions() else: sessions = [] if 'localadmin' in self.collect: unresolved = c.rpc_get_group_members(544, c.admins) c.rpc_resolve_sids(unresolved, c.admins) if 'rdp' in self.collect: unresolved = c.rpc_get_group_members(555, c.rdp) c.rpc_resolve_sids(unresolved, c.rdp) if 'dcom' in self.collect: unresolved = c.rpc_get_group_members(562, c.dcom) c.rpc_resolve_sids(unresolved, c.dcom) if 'psremote' in self.collect: unresolved = c.rpc_get_group_members(580, c.psremote) c.rpc_resolve_sids(unresolved, c.psremote) if 'loggedon' in self.collect: loggedon = c.rpc_get_loggedon() else: loggedon = [] if 'experimental' in self.collect: services = c.rpc_get_services() tasks = c.rpc_get_schtasks() else: services = [] tasks = [] c.rpc_close() # c.rpc_get_domain_trusts() if sessions is None: sessions = [] # Should we use the GC? use_gc = self.addomain.num_domains > 1 and self.do_gc_lookup # Process found sessions for ses in sessions: # For every session, resolve the SAM name in the GC if needed domain = self.addomain.domain try: users = self.addomain.samcache.get(samname) except KeyError: # Look up the SAM name in the GC entries = self.addomain.objectresolver.resolve_samname( ses['user'], use_gc=use_gc) if entries is not None: users = [ user['attributes']['objectSid'] for user in entries ] if entries is None or users == []: logging.warning( 'Failed to resolve SAM name %s in current forest', samname) continue self.addomain.samcache.put(samname, users) # Resolve the IP to obtain the host the session is from try: target = self.addomain.dnscache.get(ses['source']) except KeyError: # TODO: also use discovery based on port 445 connections similar to sharphound target = ADUtils.ip2host(ses['source'], self.addomain.dnsresolver, self.addomain.dns_tcp) # Even if the result is the IP (aka could not resolve PTR) we still cache # it since this result is unlikely to change during this run self.addomain.dnscache.put_single( ses['source'], target) if ':' in target: # IPv6 address, not very useful continue if '.' not in target: logging.debug( 'Resolved target does not look like an IP or domain. Assuming hostname: %s', target) target = '%s.%s' % (target, domain) # Resolve target hostname try: hostsid = self.addomain.computersidcache.get( target.lower()) except KeyError: logging.warning( 'Could not resolve hostname to SID: %s', target) continue # Put the result on the results queue. for user in users: c.sessions.append({ 'ComputerId': hostsid, 'UserId': user }) if loggedon is None: loggedon = [] # Put the logged on users on the queue too for user, userdomain in loggedon: # Construct fake UPN to cache this user fupn = '%s@%s' % (user.upper(), userdomain.upper()) try: users = self.addomain.samcache.get(fupn) except KeyError: entries = self.addomain.objectresolver.resolve_samname( user, use_gc=use_gc) if entries is not None: if len(entries) > 1: for resolved_user in entries: edn = ADUtils.get_entry_property( resolved_user, 'distinguishedName') edom = ADUtils.ldap2domain(edn).lower() if edom == userdomain.lower(): users = [ resolved_user['attributes'] ['objectSid'] ] break logging.debug( 'Skipping resolved user %s since domain does not match (%s != %s)', edn, edom, userdomain.lower()) else: users = [ resolved_user['attributes']['objectSid'] for resolved_user in entries ] if entries is None or users == []: logging.warning( 'Failed to resolve SAM name %s in current forest', samname) continue self.addomain.samcache.put(fupn, users) for resultuser in users: c.sessions.append({ 'ComputerId': objectsid, 'UserId': resultuser }) # Process Tasks for taskuser in tasks: c.sessions.append({ 'ComputerId': objectsid, 'UserId': taskuser }) # Process Services for serviceuser in services: try: user = self.addomain.sidcache.get(serviceuser) except KeyError: # Resolve UPN in GC userentry = self.addomain.objectresolver.resolve_upn( serviceuser) # Resolve it to an entry and store in the cache self.addomain.sidcache.put( serviceuser, userentry['attributes']['objectSid']) user = userentry['attributes']['objectSid'] logging.debug('Resolved Service UPN to SID: %s', user['objectsid']) c.sessions.append({ 'ComputerId': objectsid, 'UserId': user }) results_q.put( ('computer', c.get_bloodhound_data(entry, self.collect))) except DCERPCException: logging.debug(traceback.format_exc()) logging.warning('Querying computer failed: %s', hostname) except Exception as e: logging.error( 'Unhandled exception in computer %s processing: %s', hostname, str(e)) logging.info(traceback.format_exc()) else: # Write the info we have to the file regardless try: results_q.put( ('computer', c.get_bloodhound_data(entry, self.collect))) except Exception as e: logging.error( 'Unhandled exception in computer %s processing: %s', hostname, str(e)) logging.info(traceback.format_exc())