main
mpgn 2021-09-18 17:04:46 -04:00
commit 86ad83f74b
10 changed files with 816 additions and 528 deletions

View File

@ -46,8 +46,9 @@ def gen_cli_args():
std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="username(s) or file(s) containing usernames")
std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="password(s) or file(s) containing passwords")
std_parser.add_argument("-k", "--kerberos", action='store_true', help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
std_parser.add_argument("--export", metavar="EXPORT", nargs='+', help="Export result into a file, probably buggy")
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs='+', help="AES key to use for Kerberos Authentication (128 or 256 bits)")
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
fail_group = std_parser.add_mutually_exclusive_group()
fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='max number of global failed login attempts')

View File

@ -7,6 +7,7 @@ from socket import gethostbyname
from functools import wraps
from cme.logger import CMEAdapter
from cme.context import Context
from cme.helpers.logger import write_log
sem = BoundedSemaphore(1)
global_failed_logins = 0
@ -45,6 +46,7 @@ class connection(object):
self.kerberos = True if self.args.kerberos else False
self.aesKey = None if not self.args.aesKey else self.args.aesKey
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.export = None if not self.args.export else self.args.export
self.failed_logins = 0
self.local_ip = None
@ -103,7 +105,9 @@ class connection(object):
if hasattr(self, k) and hasattr(getattr(self, k), '__call__'):
if v is not False and v is not None:
logging.debug('Calling {}()'.format(k))
getattr(self, k)()
r = getattr(self, k)()
if self.export:
write_log(str(r), self.export[0])
def call_modules(self):
module_logger = CMEAdapter(extra={

View File

@ -23,6 +23,7 @@ class CMEModule:
multiple_hosts = False
def on_login(self, context, connection):
result = []
context.log.info('Getting the MachineAccountQuota')
searchFilter = '(objectClass=*)'
attributes = ['ms-DS-MachineAccountQuota']

View File

@ -1,4 +1,5 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import re
class CMEModule:
@ -35,13 +36,13 @@ class CMEModule:
searchFilter = "(objectclass=user)"
try:
logging.debug('Search Filter=%s' % searchFilter)
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=['sAMAccountName','description'],
sizeLimit=999)
sizeLimit=0)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find('sizeLimitExceeded') >= 0:
logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
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()
@ -51,7 +52,7 @@ class CMEModule:
return False
answers = []
logging.debug('Total of records returned %d' % len(resp))
context.log.debug('Total of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
@ -66,14 +67,14 @@ class CMEModule:
if sAMAccountName != '' and description != '':
answers.append([sAMAccountName,description])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(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('User: {} description: {}'.format(answer[0],answer[1]))
context.log.highlight(u'User: {} description: {}'.format(answer[0],answer[1]))
def filter_answer(self, context, answers):
# No option to filter

View File

@ -4,6 +4,7 @@
import requests
import logging
import configparser
from binascii import b2a_hex, unhexlify, hexlify
from cme.connection import *
from cme.helpers.logger import highlight
from cme.logger import CMEAdapter
@ -18,6 +19,17 @@ from impacket.krb5 import constants
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from io import StringIO
ldap_error_status = {
"533":"STATUS_ACCOUNT_DISABLED",
"701":"STATUS_ACCOUNT_EXPIRED",
"531":"STATUS_ACCOUNT_RESTRICTION",
"530":"STATUS_INVALID_LOGON_HOURS",
"532":"STATUS_PASSWORD_EXPIRED",
"773":"STATUS_PASSWORD_MUST_CHANGE",
"775":"USER_ACCOUNT_LOCKED",
"50":"LDAP_INSUFFICIENT_ACCESS"
}
class ldap(connection):
def __init__(self, args, db, host):
@ -58,7 +70,7 @@ class ldap(connection):
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
vgroup.add_argument("--users", action="store_true", help="Enumerate domain users")
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
return parser
@ -142,18 +154,19 @@ class ldap(connection):
self.smbv1))
def kerberos_login(self, aesKey, kdcHost):
# Create the baseDN
domainParts = self.domain.split('.')
self.baseDN = ''
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
if self.kdcHost is not None:
target = self.kdcHost
else:
target = self.domain
self.kdcHost = domain
# Create the baseDN
self.baseDN = ''
domainParts = self.kdcHost.split('.')
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
try:
self.ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash,
@ -164,6 +177,13 @@ class ldap(connection):
self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost)
self.ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash,
self.aesKey, kdcHost=self.kdcHost)
else:
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode in ldap_error_status else 'red')
return True
@ -172,18 +192,20 @@ class ldap(connection):
self.username = username
self.password = password
self.domain = domain
# Create the baseDN
self.baseDN = ''
domainParts = self.domain.split('.')
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
if self.kdcHost is not None:
target = self.kdcHost
else:
target = domain
self.kdcHost = domain
# Create the baseDN
self.baseDN = ''
domainParts = self.kdcHost.split('.')
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
if self.password == '' and self.args.asreproast:
hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username)
@ -196,7 +218,7 @@ class ldap(connection):
try:
self.ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN, self.kdcHost)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
#self.check_if_admin()
self.check_if_admin()
# Connect to LDAP
out = u'{}{}:{} {}'.format('{}\\'.format(domain),
@ -216,20 +238,26 @@ class ldap(connection):
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.logger.success(out)
except ldap_impacket.LDAPSessionError as e:
self.logger.error(u'{}\{}:{}'.format(self.domain,
self.username,
self.password))
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode in ldap_error_status else 'red')
else:
self.logger.error(u'{}\{}:{}'.format(self.domain,
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password))
self.password,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode in ldap_error_status else 'red')
return False
except OSError as e:
self.logger.error(u'{}\{}:{} {}'.format(self.domain,
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password,
"Error connecting to the domain, please add option --kdcHost with the IP of the domain controller"))
"Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller"))
return False
@ -249,18 +277,20 @@ class ldap(connection):
self.username = username
self.domain = domain
# Create the baseDN
self.baseDN = ''
domainParts = self.domain.split('.')
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
if self.kdcHost is not None:
target = self.kdcHost
else:
target = domain
self.kdcHost = domain
# Create the baseDN
self.baseDN = ''
domainParts = self.kdcHost.split('.')
for i in domainParts:
self.baseDN += 'dc=%s,' % i
# Remove last ','
self.baseDN = self.baseDN[:-1]
if self.hash == '' and self.args.asreproast:
hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username)
@ -277,7 +307,7 @@ class ldap(connection):
try:
self.ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN, self.kdcHost)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
#self.check_if_admin()
self.check_if_admin()
self.logger.success(out)
if not self.args.continue_on_success:
@ -290,19 +320,25 @@ class ldap(connection):
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.logger.success(out)
except ldap_impacket.LDAPSessionError as e:
self.logger.error(u'{}\{}:{}'.format(self.domain,
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.nthash))
self.password,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode in ldap_error_status else 'red')
else:
self.logger.error(u'{}\{}:{}'.format(self.domain,
self.username,
self.nthash))
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode in ldap_error_status else 'red')
return False
except OSError as e:
self.logger.error(u'{}\{}:{} {}'.format(self.domain,
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.nthash,
"Error connecting to the domain, please add option --kdcHost with the IP of the domain controller"))
"Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller"))
return False
def create_smbv1_conn(self):
@ -339,12 +375,71 @@ class ldap(connection):
return False
def sid_to_str(self, sid):
try:
# revision
revision = int(sid[0])
# count of sub authorities
sub_authorities = int(sid[1])
# big endian
identifier_authority = int.from_bytes(sid[2:8], byteorder='big')
# If true then it is represented in hex
if identifier_authority >= 2 ** 32:
identifier_authority = hex(identifier_authority)
# loop over the count of small endians
sub_authority = '-' + '-'.join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder='little')) for i in range(sub_authorities)])
objectSid = 'S-' + str(revision) + '-' + str(identifier_authority) + sub_authority
return objectSid
except Exception:
pass
return sid
def check_if_admin(self):
# 1. get SID of the domaine
sid_domaine = ""
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=8192)"
attributes= ["objectSid"]
resp = self.search(searchFilter, attributes, sizeLimit=0)
answers = []
for attribute in resp[0][1]:
if str(attribute['type']) == 'objectSid':
sid = self.sid_to_str(attribute['vals'][0])
sid_domaine = '-'.join(sid.split('-')[:-1])
# 2. get all group cn name
searchFilter = "(|(objectSid="+sid_domaine+"-512)(objectSid="+sid_domaine+"-544)(objectSid="+sid_domaine+"-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))"
attributes= ["distinguishedName"]
resp = self.search(searchFilter, attributes, sizeLimit=0)
answers = []
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
for attribute in item['attributes']:
if str(attribute['type']) == 'distinguishedName':
answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute['vals'][0] + ")"))
# 3. get memeber of these groups
searchFilter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + ''.join(answers) + "))"
attributes= [""]
resp = self.search(searchFilter, attributes, sizeLimit=0)
answers = []
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
if item:
self.admin_privs = True
def getUnixTime(self, t):
t -= 116444736000000000
t /= 10000000
return t
def search(self, searchFilter, attributes, sizeLimit=999):
def search(self, searchFilter, attributes, sizeLimit=0):
try:
logging.debug('Search Filter=%s' % searchFilter)
resp = self.ldapConnection.search(searchFilter=searchFilter,
@ -352,13 +447,13 @@ class ldap(connection):
sizeLimit=sizeLimit)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find('sizeLimitExceeded') >= 0:
logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
self.logger.error('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)
self.logger.error(e)
return False
return resp
@ -366,76 +461,74 @@ class ldap(connection):
# Building the search filter
searchFilter = "(sAMAccountType=805306368)"
attributes= []
resp = self.search(searchFilter, attributes, 999)
answers = []
logging.debug('Total of records returned %d' % len(resp))
resp = self.search(searchFilter, attributes, sizeLimit=0)
if resp:
answers = []
self.logger.info('Total of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
sAMAccountName = ''
badPasswordTime = ''
badPwdCount = 0
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
mustCommit = True
elif str(attribute['type']) == 'badPwdCount':
badPwdCount = "0x%x" % int(attribute['vals'][0])
elif str(attribute['type']) == 'badPasswordTime':
if str(attribute['vals'][0]) == '0':
badPasswordTime = '<never>'
else:
badPasswordTime = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
if mustCommit is True:
answers.append([sAMAccountName, badPwdCount, badPasswordTime])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
logging.debug(answers)
for value in answers:
self.logger.highlight('{:<30} badpwdcount: {} pwdLastSet: {}'.format(value[0], int(value[1],16),value[2]))
else:
self.logger.error("No entries found!")
return
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ''
badPasswordTime = ''
badPwdCount = 0
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
elif str(attribute['type']) == 'badPwdCount':
badPwdCount = str(attribute['vals'][0])
elif str(attribute['type']) == 'pwdLastSet':
if str(attribute['vals'][0]) == '0':
pwdLastSet = '<never>'
else:
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
answers.append([sAMAccountName, badPwdCount, pwdLastSet])
except Exception as e:
self.logger.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
logging.debug(answers)
for value in answers:
self.logger.highlight('{:<30} badpwdcount: {} pwdLastSet: {}'.format(value[0], value[1],value[2]))
else:
self.logger.error("No entries found!")
return
def groups(self):
# Building the search filter
searchFilter = "(objectCategory=group)"
attributes=[]
resp = self.search(searchFilter, attributes, 999)
answers = []
logging.debug('Total of records returned %d' % len(resp))
resp = self.search(searchFilter, attributes, 0)
if resp:
answers = []
logging.debug('Total of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
name = ''
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'name':
name = str(attribute['vals'][0])
mustCommit = True
# if str(attribute['type']) == 'objectSid':
# print(format_sid((attribute['vals'][0])))
if mustCommit is True:
answers.append([name])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
logging.debug(answers)
for value in answers:
self.logger.highlight('{}'.format(value[0]))
else:
self.logger.error("No entries found!")
return
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
name = ''
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'name':
name = str(attribute['vals'][0])
mustCommit = True
# if str(attribute['type']) == 'objectSid':
# print(format_sid((attribute['vals'][0])))
if mustCommit is True:
answers.append([name])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
logging.debug(answers)
for value in answers:
self.logger.highlight('{}'.format(value[0]))
else:
self.logger.error("No entries found!")
return
def asreproast(self):
if self.password == '' and self.nthash == '' and self.kerberos == False:
@ -445,139 +538,143 @@ class ldap(connection):
"(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % \
(UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
attributes = ['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon']
resp = self.search(searchFilter, attributes, 999)
resp = self.search(searchFilter, attributes, 0)
if resp:
answers = []
self.logger.info('Total of records returned %d' % len(resp))
answers = []
logging.debug('Total of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
sAMAccountName = ''
memberOf = ''
pwdLastSet = ''
userAccountControl = 0
lastLogon = 'N/A'
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
mustCommit = True
elif str(attribute['type']) == 'userAccountControl':
userAccountControl = "0x%x" % int(attribute['vals'][0])
elif str(attribute['type']) == 'memberOf':
memberOf = str(attribute['vals'][0])
elif str(attribute['type']) == 'pwdLastSet':
if str(attribute['vals'][0]) == '0':
pwdLastSet = '<never>'
else:
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'lastLogon':
if str(attribute['vals'][0]) == '0':
lastLogon = '<never>'
else:
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
if mustCommit is True:
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
for user in answers:
hash_TGT = KerberosAttacks(self).getTGT_asroast(user[0])
self.logger.highlight(u'{}'.format(hash_TGT))
with open(self.args.asreproast, 'a+') as hash_asreproast:
hash_asreproast.write(hash_TGT + '\n')
return True
else:
self.logger.error("No entries found!")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
sAMAccountName = ''
memberOf = ''
pwdLastSet = ''
userAccountControl = 0
lastLogon = 'N/A'
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
mustCommit = True
elif str(attribute['type']) == 'userAccountControl':
userAccountControl = "0x%x" % int(attribute['vals'][0])
elif str(attribute['type']) == 'memberOf':
memberOf = str(attribute['vals'][0])
elif str(attribute['type']) == 'pwdLastSet':
if str(attribute['vals'][0]) == '0':
pwdLastSet = '<never>'
else:
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'lastLogon':
if str(attribute['vals'][0]) == '0':
lastLogon = '<never>'
else:
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
if mustCommit is True:
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.debug('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
for user in answers:
hash_TGT = KerberosAttacks(self).getTGT_asroast(user[0])
self.logger.highlight(u'{}'.format(hash_TGT))
with open(self.args.asreproast, 'a+') as hash_asreproast:
hash_asreproast.write(hash_TGT + '\n')
return True
else:
self.logger.highlight("No entries found!")
return
self.logger.error("Error with the LDAP account used")
def kerberoasting(self):
# Building the search filter
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \
"(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
attributes = ['servicePrincipalName', 'sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon']
resp = self.search(searchFilter, attributes, 999)
resp = self.search(searchFilter, attributes, 0)
if resp:
answers = []
self.logger.info('Total of records returned %d' % len(resp))
answers = []
logging.debug('Total of records returned %d' % len(resp))
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
sAMAccountName = ''
memberOf = ''
SPNs = []
pwdLastSet = ''
userAccountControl = 0
lastLogon = 'N/A'
delegation = ''
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
mustCommit = True
elif str(attribute['type']) == 'userAccountControl':
userAccountControl = str(attribute['vals'][0])
if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION:
delegation = 'unconstrained'
elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION:
delegation = 'constrained'
elif str(attribute['type']) == 'memberOf':
memberOf = str(attribute['vals'][0])
elif str(attribute['type']) == 'pwdLastSet':
if str(attribute['vals'][0]) == '0':
pwdLastSet = '<never>'
else:
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'lastLogon':
if str(attribute['vals'][0]) == '0':
lastLogon = '<never>'
else:
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'servicePrincipalName':
for spn in attribute['vals']:
SPNs.append(str(spn))
if mustCommit is True:
if int(userAccountControl) & UF_ACCOUNTDISABLE:
logging.debug('Bypassing disabled account %s ' % sAMAccountName)
else:
for spn in SPNs:
answers.append([spn, sAMAccountName,memberOf, pwdLastSet, lastLogon, delegation])
except Exception as e:
logging.error('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
#users = dict( (vals[1], vals[0]) for vals in answers)
TGT = KerberosAttacks(self).getTGT_kerberoasting()
for SPN, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation in answers:
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
mustCommit = False
sAMAccountName = ''
memberOf = ''
SPNs = []
pwdLastSet = ''
userAccountControl = 0
lastLogon = 'N/A'
delegation = ''
try:
serverName = Principal(SPN, type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, self.domain,
self.kdcHost,
TGT['KDC_REP'], TGT['cipher'],
TGT['sessionKey'])
r = KerberosAttacks(self).outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, SPN)
self.logger.highlight(u'sAMAccountName: {} memberOf: {} pwdLastSet: {} lastLogon:{}'.format(sAMAccountName, memberOf, pwdLastSet, lastLogon))
self.logger.highlight(u'{}'.format(r))
with open(self.args.kerberoasting, 'a+') as hash_kerberoasting:
hash_kerberoasting.write(r + '\n')
for attribute in item['attributes']:
if str(attribute['type']) == 'sAMAccountName':
sAMAccountName = str(attribute['vals'][0])
mustCommit = True
elif str(attribute['type']) == 'userAccountControl':
userAccountControl = str(attribute['vals'][0])
if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION:
delegation = 'unconstrained'
elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION:
delegation = 'constrained'
elif str(attribute['type']) == 'memberOf':
memberOf = str(attribute['vals'][0])
elif str(attribute['type']) == 'pwdLastSet':
if str(attribute['vals'][0]) == '0':
pwdLastSet = '<never>'
else:
pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'lastLogon':
if str(attribute['vals'][0]) == '0':
lastLogon = '<never>'
else:
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0])))))
elif str(attribute['type']) == 'servicePrincipalName':
for spn in attribute['vals']:
SPNs.append(str(spn))
if mustCommit is True:
if int(userAccountControl) & UF_ACCOUNTDISABLE:
logging.debug('Bypassing disabled account %s ' % sAMAccountName)
else:
for spn in SPNs:
answers.append([spn, sAMAccountName,memberOf, pwdLastSet, lastLogon, delegation])
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.error('SPN: %s - %s' % (SPN,str(e)))
else:
self.logger.error("No entries found!")
logging.error('Skipping item, cannot process due to error %s' % str(e))
pass
if len(answers)>0:
#users = dict( (vals[1], vals[0]) for vals in answers)
TGT = KerberosAttacks(self).getTGT_kerberoasting()
for SPN, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation in answers:
try:
serverName = Principal(SPN, type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, self.domain,
self.kdcHost,
TGT['KDC_REP'], TGT['cipher'],
TGT['sessionKey'])
r = KerberosAttacks(self).outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, SPN)
self.logger.highlight(u'sAMAccountName: {} memberOf: {} pwdLastSet: {} lastLogon:{}'.format(sAMAccountName, memberOf, pwdLastSet, lastLogon))
self.logger.highlight(u'{}'.format(r))
with open(self.args.kerberoasting, 'a+') as hash_kerberoasting:
hash_kerberoasting.write(r + '\n')
except Exception as e:
logging.debug("Exception:", exc_info=True)
logging.error('SPN: %s - %s' % (SPN,str(e)))
else:
self.logger.highlight("No entries found!")
return
self.logger.error("Error with the LDAP account used")
def trusted_for_delegation(self):
# Building the search filter
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
attributes = ['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon']
resp = self.search(searchFilter, attributes, 999)
resp = self.search(searchFilter, attributes, 0)
answers = []
logging.debug('Total of records returned %d' % len(resp))
@ -632,7 +729,7 @@ class ldap(connection):
resp = self.ldapConnection.search(searchFilter=searchFilter,
attributes=['sAMAccountName',
'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'],
sizeLimit=999)
sizeLimit=0)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find('sizeLimitExceeded') >= 0:
logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
@ -694,7 +791,7 @@ class ldap(connection):
# Building the search filter
searchFilter = "(adminCount=1)"
attributes=['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon']
resp = self.search(searchFilter, attributes, 999)
resp = self.search(searchFilter, attributes, 0)
answers = []
logging.debug('Total of records returned %d' % len(resp))

View File

@ -25,6 +25,7 @@ from cme.protocols.smb.smbexec import SMBEXEC
from cme.protocols.smb.mmcexec import MMCEXEC
from cme.protocols.smb.smbspider import SMBSpider
from cme.protocols.smb.passpol import PassPolDump
from cme.protocols.smb.samruser import UserSamrDump
from cme.helpers.logger import highlight
from cme.helpers.misc import *
from cme.helpers.powershell import create_ps_command
@ -149,6 +150,7 @@ class smb(connection):
egroup.add_argument("--loggedon-users", action='store_true', help='enumerate logged on users')
egroup.add_argument('--users', nargs='?', const='', metavar='USER', help='enumerate domain users, if a user is specified than only its information is queried.')
egroup.add_argument("--groups", nargs='?', const='', metavar='GROUP', help='enumerate domain groups, if a group is specified than its members are enumerated')
egroup.add_argument('--computers', nargs='?', const='', metavar='COMPUTER', help='enumerate computer users')
egroup.add_argument("--local-groups", nargs='?', const='', metavar='GROUP', help='enumerate local groups, if a group is specified then its members are enumerated')
egroup.add_argument("--pass-pol", action='store_true', help='dump password policy')
egroup.add_argument("--rid-brute", nargs='?', type=int, const=4000, metavar='MAX_RID', help='enumerate users by bruteforcing RID\'s (default: 4000)')
@ -675,12 +677,17 @@ class smb(connection):
domain = domain+"."+v
return domain
def domainfromdnshostname(self, dns):
dnsparts = dns.split(".")
domain = ".".join(dnsparts[1:])
return domain, dnsparts[0]+"$"
def groups(self):
groups = []
for dc_ip in self.get_dc_ips():
if self.args.groups:
try:
groups = get_netgroupmember(dc_ip, '', self.username, password=self.password,
groups = get_netgroupmember(dc_ip, self.domain, self.username, password=self.password,
lmhash=self.lmhash, nthash=self.nthash, queried_groupname=self.args.groups, queried_sid=str(),
queried_domain=str(), ads_path=str(), recurse=False, use_matching_rule=False,
full_data=False, custom_filter=str())
@ -703,7 +710,7 @@ class smb(connection):
self.logger.error('Error enumerating domain group members using dc ip {}: {}'.format(dc_ip, e))
else:
try:
groups = get_netgroup(dc_ip, '', self.username, password=self.password,
groups = get_netgroup(dc_ip, self.domain, self.username, password=self.password,
lmhash=self.lmhash, nthash=self.nthash, queried_groupname=str(), queried_sid=str(),
queried_username=str(), queried_domain=str(), ads_path=str(),
admin_count=False, full_data=True, custom_filter=str())
@ -726,7 +733,7 @@ class smb(connection):
users = []
for dc_ip in self.get_dc_ips():
try:
users = get_netuser(dc_ip, '', self.username, password=self.password, lmhash=self.lmhash,
users = get_netuser(dc_ip, self.domain, self.username, password=self.password, lmhash=self.lmhash,
nthash=self.nthash, queried_username=self.args.users, queried_domain='', ads_path=str(),
admin_count=False, spn=False, unconstrained=False, allow_delegation=False,
custom_filter=str())
@ -735,14 +742,32 @@ class smb(connection):
for user in users:
domain = self.domainfromdsn(user.distinguishedname)
self.logger.highlight('{}\\{:<30} badpwdcount: {} baddpwdtime: {}'.format(domain,user.samaccountname,getattr(user,'badpwdcount',0),getattr(user, 'badpasswordtime','')))
self.db.add_user(domain, user.samaccountname)
#self.db.add_user(domain, user.samaccountname)
break
except Exception as e:
logging.debug('Error enumerating domain users using dc ip {}: {}'.format(dc_ip, e))
self.logger.error('Error enumerating domain users using dc ip {}: {}'.format(dc_ip, e))
self.logger.info('Trying with SAMRPC protocol')
users = UserSamrDump(self).dump()
break
return users
def computers(self):
computers = []
for dc_ip in self.get_dc_ips():
try:
computers = get_netcomputer(dc_ip, self.domain, self.username, password=self.password, lmhash=self.lmhash,
nthash=self.nthash, queried_domain='', ads_path=str(), custom_filter=str())
self.logger.success('Enumerated domain computer(s)')
for computer in computers:
domain, computerclean = self.domainfromdnshostname(computer.dnshostname)
self.logger.highlight('{}\\{:<30}'.format(domain, computerclean))
break
except Exception as e:
self.logger.error('Error enumerating domain computers using dc ip {}: {}'.format(dc_ip, e))
break
return computers
def loggedon_users(self):
loggedon = []
try:

View File

@ -173,6 +173,8 @@ class PassPolDump:
'lock_accnt_dur': self.__lock_accnt_dur, 'accnt_lock_thres': self.__accnt_lock_thres,
'force_logoff_time': self.__force_logoff_time}
dce.disconnect()
def pretty_print(self):
PASSCOMPLEX = {

View File

@ -0,0 +1,113 @@
#Stolen from Impacket
import logging
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
from impacket.dcerpc.v5 import transport, samr
from impacket.dcerpc.v5.samr import DCERPCSessionError
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket import ntlm
class UserSamrDump:
KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\samr]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\samr]', 445),
}
def __init__(self, connection):
self.logger = connection.logger
self.addr = connection.host
self.protocol = connection.args.port
self.username = connection.username
self.password = connection.password
self.domain = connection.domain
self.hash = connection.hash
self.lmhash = ''
self.nthash = ''
self.aesKey = None
self.doKerberos = False
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
self.users = []
if self.hash is not None:
if self.hash.find(':') != -1:
self.lmhash, self.nthash = self.hash.split(':')
else:
self.nthash = self.hash
if self.password is None:
self.password = ''
def dump(self):
# Try all requested protocols until one works.
for protocol in self.protocols:
try:
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
port = protodef[1]
except KeyError:
logging.debug("Invalid Protocol '{}'".format(protocol))
logging.debug("Trying protocol {}".format(protocol))
rpctransport = transport.SMBTransport(self.addr, port, r'\samr', self.username, self.password, self.domain,
self.lmhash, self.nthash, self.aesKey, doKerberos = self.doKerberos)
try:
self.fetchList(rpctransport)
except Exception as e:
logging.debug('Protocol failed: {}'.format(e))
return self.users
def fetchList(self, rpctransport):
dce = DCERPC_v5(rpctransport)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
# Setup Connection
resp = samr.hSamrConnect2(dce)
if resp['ErrorCode'] != 0:
raise Exception('Connect error')
resp2 = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle=resp['ServerHandle'],
enumerationContext=0,
preferedMaximumLength=500)
if resp2['ErrorCode'] != 0:
raise Exception('Connect error')
resp3 = samr.hSamrLookupDomainInSamServer(dce, serverHandle=resp['ServerHandle'],
name=resp2['Buffer']['Buffer'][0]['Name'])
if resp3['ErrorCode'] != 0:
raise Exception('Connect error')
resp4 = samr.hSamrOpenDomain(dce, serverHandle=resp['ServerHandle'],
desiredAccess=samr.MAXIMUM_ALLOWED,
domainId=resp3['DomainId'])
if resp4['ErrorCode'] != 0:
raise Exception('Connect error')
self.__domains = resp2['Buffer']['Buffer']
domainHandle = resp4['DomainHandle']
# End Setup
status = STATUS_MORE_ENTRIES
enumerationContext = 0
while status == STATUS_MORE_ENTRIES:
try:
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext = enumerationContext)
except DCERPCException as e:
if str(e).find('STATUS_MORE_ENTRIES') < 0:
self.logger.error('Error enumerating domain user(s)')
break
resp = e.get_packet()
self.logger.success('Enumerated domain user(s)')
for user in resp['Buffer']['Buffer']:
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user['RelativeId'])
info = samr.hSamrQueryInformationUser2(dce, r['UserHandle'],samr.USER_INFORMATION_CLASS.UserAllInformation)
(username, uid, info_user) = (user['Name'], user['RelativeId'], info['Buffer']['All'])
self.logger.highlight('{}\\{:<30} {}'.format(self.domain, user['Name'], info_user['AdminComment']))
self.users.append(user['Name'])
samr.hSamrCloseHandle(dce, r['UserHandle'])
enumerationContext = resp['EnumerationContext']
status = resp['ErrorCode']
dce.disconnect()

614
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -61,11 +61,11 @@ neo4j = "^4.1.1"
pylnk3 = "^0.3.0"
pypsrp = "^0.5.0"
paramiko = "^2.7.2"
impacket = "^0.9.22"
impacket = "^0.9.23"
xmltodict = "^0.12.0"
terminaltables = "^3.1.0"
aioconsole = "^0.3.1"
pywerview = "^0.3.0"
pywerview = "^0.3.1"
[tool.poetry.dev-dependencies]