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_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 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_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 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 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 dns_resolve(self, domain=None, kerberos=True): logging.debug('Querying domain controller information from DNS') basequery = '_ldap._tcp.pdc._msdcs' if domain is not None: logging.debug('Using domain hint: %s' % str(domain)) query = '_ldap._tcp.pdc._msdcs.%s' % domain else: # Assume a DNS search domain is (correctly) configured on the host # in which case the resolver will autocomplete our request query = basequery try: q = self.dnsresolver.query(query, 'SRV') if str(q.qname).lower().startswith('_ldap._tcp.pdc._msdcs'): ad_domain = str(q.qname).lower()[len(basequery):].strip('.') logging.info('Found AD domain: %s' % ad_domain) self.domain = ad_domain if self.auth.domain is None: self.auth.domain = ad_domain self.baseDN = ADUtils.domain2ldap(ad_domain) for r in q: dc = str(r.target).rstrip('.') logging.debug('Found primary DC: %s' % dc) if dc not in self._dcs: self._dcs.append(dc) except resolver.NXDOMAIN: pass try: q = self.dnsresolver.query(query.replace('pdc', 'gc'), 'SRV') for r in q: gc = str(r.target).rstrip('.') logging.debug('Found Global Catalog server: %s' % gc) if gc not in self._gcs: self._gcs.append(gc) except resolver.NXDOMAIN: pass if kerberos is True: try: q = self.dnsresolver.query('_kerberos._tcp.dc._msdcs', 'SRV') for r in q: kdc = str(r.target).rstrip('.') logging.debug('Found KDC: %s' % str(r.target).rstrip('.')) if kdc not in self._kdcs: self._kdcs.append(kdc) self.auth.kdc = self._kdcs[0] except resolver.NXDOMAIN: pass return True
def __init__(self, domain=None, auth=None, nameserver=None, dns_tcp=False, dns_timeout=3.0): self.domain = domain # Object of type ADDomain, added later self.domain_object = None self.auth = auth # List of DCs for this domain. Contains just one DC since # we query for the primary DC specifically self._dcs = [] # Kerberos servers self._kdcs = [] # Global catalog servers self._gcs = [] self.domains = {} self.nbdomains = {} self.groups = {} # Groups by DN self.groups_dnmap = {} # Group mapping from gid to DN self.computers = {} self.users = {} # Users by DN # Create a resolver object self.dnsresolver = resolver.Resolver() if nameserver: self.dnsresolver.nameservers = [nameserver] # Resolve DNS over TCP? self.dns_tcp = dns_tcp # Set DNS timeout self.dns_timeout = dns_timeout # Give it a cache to prevent duplicate lookups self.dnsresolver.cache = resolver.Cache() # Default timeout after 3 seconds if the DNS servers # do not come up with an answer self.dnsresolver.lifetime = dns_timeout self.dnsresolver.timeout = dns_timeout # Also create a custom cache for both forward and backward lookups # this cache is thread-safe self.dnscache = DNSCache() # Create a thread-safe SID lookup cache self.sidcache = SidCache() # Create a thread-safe SAM lookup cache self.samcache = SamCache() # Create SID cache for computer accounts self.computersidcache = SidCache() # Object Resolver, initialized later self.objectresolver = None # Number of domains within the forest self.num_domains = 1 # Does the schema have laps properties self.has_laps = False if domain is not None: self.baseDN = ADUtils.domain2ldap(domain) else: self.baseDN = None
def rpc_get_schtasks(self): """ Query the scheduled tasks via RPC. Requires admin privileges. These credentials can be dumped with mimikatz via vault::cred """ # Blacklisted folders (Default ones) blacklist = [u'Microsoft\x00'] # Start with the root folder folders = ['\\'] tasks = [] schtaskusers = [] binding = r'ncacn_np:%s[\PIPE\atsvc]' % self.addr try: dce = self.dce_rpc_connect(binding, tsch.MSRPC_UUID_TSCHS, True) if dce is None: return schtaskusers # Get root folder resp = tsch.hSchRpcEnumFolders(dce, '\\') for item in resp['pNames']: data = item['Data'] if data not in blacklist: folders.append('\\' + data) # Enumerate the folders we found # subfolders not supported yet for folder in folders: try: resp = tsch.hSchRpcEnumTasks(dce, folder) for item in resp['pNames']: data = item['Data'] if folder != '\\': # Make sure to strip the null byte tasks.append(folder[:-1] + '\\' + data) else: tasks.append(folder + data) except DCERPCException as e: logging.debug('Error enumerating task folder %s: %s', folder, e) for task in tasks: try: resp = tsch.hSchRpcRetrieveTask(dce, task) # This returns a tuple (sid, logontype) or None userinfo = ADUtils.parse_task_xml(resp['pXml']) if userinfo: if userinfo[1] == u'Password': # Convert to byte string because our cache format is in bytes schtaskusers.append(str(userinfo[0])) logging.info( 'Found scheduled task %s on %s with stored credentials for SID %s', task, self.hostname, userinfo[0]) except DCERPCException as e: logging.debug('Error querying task %s: %s', task, e) except DCERPCException as e: logging.debug('Exception enumerating scheduled tasks: %s', e) dce.disconnect() return schtaskusers
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 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 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 try_connect(self): addr = None try: addr = self.ad.dnscache.get(self.hostname) except KeyError: try: q = self.ad.dnsresolver.query(self.hostname, 'A', tcp=self.ad.dns_tcp) for r in q: addr = r.address if addr == None: return False # Do exit properly on keyboardinterrupts except KeyboardInterrupt: raise except Exception as e: # Doesn't exist if "None of DNS query names exist" in str(e): logging.info( 'Skipping enumeration for %s since it could not be resolved.', self.hostname) else: logging.warning('Could not resolve: %s: %s', self.hostname, e) return False logging.debug('Resolved: %s' % addr) self.ad.dnscache.put(self.hostname, addr) self.addr = addr logging.debug('Trying connecting to computer: %s', self.hostname) # We ping the host here, this adds a small overhead for setting up an extra socket # but saves us from constructing RPC Objects for non-existing hosts. Also RPC over # SMB does not support setting a connection timeout, so we catch this here. if ADUtils.tcp_ping(addr, 445) is False: return False return True
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 fromLDAP(identifier, sid=None): dns_name = ADUtils.ldap2domain(identifier) return ADDomain(name=dns_name, sid=sid, distinguishedname=identifier)
def dns_resolve(self, domain=None, kerberos=True, options=None): logging.debug('Querying domain controller information from DNS') basequery = '_ldap._tcp.pdc._msdcs' if domain is not None: logging.debug('Using domain hint: %s' % str(domain)) query = '_ldap._tcp.pdc._msdcs.%s' % domain else: # Assume a DNS search domain is (correctly) configured on the host # in which case the resolver will autocomplete our request query = basequery try: q = self.dnsresolver.query(query, 'SRV', tcp=self.dns_tcp) if str(q.qname).lower().startswith('_ldap._tcp.pdc._msdcs'): ad_domain = str(q.qname).lower()[len(basequery):].strip('.') logging.info('Found AD domain: %s' % ad_domain) self.domain = ad_domain if self.auth.domain is None: self.auth.domain = ad_domain self.baseDN = ADUtils.domain2ldap(ad_domain) for r in q: dc = str(r.target).rstrip('.') logging.debug('Found primary DC: %s' % dc) if dc not in self._dcs: self._dcs.append(dc) except resolver.NXDOMAIN: pass try: q = self.dnsresolver.query(query.replace('pdc', 'gc'), 'SRV', tcp=self.dns_tcp) for r in q: gc = str(r.target).rstrip('.') logging.debug('Found Global Catalog server: %s' % gc) if gc not in self._gcs: self._gcs.append(gc) except resolver.NXDOMAIN: # Only show warning if we don't already have a GC specified manually if options and not options.global_catalog: if not options.disable_autogc: logging.warning( 'Could not find a global catalog server, assuming the primary DC has this role\n' 'If this gives errors, either specify a hostname with -gc or disable gc resolution with --disable-autogc' ) self._gcs = self._dcs else: logging.warning( 'Could not find a global catalog server. Please specify one with -gc' ) if kerberos is True: try: q = self.dnsresolver.query('_kerberos._tcp.dc._msdcs', 'SRV', tcp=self.dns_tcp) for r in q: kdc = str(r.target).rstrip('.') logging.debug('Found KDC: %s' % str(r.target).rstrip('.')) if kdc not in self._kdcs: self._kdcs.append(kdc) self.auth.kdc = self._kdcs[0] except resolver.NXDOMAIN: pass return True
def get_root_domain(self): return ADUtils.ldap2domain( self.ldap.server.info.other['configurationNamingContext'][0])
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 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_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 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 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 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
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 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