Example #1
0
    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
Example #2
0
 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()
     }
Example #3
0
    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']))
Example #4
0
    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)
Example #5
0
 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()
     }
Example #6
0
 def fromLDAP(identifier, sid=None):
     dns_name = ADUtils.ldap2domain(identifier)
     return ADDomain(name=dns_name, sid=sid, distinguishedname=identifier)
Example #7
0
 def get_root_domain(self):
     return ADUtils.ldap2domain(
         self.ldap.server.info.other['configurationNamingContext'][0])
    def rpc_resolve_sids(self):
        """
        Resolve any remaining unknown SIDs for local administrator accounts.
        """
        # If all sids were already cached, we can just return
        if len(self.admin_sids) == 0:
            return
        binding = r'ncacn_np:%s[\PIPE\lsarpc]' % self.addr

        dce = self.dce_rpc_connect(binding, lsat.MSRPC_UUID_LSAT)

        if dce is None:
            logging.warning('Connection failed')
            return

        try:
            resp = lsat.hLsarOpenPolicy2(dce, lsat.POLICY_LOOKUP_NAMES | MAXIMUM_ALLOWED)
        except Exception as e:
            if str(e).find('Broken pipe') >= 0:
                return
            else:
                raise

        policyHandle = resp['PolicyHandle']

        # We could look up the SIDs all at once, but if not all SIDs are mapped, we don't know which
        # ones were resolved and which not, making it impossible to map them in the cache.
        # Therefor we use more SAMR calls at the start, but after a while most SIDs will be reliable
        # in our cache and this function doesn't even need to get called anymore.
        for sid_string in self.admin_sids:
            try:
                resp = lsat.hLsarLookupSids(dce, policyHandle, [sid_string], lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
            except DCERPCException as e:
                if str(e).find('STATUS_NONE_MAPPED') >= 0:
                    logging.warning('SID %s lookup failed, return status: STATUS_NONE_MAPPED', sid_string)
                    # Try next SID
                    continue
                elif str(e).find('STATUS_SOME_NOT_MAPPED') >= 0:
                    # Not all could be resolved, work with the ones that could
                    resp = e.get_packet()
                else:
                    raise

            domains = []
            for entry in resp['ReferencedDomains']['Domains']:
                domains.append(entry['Name'])

            for entry in resp['TranslatedNames']['Names']:
                domain = domains[entry['DomainIndex']]
                domain_entry = self.ad.get_domain_by_name(domain)
                if domain_entry is not None:
                    domain = ADUtils.ldap2domain(domain_entry['attributes']['distinguishedName'])
                # TODO: what if it isn't? Should we fall back to LDAP?

                if entry['Name'] != '':
                    resolved_entry = ADUtils.resolve_sid_entry(entry, domain)
                    logging.debug('Resolved SID to name: %s', resolved_entry['principal'])
                    self.admins.append({'Name': resolved_entry['principal'],
                                        'Type': resolved_entry['type'].capitalize()})
                    # Add it to our cache
                    self.ad.sidcache.put(sid_string, resolved_entry)
                else:
                    logging.warning('Resolved name is empty [%s]', entry)

        dce.disconnect()
Example #9
0
    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())