2022-07-18 23:59:14 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2020-12-11 17:48:35 +00:00
|
|
|
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
2021-06-24 18:37:54 +00:00
|
|
|
from impacket.ldap import ldap as ldap_impacket
|
2020-12-11 17:48:35 +00:00
|
|
|
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:
|
2021-06-24 18:37:54 +00:00
|
|
|
context.log.debug('Search Filter=%s' % searchFilter)
|
2020-12-11 17:48:35 +00:00
|
|
|
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
|
|
|
attributes=['sAMAccountName','description'],
|
2021-06-24 18:37:54 +00:00
|
|
|
sizeLimit=0)
|
2020-12-11 17:48:35 +00:00
|
|
|
except ldap_impacket.LDAPSearchError as e:
|
|
|
|
if e.getErrorString().find('sizeLimitExceeded') >= 0:
|
2021-06-24 18:37:54 +00:00
|
|
|
context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
|
2020-12-11 17:48:35 +00:00
|
|
|
# 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 = []
|
2021-06-24 18:37:54 +00:00
|
|
|
context.log.debug('Total of records returned %d' % len(resp))
|
2020-12-11 17:48:35 +00:00
|
|
|
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:
|
2021-06-24 18:37:54 +00:00
|
|
|
context.log.debug("Exception:", exc_info=True)
|
|
|
|
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
|
2020-12-11 17:48:35 +00:00
|
|
|
pass
|
|
|
|
answers = self.filter_answer(context, answers)
|
|
|
|
if len(answers) > 0:
|
|
|
|
context.log.success('Found following users: ')
|
|
|
|
for answer in answers:
|
2021-06-24 18:37:54 +00:00
|
|
|
context.log.highlight(u'User: {} description: {}'.format(answer[0],answer[1]))
|
2020-12-11 17:48:35 +00:00
|
|
|
|
|
|
|
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
|