from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldap as ldap_impacket import re class CMEModule: ''' Get description of users Module by @nodauf ''' name = 'get-desc-users' description = 'Get description of the users. May contained password' supported_protocols = ['ldap'] opsec_safe= True #Does the module touch disk? multiple_hosts = True #Does it make sense to run this module on multiple hosts at a time? def options(self, context, module_options): ''' FILTER Apply the FILTER (grep-like) (default: '') PASSWORDPOLICY Is the windows password policy enabled ? (default: False) MINLENGTH Minimum password length to match, only used if PASSWORDPOLICY is True (default: 6) ''' self.FILTER = '' self.MINLENGTH = '6' self.PASSWORDPOLICY = False if 'FILTER' in module_options: self.FILTER = module_options['FILTER'] if 'MINLENGTH' in module_options: self.MINLENGTH = module_options['MINLENGTH'] if 'PASSWORDPOLICY' in module_options: self.PASSWORDPOLICY = True self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{"+self.MINLENGTH+",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following def on_login(self, context, connection): '''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection''' # Building the search filter searchFilter = "(objectclass=user)" try: context.log.debug('Search Filter=%s' % searchFilter) resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=['sAMAccountName','description'], sizeLimit=0) except ldap_impacket.LDAPSearchError as e: if e.getErrorString().find('sizeLimitExceeded') >= 0: context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received') # We reached the sizeLimit, process the answers we have already and that's it. Until we implement # paged queries resp = e.getAnswers() pass else: logging.debug(e) return False answers = [] context.log.debug('Total of records returned %d' % len(resp)) for item in resp: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue sAMAccountName = '' description = '' try: for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) elif str(attribute['type']) == 'description': description = attribute['vals'][0] if sAMAccountName != '' and description != '': answers.append([sAMAccountName,description]) except Exception as e: context.log.debug("Exception:", exc_info=True) context.log.debug('Skipping item, cannot process due to error %s' % str(e)) pass answers = self.filter_answer(context, answers) if len(answers) > 0: context.log.success('Found following users: ') for answer in answers: context.log.highlight(u'User: {} description: {}'.format(answer[0],answer[1])) def filter_answer(self, context, answers): # No option to filter if self.FILTER == '' and not self.PASSWORDPOLICY: context.log.debug("No filter option enabled") return answers answersFiltered = [] context.log.debug("Prepare to filter") if len(answers) > 0: for answer in answers: conditionFilter = False description = str(answer[1]) # Filter if self.FILTER != '': conditionFilter = False if self.FILTER in description: conditionFilter = True # Password policy if self.PASSWORDPOLICY: conditionPasswordPolicy = False if self.regex.search(description): conditionPasswordPolicy = True if (self.FILTER and conditionFilter and self.PASSWORDPOLICY and conditionPasswordPolicy): answersFiltered.append([answer[0],description]) elif not self.FILTER and self.PASSWORDPOLICY and conditionPasswordPolicy: answersFiltered.append([answer[0],description]) elif not self.PASSWORDPOLICY and self.FILTER and conditionFilter: answersFiltered.append([answer[0],description]) return answersFiltered