def get_computers(self, include_properties=False, acl=False): properties = ['samaccountname', 'userAccountControl', 'distinguishedname', 'dnshostname', 'samaccounttype', 'objectSid', 'primaryGroupID'] if include_properties: properties += ['servicePrincipalName', 'msDS-AllowedToDelegateTo', 'lastLogon', 'lastLogonTimestamp', 'pwdLastSet', 'operatingSystem', 'description', 'operatingSystemServicePack'] if 'msDS-AllowedToActOnBehalfOfOtherIdentity'.lower() in self.objecttype_guid_map: properties.append('msDS-AllowedToActOnBehalfOfOtherIdentity') if self.ad.has_laps: properties.append('ms-mcs-admpwdexpirationtime') if acl: # Also collect LAPS expiration time since this matters for reporting (no LAPS = no ACL reported) if self.ad.has_laps: properties += ['nTSecurityDescriptor', 'ms-mcs-admpwdexpirationtime'] else: properties.append('nTSecurityDescriptor') entries = self.search('(&(sAMAccountType=805306369)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))', properties, generator=True, query_sd=acl) entriesNum = 0 for entry in entries: entriesNum += 1 self.ad.computers[ADUtils.get_entry_property(entry, 'distinguishedName', '')] = entry self.ad.computersidcache.put(ADUtils.get_entry_property(entry, 'dNSHostname', '').lower(), entry['attributes']['objectSid']) logging.info('Found %u computers', entriesNum) return entries
def get_bloodhound_data(self, entry, collect): data = { 'Name': self.hostname.upper(), 'PrimaryGroup': self.primarygroup, 'LocalAdmins': self.admins, 'Properties': { 'objectsid': self.objectsid, 'domain': self.ad.domain, 'highvalue': False }, "RemoteDesktopUsers": [], "DcomUsers": [], "AllowedToDelegate": [] } props = data['Properties'] # via the TRUSTED_FOR_DELEGATION (0x00080000) flag in UAC props['unconstraineddelegation'] = ADUtils.get_entry_property(entry, 'userAccountControl', default=0) & 0x00080000 == 0x00080000 if 'objectprops' in collect: props['enabled'] = ADUtils.get_entry_property(entry, 'userAccountControl', default=0) & 2 == 0 props['lastlogon'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastLogon', default=0, raw=True) ) props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True) ) props['serviceprincipalnames'] = ADUtils.get_entry_property(entry, 'servicePrincipalName', []) props['description'] = ADUtils.get_entry_property(entry, 'description') props['operatingsystem'] = ADUtils.get_entry_property(entry, 'operatingSystem') # Add SP to OS if specified servicepack = ADUtils.get_entry_property(entry, 'operatingSystemServicePack') if servicepack: props['operatingsystem'] = '%s %s' % (props['operatingsystem'], servicepack) # TODO: AllowedToDelegate return data
def parse_gmsa(self, user, entry): """ Parse GMSA DACL which states which users can read the password """ _, aces = parse_binary_acl(user, 'user', ADUtils.get_entry_property(entry, 'msDS-GroupMSAMembership', raw=True), self.addc.objecttype_guid_map) processed_aces = self.aceresolver.resolve_aces(aces) for ace in processed_aces: if ace['RightName'] == 'Owner': continue ace['RightName'] = 'ReadGMSAPassword' user['Aces'].append(ace)
def enumerate_gpos(self): gpos = [] resolver = AceResolver(self.addomain, self.addomain.objectresolver) entries = self.addc.get_gpos() for entry in entries: gpo = { "Properties": { "highvalue": ADUtils.get_entry_property(entry, 'isCriticalSystemObject', default=False), "name": ADUtils.get_entry_property(entry, 'displayName'), "domain": '.'.join(str(ADUtils.get_entry_property(entry, 'distinguishedName')).split('DC')[1:]).translate({ord(c):'' for c in '=,'}), "objectid": str(ADUtils.get_entry_property(entry, 'objectGUID')).translate({ord(c):'' for c in '}{'}), "distinguishedname": ADUtils.get_entry_property(entry, 'distinguishedName'), "description": None, "gpcpath": ADUtils.get_entry_property(entry, 'gPCFileSysPath') }, "ObjectIdentifier": str(ADUtils.get_entry_property(entry, 'objectGUID')).translate({ord(c):'' for c in '}{'}), "Aces": [] } _, aces = parse_binary_acl(gpo, 'gpo', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor'), self.addc.objecttype_guid_map) gpo['Aces'] = resolver.resolve_aces(aces) gpos.append(gpo) self.dump_gpos(gpos)
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 add_user_properties(user, entry): """ Resolve properties for user objects """ props = user['Properties'] # print entry # Is user enabled? Checked by seeing if the UAC flag 2 (ACCOUNT_DISABLED) is not set props['enabled'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 2 == 0 props['lastlogon'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastLogon', default=0, raw=True)) props['lastlogontimestamp'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastlogontimestamp', default=0, raw=True)) if props['lastlogontimestamp'] == 0: props['lastlogontimestamp'] = -1 props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True)) props['dontreqpreauth'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00400000 == 0x00400000 props['pwdneverexpires'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00010000 == 0x00010000 props['sensitive'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00100000 == 0x00100000 props['serviceprincipalnames'] = ADUtils.get_entry_property( entry, 'servicePrincipalName', []) props['hasspn'] = len(props['serviceprincipalnames']) > 0 props['displayname'] = ADUtils.get_entry_property(entry, 'displayName') props['email'] = ADUtils.get_entry_property(entry, 'mail') props['title'] = ADUtils.get_entry_property(entry, 'title') props['homedirectory'] = ADUtils.get_entry_property( entry, 'homeDirectory') props['description'] = ADUtils.get_entry_property(entry, 'description') props['userpassword'] = ADUtils.ensure_string( ADUtils.get_entry_property(entry, 'userPassword')) props['admincount'] = ADUtils.get_entry_property( entry, 'adminCount', 0) == 1 if len( ADUtils.get_entry_property(entry, 'msDS-AllowedToDelegateTo', [])) > 0: props['allowedtodelegate'] = ADUtils.get_entry_property( entry, 'msDS-AllowedToDelegateTo', []) props['sidhistory'] = [ LDAP_SID(bsid).formatCanonical() for bsid in ADUtils.get_entry_property(entry, 'sIDHistory', []) ] # v4 props whencreated = ADUtils.get_entry_property(entry, 'whencreated', default=0) if isinstance(whencreated, int): props['whencreated'] = whencreated else: props['whencreated'] = calendar.timegm(whencreated.timetuple()) props['unixpassword'] = ADUtils.ensure_string( ADUtils.get_entry_property(entry, 'unixuserpassword')) props['unicodepassword'] = ADUtils.ensure_string( ADUtils.get_entry_property(entry, 'unicodepwd')) # Non-default schema? # props['sfupassword'] = ADUtils.ensure_string(ADUtils.get_entry_property(entry, 'msSFU30Password')) props['sfupassword'] = None
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 add_user_properties(user, entry): props = user['Properties'] # print entry # Is user enabled? Checked by seeing if the UAC flag 2 (ACCOUNT_DISABLED) is not set props['enabled'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 2 == 0 props['lastlogon'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastLogon', default=0, raw=True)) props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True)) props['serviceprincipalnames'] = ADUtils.get_entry_property( entry, 'servicePrincipalName', []) props['hasspn'] = len(props['serviceprincipalnames']) > 0 props['displayname'] = ADUtils.get_entry_property(entry, 'displayName') props['email'] = ADUtils.get_entry_property(entry, 'mail') props['title'] = ADUtils.get_entry_property(entry, 'title') props['homedirectory'] = ADUtils.get_entry_property( entry, 'homeDirectory') props['description'] = ADUtils.get_entry_property(entry, 'description') props['userpassword'] = ADUtils.get_entry_property( entry, 'userPassword') props['admincount'] = ADUtils.get_entry_property( entry, 'adminCount', 0) == 1
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')
def get_bloodhound_data(self, entry, collect, skip_acl=False): data = { 'ObjectIdentifier': self.objectsid, 'AllowedToAct': [], 'PrimaryGroupSid': self.primarygroup, 'LocalAdmins': self.admins, 'PSRemoteUsers': self.psremote, 'Properties': { 'name': self.hostname.upper(), 'objectid': self.objectsid, 'domain': self.ad.domain.upper(), 'highvalue': False, 'distinguishedname': ADUtils.get_entry_property(entry, 'distinguishedName') }, 'RemoteDesktopUsers': self.rdp, 'DcomUsers': self.dcom, 'AllowedToDelegate': [], 'Sessions': self.sessions, 'Aces': [] } props = data['Properties'] # via the TRUSTED_FOR_DELEGATION (0x00080000) flag in UAC props['unconstraineddelegation'] = ADUtils.get_entry_property(entry, 'userAccountControl', default=0) & 0x00080000 == 0x00080000 props['enabled'] = ADUtils.get_entry_property(entry, 'userAccountControl', default=0) & 2 == 0 if 'objectprops' in collect or 'acl' in collect: props['haslaps'] = ADUtils.get_entry_property(entry, 'ms-mcs-admpwdexpirationtime', 0) != 0 if 'objectprops' in collect: props['lastlogontimestamp'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastlogontimestamp', default=0, raw=True) ) props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True) ) props['serviceprincipalnames'] = ADUtils.get_entry_property(entry, 'servicePrincipalName', []) props['description'] = ADUtils.get_entry_property(entry, 'description') props['operatingsystem'] = ADUtils.get_entry_property(entry, 'operatingSystem') # Add SP to OS if specified servicepack = ADUtils.get_entry_property(entry, 'operatingSystemServicePack') if servicepack: props['operatingsystem'] = '%s %s' % (props['operatingsystem'], servicepack) delegatehosts = ADUtils.get_entry_property(entry, 'msDS-AllowedToDelegateTo', []) for host in delegatehosts: try: target = host.split('/')[1] except IndexError: logging.warning('Invalid delegation target: %s', host) continue try: sid = self.ad.computersidcache.get(target.lower()) data['AllowedToDelegate'].append(sid) except KeyError: if '.' in target: data['AllowedToDelegate'].append(target.upper()) if len(delegatehosts) > 0: props['allowedtodelegate'] = delegatehosts # Process resource-based constrained delegation _, aces = parse_binary_acl(data, 'computer', ADUtils.get_entry_property(entry, 'msDS-AllowedToActOnBehalfOfOtherIdentity', raw=True), self.addc.objecttype_guid_map) outdata = self.aceresolver.resolve_aces(aces) for delegated in outdata: if delegated['RightName'] == 'Owner': continue if delegated['RightName'] == 'GenericAll': data['AllowedToAct'].append({'MemberId': delegated['PrincipalSID'], 'MemberType': delegated['PrincipalType']}) # Run ACL collection if this was not already done centrally if 'acl' in collect and not skip_acl: _, aces = parse_binary_acl(data, 'computer', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map) # Parse aces data['Aces'] = self.aceresolver.resolve_aces(aces) return data
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())
def dump_trusts(self, filename='domains.json'): """ Dump trusts. This is currently the only domain info we support, so this function handles the entire domain dumping. """ entries = self.addc.get_trusts() 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 # If the logging level is DEBUG, we ident the objects if logging.getLogger().getEffectiveLevel() == logging.DEBUG: indent_level = 1 else: indent_level = None logging.debug('Writing trusts to file: %s' % filename) # Todo: fix this properly. Current code is quick fix to work with domains # that have custom casing in their DN domain_object = None for domain in self.addomain.domains.keys(): if domain.lower() == self.addomain.baseDN.lower(): domain_object = self.addomain.domains[domain] break if not domain_object: logging.error('Could not find domain object. Abortint trust enumeration') return # Initialize json structure datastruct = { "domains": [], "meta": { "type": "domains", "count": 0 } } # Get functional level level_id = ADUtils.get_entry_property(domain_object, 'msds-behavior-version') try: functional_level = ADUtils.FUNCTIONAL_LEVELS[int(level_id)] except KeyError: functional_level = 'Unknown' domain = { "Name": self.addomain.domain, "Properties": { "highvalue": True, "objectsid": domain_object['attributes']['objectSid'], "description": ADUtils.get_entry_property(domain_object, 'description'), "functionallevel": functional_level }, "Trusts": [], # The below is all for GPO collection, unsupported as of now. "Links": [], "Aces": [], "Users": [], "Computers": [], "ChildOus": [] } num_entries = 0 for entry in entries: num_entries += 1 # TODO: self.addomain is currently only a single domain. In multi domain mode # this might need to be updated trust = ADDomainTrust(self.addomain.domain, entry['attributes']['name'], entry['attributes']['trustDirection'], entry['attributes']['trustType'], entry['attributes']['trustAttributes']) domain['Trusts'].append(trust.to_output()) logging.info('Found %u trusts', num_entries) # Single domain only datastruct['meta']['count'] = 1 datastruct['domains'].append(domain) json.dump(datastruct, out, indent=indent_level) logging.debug('Finished writing trusts') out.close()
def add_user_properties(user, entry): """ Resolve properties for user objects """ props = user['Properties'] # print entry # Is user enabled? Checked by seeing if the UAC flag 2 (ACCOUNT_DISABLED) is not set props['enabled'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 2 == 0 props['lastlogon'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastLogon', default=0, raw=True)) if props['lastlogon'] == 0: props['lastlogon'] = -1 props['lastlogontimestamp'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastlogontimestamp', default=0, raw=True)) if props['lastlogontimestamp'] == 0: props['lastlogontimestamp'] = -1 props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True)) props['dontreqpreauth'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00400000 == 0x00400000 props['pwdneverexpires'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00010000 == 0x00010000 props['sensitive'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00100000 == 0x00100000 props['serviceprincipalnames'] = ADUtils.get_entry_property( entry, 'servicePrincipalName', []) props['hasspn'] = len(props['serviceprincipalnames']) > 0 props['displayname'] = ADUtils.get_entry_property(entry, 'displayName') props['email'] = ADUtils.get_entry_property(entry, 'mail') props['title'] = ADUtils.get_entry_property(entry, 'title') props['homedirectory'] = ADUtils.get_entry_property( entry, 'homeDirectory') props['description'] = ADUtils.get_entry_property(entry, 'description') props['userpassword'] = ADUtils.get_entry_property( entry, 'userPassword') props['admincount'] = ADUtils.get_entry_property( entry, 'adminCount', 0) == 1 if len( ADUtils.get_entry_property(entry, 'msDS-AllowedToDelegateTo', [])) > 0: props['allowedtodelegate'] = ADUtils.get_entry_property( entry, 'msDS-AllowedToDelegateTo', []) props['sidhistory'] = ADUtils.get_entry_property( entry, 'sIDHistory', [])
def dump_domain(self, collect, filename='domains.json'): """ Dump trusts. This is currently the only domain info we support, so this function handles the entire domain dumping. """ if 'trusts' in collect: entries = self.addc.get_trusts() else: entries = [] 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 # If the logging level is DEBUG, we ident the objects if logging.getLogger().getEffectiveLevel() == logging.DEBUG: indent_level = 1 else: indent_level = None # Todo: fix this properly. Current code is quick fix to work with domains # that have custom casing in their DN domain_object = None for domain in self.addomain.domains.keys(): if domain.lower() == self.addomain.baseDN.lower(): domain_object = self.addomain.domains[domain] break if not domain_object: logging.error( 'Could not find domain object. Aborting domain enumeration') return # Initialize json structure datastruct = { "domains": [], "meta": { "type": "domains", "count": 0, "version": 3 } } # Get functional level level_id = ADUtils.get_entry_property(domain_object, 'msds-behavior-version') try: functional_level = ADUtils.FUNCTIONAL_LEVELS[int(level_id)] except KeyError: functional_level = 'Unknown' domain = { "ObjectIdentifier": domain_object['attributes']['objectSid'], "Properties": { "name": self.addomain.domain.upper(), "domain": self.addomain.domain.upper(), "highvalue": True, "objectid": ADUtils.get_entry_property(domain_object, 'objectSid'), "distinguishedname": ADUtils.get_entry_property(domain_object, 'distinguishedName'), "description": ADUtils.get_entry_property(domain_object, 'description'), "functionallevel": functional_level }, "Trusts": [], "Aces": [], # The below is all for GPO collection, unsupported as of now. "Links": [], "Users": [], "Computers": [], "ChildOus": [] } if 'acl' in collect: resolver = AceResolver(self.addomain, self.addomain.objectresolver) _, aces = parse_binary_acl( domain, 'domain', ADUtils.get_entry_property(domain_object, 'nTSecurityDescriptor'), self.addc.objecttype_guid_map) domain['Aces'] = resolver.resolve_aces(aces) if 'trusts' in collect: num_entries = 0 for entry in entries: num_entries += 1 trust = ADDomainTrust( ADUtils.get_entry_property(entry, 'name'), ADUtils.get_entry_property(entry, 'trustDirection'), ADUtils.get_entry_property(entry, 'trustType'), ADUtils.get_entry_property(entry, 'trustAttributes'), ADUtils.get_entry_property(entry, 'securityIdentifier')) domain['Trusts'].append(trust.to_output()) logging.info('Found %u trusts', num_entries) # Single domain only datastruct['meta']['count'] = 1 datastruct['domains'].append(domain) json.dump(datastruct, out, indent=indent_level) logging.debug('Finished writing domain info') out.close()
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_computers_dconly(self, timestamp=""): ''' Enumerate computer objects. This function is only used if no collection was requested that required connecting to computers anyway. ''' filename = timestamp + 'computers.json' acl = 'acl' in self.collect entries = self.addc.ad.computers.values() logging.debug('Writing computers ACL 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, 'computers', filename)) results_worker.daemon = True results_worker.start() if acl and not self.disable_pooling: self.aclenumerator.init_pool() # This loops over the cached entries for entry in entries: if not 'attributes' in entry: continue if 'dNSHostName' not in entry['attributes']: continue hostname = entry['attributes']['dNSHostName'] if not hostname: continue samname = entry['attributes']['sAMAccountName'] cobject = ADComputer(hostname=hostname, samname=samname, ad=self.addomain, addc=self.addc, objectsid=entry['attributes']['objectSid']) cobject.primarygroup = MembershipEnumerator.get_primary_membership( entry) computer = cobject.get_bloodhound_data(entry, self.collect, skip_acl=True) # 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( computer, 'computer', 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=(computer, 'computer', 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(computer) # 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 computers')
def get_bloodhound_data(self, entry, collect, skip_acl=False): data = { 'ObjectIdentifier': self.objectsid, 'AllowedToAct': [], 'PrimaryGroupSID': self.primarygroup, 'LocalAdmins': { 'Collected': 'localadmin' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.admins, }, 'PSRemoteUsers': { 'Collected': 'psremote' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.psremote }, 'Properties': { 'name': self.hostname.upper(), 'domainsid': self.ad.domain_object.sid, 'domain': self.ad.domain.upper(), 'distinguishedname': ADUtils.get_entry_property(entry, 'distinguishedName').upper() }, 'RemoteDesktopUsers': { 'Collected': 'rdp' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.rdp }, 'DcomUsers': { 'Collected': 'dcom' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.dcom }, 'AllowedToDelegate': [], 'Sessions': { 'Collected': 'session' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.sessions }, 'PrivilegedSessions': { 'Collected': 'loggedon' in collect and not self.permanentfailure, 'FailureReason': None, 'Results': self.loggedon }, # Unsupported for now 'RegistrySessions': { 'Collected': False, 'FailureReason': None, 'Results': [] }, 'Aces': [], 'HasSIDHistory': [], 'IsDeleted': ADUtils.get_entry_property(entry, 'isDeleted', default=False), 'Status': None } props = data['Properties'] # via the TRUSTED_FOR_DELEGATION (0x00080000) flag in UAC props['unconstraineddelegation'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x00080000 == 0x00080000 props['enabled'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 2 == 0 props['trustedtoauth'] = ADUtils.get_entry_property( entry, 'userAccountControl', default=0) & 0x01000000 == 0x01000000 if 'objectprops' in collect or 'acl' in collect: props['haslaps'] = ADUtils.get_entry_property( entry, 'ms-mcs-admpwdexpirationtime', 0) != 0 if 'objectprops' in collect: props['lastlogon'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastlogon', default=0, raw=True)) props['lastlogontimestamp'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'lastlogontimestamp', default=0, raw=True)) if props['lastlogontimestamp'] == 0: props['lastlogontimestamp'] = -1 props['pwdlastset'] = ADUtils.win_timestamp_to_unix( ADUtils.get_entry_property(entry, 'pwdLastSet', default=0, raw=True)) whencreated = ADUtils.get_entry_property(entry, 'whencreated', default=0) if not isinstance(whencreated, int): whencreated = calendar.timegm(whencreated.timetuple()) props['whencreated'] = whencreated props['serviceprincipalnames'] = ADUtils.get_entry_property( entry, 'servicePrincipalName', []) props['description'] = ADUtils.get_entry_property( entry, 'description') props['operatingsystem'] = ADUtils.get_entry_property( entry, 'operatingSystem') # Add SP to OS if specified servicepack = ADUtils.get_entry_property( entry, 'operatingSystemServicePack') if servicepack: props['operatingsystem'] = '%s %s' % (props['operatingsystem'], servicepack) props['sidhistory'] = [ LDAP_SID(bsid).formatCanonical() for bsid in ADUtils.get_entry_property(entry, 'sIDHistory', []) ] delegatehosts = ADUtils.get_entry_property( entry, 'msDS-AllowedToDelegateTo', []) for host in delegatehosts: try: target = host.split('/')[1] except IndexError: logging.warning('Invalid delegation target: %s', host) continue try: sid = self.ad.computersidcache.get(target.lower()) data['AllowedToDelegate'].append(sid) except KeyError: if '.' in target: data['AllowedToDelegate'].append(target.upper()) if len(delegatehosts) > 0: props['allowedtodelegate'] = delegatehosts # Process resource-based constrained delegation _, aces = parse_binary_acl( data, 'computer', ADUtils.get_entry_property( entry, 'msDS-AllowedToActOnBehalfOfOtherIdentity', raw=True), self.addc.objecttype_guid_map) outdata = self.aceresolver.resolve_aces(aces) for delegated in outdata: if delegated['RightName'] == 'Owner': continue if delegated['RightName'] == 'GenericAll': data['AllowedToAct'].append({ 'ObjectIdentifier': delegated['PrincipalSID'], 'ObjectType': delegated['PrincipalType'] }) # Run ACL collection if this was not already done centrally if 'acl' in collect and not skip_acl: _, aces = parse_binary_acl( data, 'computer', ADUtils.get_entry_property(entry, 'nTSecurityDescriptor', raw=True), self.addc.objecttype_guid_map) # Parse aces data['Aces'] = self.aceresolver.resolve_aces(aces) return data