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 get_primary_membership(self, entry): """ Looks up the primary membership based on RID. Resolves it if needed """ try: primarygroupid = int(entry['attributes']['primaryGroupID']) except (TypeError, KeyError): # Doesn't have a primarygroupid, means it is probably a Group instead of a user return try: group = self.addomain.groups[ self.addomain.groups_dnmap[primarygroupid]] return group['principal'] except KeyError: # Look it up # Construct group sid by taking the domain sid, removing the user rid and appending the group rid groupsid = '%s-%d' % ('-'.join( entry['attributes']['objectSid'].split('-')[:-1]), primarygroupid) group = self.addomain.objectresolver.resolve_sid(groupsid, use_gc=False) if group is None: logging.warning('Warning: Unknown primarygroupid %d', primarygroupid) return None resolved_entry = ADUtils.resolve_ad_entry(group) self.addomain.groups[group['attributes'] ['distinguishedName']] = resolved_entry self.addomain.groups_dnmap[primarygroupid] = group['attributes'][ 'distinguishedName'] return resolved_entry['principal']
def enumerate_memberships(self, filename='group_membership.csv'): entries = self.addc.get_memberships() try: logging.debug('Opening file for writing: %s' % filename) out = codecs.open(filename, 'w', 'utf-8') except: logging.warning('Could not write file: %s' % filename) return logging.debug('Writing group memberships to file: %s' % filename) out.write('GroupName,AccountName,AccountType\n') entriesNum = 0 for entry in entries: entriesNum += 1 resolved_entry = ADUtils.resolve_ad_entry(entry) try: for m in entry['attributes']['memberOf']: self.write_membership(resolved_entry, m, out) except (KeyError, LDAPKeyError): logging.debug(traceback.format_exc()) self.write_primary_membership(resolved_entry, entry, out) logging.info('Found %d memberships', entriesNum) logging.debug('Finished writing membership') out.close()
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 enumerate_users(self): filename = 'users.json' # Should we include extra properties in the query? with_properties = 'objectprops' in self.collect acl = 'acl' in self.collect entries = self.addc.get_users(include_properties=with_properties, acl=acl) # If the logging level is DEBUG, we ident the objects if logging.getLogger().getEffectiveLevel() == logging.DEBUG: indent_level = 1 else: indent_level = None try: out = codecs.open(filename, 'w', 'utf-8') except: logging.warning('Could not write file: %s' % filename) return logging.debug('Writing users to file: %s' % filename) # Initialize json header out.write('{"users":[') num_entries = 0 for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) user = { "Name": resolved_entry['principal'], "PrimaryGroup": self.get_primary_membership(entry), "Properties": { "domain": self.addomain.domain, "objectsid": entry['attributes']['objectSid'], "highvalue": False } } if with_properties: MembershipEnumerator.add_user_properties(user, entry) self.addomain.users[entry['dn']] = resolved_entry if num_entries != 0: out.write(',') json.dump(user, out, indent=indent_level) num_entries += 1 logging.info('Found %d users', num_entries) out.write('],"meta":{"type":"users","count":%d}}' % num_entries) logging.debug('Finished writing users') out.close()
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 write_primary_membership(self, resolved_entry, entry, out): try: primarygroupid = int(entry['attributes']['primaryGroupID']) except (TypeError, KeyError): # Doesn't have a primarygroupid, means it is probably a Group instead of a user return try: group = self.addomain.groups[ self.addomain.groups_dnmap[primarygroupid]] pr = ADUtils.resolve_ad_entry(group) out.write('%s,%s,%s\n' % (pr['principal'], resolved_entry['principal'], resolved_entry['type'])) except KeyError: logging.warning('Warning: Unknown primarygroupid %d', primarygroupid)
def write_default_groups(self): """ Put default groups in the groups.json file """ # Domain controllers rootdomain = self.addc.get_root_domain().upper() entries = self.addc.get_domain_controllers() group = { "IsDeleted": False, "IsACLProtected": False, "ObjectIdentifier": "%s-S-1-5-9" % rootdomain, "Properties": { "domain": rootdomain.upper(), "name": "ENTERPRISE DOMAIN CONTROLLERS@%s" % rootdomain, }, "Members": [], "Aces": [] } for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) memberdata = { "ObjectIdentifier": resolved_entry['objectid'], "ObjectType": resolved_entry['type'].capitalize() } group["Members"].append(memberdata) self.result_q.put(group) domainsid = self.addomain.domain_object.sid domainname = self.addomain.domain.upper() # Everyone evgroup = { "IsDeleted": False, "IsACLProtected": False, "ObjectIdentifier": "%s-S-1-1-0" % domainname, "Properties": { "domain": domainname, "domainsid": self.addomain.domain_object.sid, "name": "EVERYONE@%s" % domainname, }, "Members": [], "Aces": [] } self.result_q.put(evgroup) # Authenticated users augroup = { "IsDeleted": False, "IsACLProtected": False, "ObjectIdentifier": "%s-S-1-5-11" % domainname, "Properties": { "domain": domainname, "domainsid": self.addomain.domain_object.sid, "name": "AUTHENTICATED USERS@%s" % domainname, }, "Members": [], "Aces": [] } self.result_q.put(augroup) # Interactive iugroup = { "IsDeleted": False, "IsACLProtected": False, "ObjectIdentifier": "%s-S-1-5-4" % domainname, "Properties": { "domain": domainname, "domainsid": self.addomain.domain_object.sid, "name": "INTERACTIVE@%s" % domainname, }, "Members": [], "Aces": [] } self.result_q.put(iugroup)
def enumerate_groups(self, timestamp=""): highvalue = [ "S-1-5-32-544", "S-1-5-32-550", "S-1-5-32-549", "S-1-5-32-551", "S-1-5-32-548" ] def is_highvalue(sid): if sid.endswith("-512") or sid.endswith("-516") or sid.endswith( "-519") or sid.endswith("-520"): return True if sid in highvalue: return True return False # Should we include extra properties in the query? with_properties = 'objectprops' in self.collect acl = 'acl' in self.collect filename = timestamp + 'groups.json' entries = self.addc.get_groups(include_properties=with_properties, acl=acl) logging.debug('Writing groups to file: %s', filename) # Use a separate queue for processing the results self.result_q = queue.Queue() results_worker = threading.Thread( target=OutputWorker.membership_write_worker, args=(self.result_q, 'groups', filename)) results_worker.daemon = True results_worker.start() if acl and not self.disable_pooling: self.aclenumerator.init_pool() for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) self.addomain.groups[entry['dn']] = resolved_entry try: sid = entry['attributes']['objectSid'] except KeyError: #Somehow we found a group without a sid? logging.warning('Could not determine SID for group %s', entry['attributes']['distinguishedName']) continue group = { "ObjectIdentifier": sid, "Properties": { "domain": self.addomain.domain.upper(), "domainsid": self.addomain.domain_object.sid, "name": resolved_entry['principal'], "distinguishedname": ADUtils.get_entry_property(entry, 'distinguishedName').upper() }, "Members": [], "Aces": [], "IsDeleted": ADUtils.get_entry_property(entry, 'isDeleted', default=False) } if sid in ADUtils.WELLKNOWN_SIDS: # Prefix it with the domain group['ObjectIdentifier'] = '%s-%s' % ( self.addomain.domain.upper(), sid) if with_properties: group['Properties']['admincount'] = ADUtils.get_entry_property( entry, 'adminCount', default=0) == 1 group['Properties'][ 'description'] = ADUtils.get_entry_property( entry, 'description') whencreated = ADUtils.get_entry_property(entry, 'whencreated', default=0) group['Properties']['whencreated'] = calendar.timegm( whencreated.timetuple()) for member in entry['attributes']['member']: resolved_member = self.get_membership(member) if resolved_member: group['Members'].append(resolved_member) # If we are enumerating ACLs, we break out of the loop here # this is because parsing ACLs is computationally heavy and therefor is done in subprocesses if acl: if self.disable_pooling: # Debug mode, don't run this pooled since it hides exceptions self.process_acldata( parse_binary_acl( group, 'group', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map)) else: # Process ACLs in separate processes, then call the processing function to resolve entries and write them to file self.aclenumerator.pool.apply_async( parse_binary_acl, args=(group, 'group', ADUtils.get_entry_property( entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map), callback=self.process_acldata) else: # Write it to the queue -> write to file in separate thread # this is solely for consistency with acl parsing, the performance improvement is probably minimal self.result_q.put(group) self.write_default_groups() # If we are parsing ACLs, close the parsing pool first # then close the result queue and join it if acl and not self.disable_pooling: self.aclenumerator.pool.close() self.aclenumerator.pool.join() self.result_q.put(None) else: self.result_q.put(None) self.result_q.join() logging.debug('Finished writing groups')
def enumerate_users(self, timestamp=""): filename = timestamp + 'users.json' # Should we include extra properties in the query? with_properties = 'objectprops' in self.collect acl = 'acl' in self.collect entries = self.addc.get_users(include_properties=with_properties, acl=acl) logging.debug('Writing users to file: %s', filename) # Use a separate queue for processing the results self.result_q = queue.Queue() results_worker = threading.Thread( target=OutputWorker.membership_write_worker, args=(self.result_q, 'users', filename)) results_worker.daemon = True results_worker.start() if acl and not self.disable_pooling: self.aclenumerator.init_pool() # This loops over a generator, results are fetched from LDAP on the go for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) # Skip trust objects if resolved_entry['type'] == 'trustaccount': continue user = { "AllowedToDelegate": [], "ObjectIdentifier": ADUtils.get_entry_property(entry, 'objectSid'), "PrimaryGroupSID": MembershipEnumerator.get_primary_membership(entry), "Properties": { "name": resolved_entry['principal'], "domain": self.addomain.domain.upper(), "domainsid": self.addomain.domain_object.sid, "distinguishedname": ADUtils.get_entry_property(entry, 'distinguishedName').upper(), "unconstraineddelegation": ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00080000 == 0x00080000, "trustedtoauth": ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x01000000 == 0x01000000, "passwordnotreqd": ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00000020 == 0x00000020 }, "Aces": [], "SPNTargets": [], "HasSIDHistory": [], "IsDeleted": ADUtils.get_entry_property(entry, 'isDeleted', default=False) } if with_properties: MembershipEnumerator.add_user_properties(user, entry) if 'allowedtodelegate' in user['Properties']: for host in user['Properties']['allowedtodelegate']: try: target = host.split('/')[1] except IndexError: logging.warning('Invalid delegation target: %s', host) continue try: sid = self.addomain.computersidcache.get( target.lower()) user['AllowedToDelegate'].append(sid) except KeyError: if '.' in target: user['AllowedToDelegate'].append( target.upper()) # Parse SID history if len(user['Properties']['sidhistory']) > 0: for historysid in user['Properties']['sidhistory']: user['HasSIDHistory'].append( self.aceresolver.resolve_sid(historysid)) # If this is a GMSA, process it's ACL. We don't bother with threads/processes here # since these accounts shouldn't be that common and neither should they have very complex # DACLs which control who can read their password if ADUtils.get_entry_property( entry, 'msDS-GroupMSAMembership', default=b'', raw=True) != b'': self.parse_gmsa(user, entry) self.addomain.users[entry['dn']] = resolved_entry # If we are enumerating ACLs, we break out of the loop here # this is because parsing ACLs is computationally heavy and therefor is done in subprocesses if acl: if self.disable_pooling: # Debug mode, don't run this pooled since it hides exceptions self.process_acldata( parse_binary_acl( user, 'user', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map)) else: # Process ACLs in separate processes, then call the processing function to resolve entries and write them to file self.aclenumerator.pool.apply_async( parse_binary_acl, args=(user, 'user', ADUtils.get_entry_property( entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map), callback=self.process_acldata) else: # Write it to the queue -> write to file in separate thread # this is solely for consistency with acl parsing, the performance improvement is probably minimal self.result_q.put(user) self.write_default_users() # If we are parsing ACLs, close the parsing pool first # then close the result queue and join it if acl and not self.disable_pooling: self.aclenumerator.pool.close() self.aclenumerator.pool.join() self.result_q.put(None) else: self.result_q.put(None) self.result_q.join() logging.debug('Finished writing users')
def enumerate_groups(self): highvalue = [ "S-1-5-32-544", "S-1-5-32-550", "S-1-5-32-549", "S-1-5-32-551", "S-1-5-32-548" ] def is_highvalue(sid): if sid.endswith("-512") or sid.endswith("-516") or sid.endswith( "-519") or sid.endswith("-520"): return True if sid in highvalue: return True return False # Should we include extra properties in the query? with_properties = 'objectprops' in self.collect acl = 'acl' in self.collect filename = 'groups.json' entries = self.addc.get_groups(include_properties=with_properties, acl=acl) # If the logging level is DEBUG, we ident the objects if logging.getLogger().getEffectiveLevel() == logging.DEBUG: indent_level = 1 else: indent_level = None try: out = codecs.open(filename, 'w', 'utf-8') except: logging.warning('Could not write file: %s' % filename) return logging.debug('Writing groups to file: %s' % filename) # Initialize json header out.write('{"groups":[') num_entries = 0 for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) self.addomain.groups[entry['dn']] = resolved_entry try: sid = entry['attributes']['objectSid'] except KeyError: #Somehow we found a group without a sid? logging.warning('Could not determine SID for group %s' % entry['attributes']['distinguishedName']) continue group = { "Name": resolved_entry['principal'], "Properties": { "domain": self.addomain.domain, "objectsid": sid, "highvalue": is_highvalue(sid) }, "Members": [] } if with_properties: group['Properties']['admincount'] = ADUtils.get_entry_property( entry, 'adminCount', default=0) == 1 group['Properties'][ 'description'] = ADUtils.get_entry_property( entry, 'description') for member in entry['attributes']['member']: resolved_member = self.get_membership(member) if resolved_member: group['Members'].append(resolved_member) if num_entries != 0: out.write(',') json.dump(group, out, indent=indent_level) num_entries += 1 logging.info('Found %d groups', num_entries) out.write('],"meta":{"type":"groups","count":%d}}' % num_entries) logging.debug('Finished writing groups') out.close()
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, 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 '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() results_q.put( ('computer', c.get_bloodhound_data(entry, self.collect))) if sessions is None: sessions = [] # Process found sessions for ses in sessions: # For every session, resolve the SAM name in the GC if needed domain = self.addomain.domain if self.addomain.num_domains > 1 and self.do_gc_lookup: try: users = self.addomain.samcache.get(samname) except KeyError: # Look up the SAM name in the GC users = self.addomain.objectresolver.gc_sam_lookup( ses['user']) if users is None: # Unknown user continue self.addomain.samcache.put(samname, users) else: users = [((u'%s@%s' % (ses['user'], domain)).upper(), 2)] # Resolve the IP to obtain the host the session is from try: target = self.addomain.dnscache.get(ses['source']) except KeyError: 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) # Put the result on the results queue. for user in users: results_q.put(('session', { 'UserName': user[0].upper(), 'ComputerName': target.upper(), 'Weight': user[1] })) if loggedon is None: loggedon = [] # Put the logged on users on the queue too for user in loggedon: results_q.put(('session', { 'UserName': ('%s@%s' % user).upper(), 'ComputerName': hostname.upper(), 'Weight': 1 })) # Process Tasks for taskuser in tasks: try: user = self.addomain.sidcache.get(taskuser) except KeyError: # Resolve SID in GC userentry = self.addomain.objectresolver.resolve_sid( taskuser) # Resolve it to an entry and store in the cache user = ADUtils.resolve_ad_entry(userentry) self.addomain.sidcache.put(taskuser, user) logging.debug('Resolved TASK SID to username: %s', user['principal']) # Use sessions for now results_q.put(('session', { 'UserName': user['principal'].upper(), 'ComputerName': hostname.upper(), 'Weight': 2 })) # Process Services for serviceuser in services: # Todo: use own cache 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 user = ADUtils.resolve_ad_entry(userentry) self.addomain.sidcache.put(serviceuser, user) logging.debug('Resolved Service UPN to username: %s', user['principal']) # Use sessions for now results_q.put(('session', { 'UserName': user['principal'].upper(), 'ComputerName': hostname.upper(), 'Weight': 2 })) except DCERPCException: 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())
def enumerate_users(self): filename = 'users.json' # Should we include extra properties in the query? with_properties = 'objectprops' in self.collect acl = 'acl' in self.collect entries = self.addc.get_users(include_properties=with_properties, acl=acl) logging.debug('Writing users to file: %s', filename) # Use a separate queue for processing the results self.result_q = queue.Queue() results_worker = threading.Thread( target=OutputWorker.membership_write_worker, args=(self.result_q, 'users', filename)) results_worker.daemon = True results_worker.start() if acl and not self.disable_pooling: self.aclenumerator.init_pool() # This loops over a generator, results are fetched from LDAP on the go for entry in entries: resolved_entry = ADUtils.resolve_ad_entry(entry) user = { "Name": resolved_entry['principal'], "PrimaryGroup": self.get_primary_membership(entry), "Properties": { "domain": self.addomain.domain.upper(), "objectsid": entry['attributes']['objectSid'], "highvalue": False, "unconstraineddelegation": ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00080000 == 0x00080000 }, "Aces": [] } if with_properties: MembershipEnumerator.add_user_properties(user, entry) self.addomain.users[entry['dn']] = resolved_entry # If we are enumerating ACLs, we break out of the loop here # this is because parsing ACLs is computationally heavy and therefor is done in subprocesses if acl: if self.disable_pooling: # Debug mode, don't run this pooled since it hides exceptions self.process_stuff( parse_binary_acl( user, 'user', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor', raw=True))) else: # Process ACLs in separate processes, then call the processing function to resolve entries and write them to file self.aclenumerator.pool.apply_async( parse_binary_acl, args=(user, 'user', ADUtils.get_entry_property( entry, 'nTSecurityDescriptor', raw=True)), callback=self.process_stuff) else: # Write it to the queue -> write to file in separate thread # this is solely for consistency with acl parsing, the performance improvement is probably minimal self.result_q.put(user) # If we are parsing ACLs, close the parsing pool first # then close the result queue and join it if acl and not self.disable_pooling: self.aclenumerator.pool.close() self.aclenumerator.pool.join() self.result_q.put(None) else: self.result_q.put(None) self.result_q.join() logging.debug('Finished writing users')