Add bloodhound core feature + fix color on ldap proto

main
mpgn 2023-02-05 05:43:12 -05:00
parent 0c02ed4c0b
commit 4a443fe946
3 changed files with 1916 additions and 1064 deletions

View File

@ -5,15 +5,15 @@
import logging
import hmac
from argparse import _StoreTrueAction
from binascii import b2a_hex, unhexlify, hexlify
from cme.connection import *
from cme.helpers.logger import highlight
from cme.logger import CMEAdapter
from cme.helpers.bloodhound import add_user_bh
from cme.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from cme.protocols.ldap.kerberos import KerberosAttacks
from Cryptodome.Hash import MD4
from cme.protocols.ldap.bloodhound import BloodHound
from impacket.smbconnection import SMBConnection, SessionError
from impacket.smb import SMB_DIALECT
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
@ -23,9 +23,17 @@ from impacket.ldap import ldap as ldap_impacket
from impacket.krb5 import constants
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR
from bloodhound.ad.domain import AD
from bloodhound.ad.authentication import ADAuthentication
from argparse import _StoreTrueAction
from binascii import b2a_hex, unhexlify, hexlify
from Cryptodome.Hash import MD4
from io import StringIO
from pywerview.cli.helpers import *
from re import sub, I
from zipfile import ZipFile
ldap_error_status = {
"1":"STATUS_NOT_SUPPORTED",
@ -41,6 +49,53 @@ ldap_error_status = {
"KDC_ERR_PREAUTH_FAILED":"KDC_ERR_PREAUTH_FAILED"
}
def resolve_collection_methods(methods):
"""
Convert methods (string) to list of validated methods to resolve
"""
valid_methods = ['group', 'localadmin', 'session', 'trusts', 'default', 'all', 'loggedon',
'objectprops', 'experimental', 'acl', 'dcom', 'rdp', 'psremote', 'dconly',
'container']
default_methods = ['group', 'localadmin', 'session', 'trusts']
# Similar to SharpHound, All is not really all, it excludes loggedon
all_methods = ['group', 'localadmin', 'session', 'trusts', 'objectprops', 'acl', 'dcom', 'rdp', 'psremote', 'container']
# DC only, does not collect to computers
dconly_methods = ['group', 'trusts', 'objectprops', 'acl', 'container']
if ',' in methods:
method_list = [method.lower() for method in methods.split(',')]
validated_methods = []
for method in method_list:
if method not in valid_methods:
logging.error('Invalid collection method specified: %s', method)
return False
if method == 'default':
validated_methods += default_methods
elif method == 'all':
validated_methods += all_methods
elif method == 'dconly':
validated_methods += dconly_methods
else:
validated_methods.append(method)
return set(validated_methods)
else:
validated_methods = []
# It is only one
method = methods.lower()
if method in valid_methods:
if method == 'default':
validated_methods += default_methods
elif method == 'all':
validated_methods += all_methods
elif method == 'dconly':
validated_methods += dconly_methods
else:
validated_methods.append(method)
return set(validated_methods)
else:
logging.error('Invalid collection method specified: %s', method)
return False
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
@ -113,6 +168,11 @@ class ldap(connection):
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
bgroup = ldap_parser.add_argument_group("Bloodhound scan", "Options to play with bloodhoud")
bgroup.add_argument("--bloodhound", action="store_true", help="Perform bloodhound scan")
bgroup.add_argument("-ns", '--nameserver', help="Custom DNS IP")
bgroup.add_argument("-c", "--collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma. (default: Default)'")
return parser
def proto_logger(self):
@ -210,9 +270,6 @@ class ldap(connection):
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection['RequireSigning']
self.os_arch = self.get_os_arch()
self.output_filename = os.path.expanduser('~/.cme/logs/{}_{}_{}'.format(self.hostname, self.host, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
self.output_filename = self.output_filename.replace(":", "-")
if not self.domain:
self.domain = self.hostname
@ -233,6 +290,8 @@ class ldap(connection):
#Re-connect since we logged off
self.create_conn_obj()
self.output_filename = os.path.expanduser('~/.cme/logs/{}_{}_{}'.format(self.hostname, self.host, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
self.output_filename = self.output_filename.replace(":", "-")
def print_host_info(self):
if self.args.no_smb:
@ -258,6 +317,8 @@ class ldap(connection):
self.username = username
self.password = password
self.domain = domain
self.kdcHost = kdcHost
self.aesKey = aesKey
lmhash = ''
nthash = ''
@ -287,7 +348,7 @@ class ldap(connection):
try:
# Connect to LDAP
proto = "ldaps" if self.args.gmsa else "ldap"
proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap"
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % self.target, self.baseDN)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash,
aesKey, kdcHost=kdcHost, useCache=useCache)
@ -305,7 +366,7 @@ class ldap(connection):
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
self.logger.extra['protocol'] = "LDAP"
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
self.logger.extra['port'] = "636" if (self.args.gmsa or self.args.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
@ -418,7 +479,7 @@ class ldap(connection):
try:
# Connect to LDAP
proto = "ldaps" if self.args.gmsa else "ldap"
proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap"
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % self.target, self.baseDN)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
@ -430,7 +491,7 @@ class ldap(connection):
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
self.logger.extra['protocol'] = "LDAP"
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
self.logger.extra['port'] = "636" if (self.args.gmsa or self.args.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
@ -466,14 +527,14 @@ class ldap(connection):
self.username,
self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode and errorCode != 1 in ldap_error_status else 'red')
color='magenta' if (errorCode in ldap_error_status and errorCode != 1) else 'red')
else:
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode and errorCode != 1 in ldap_error_status else 'red')
color='magenta' if (errorCode in ldap_error_status and errorCode != 1) else 'red')
return False
except OSError as e:
@ -513,7 +574,7 @@ class ldap(connection):
try:
# Connect to LDAP
proto = "ldaps" if self.args.gmsa else "ldap"
proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap"
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % self.target, self.baseDN)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
@ -524,7 +585,7 @@ class ldap(connection):
self.nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
self.logger.extra['protocol'] = "LDAP"
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
self.logger.extra['port'] = "636" if (self.args.gmsa or self.args.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
@ -558,14 +619,14 @@ class ldap(connection):
self.username,
nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode and errorCode != 1 in ldap_error_status else 'red')
color='magenta' if (errorCode in ldap_error_status and errorCode != 1) else 'red')
else:
errorCode = str(e).split()[-2][:-1]
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
self.username,
nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
ldap_error_status[errorCode] if errorCode in ldap_error_status else ''),
color='magenta' if errorCode and errorCode != 1 in ldap_error_status else 'red')
color='magenta' if (errorCode in ldap_error_status and errorCode != 1) else 'red')
return False
except OSError as e:
self.logger.error(u'{}\\{}:{} {}'.format(self.domain,
@ -1177,4 +1238,64 @@ class ldap(connection):
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight("Account: {:<20} NTLM: {}".format(gmsa_id, passwd))
else:
self.logger.error("No string provided :'(")
self.logger.error("No string provided :'(")
def bloodhound(self):
print(self.args.use_kcache)
print(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, self.kdcHost)
auth = ADAuthentication(username=self.username, password=self.password, domain=self.domain, lm_hash=self.nthash, nt_hash=self.nthash, aeskey=self.aesKey, kdc=self.kdcHost, auth_method='auto')
ad = AD(auth=auth, domain=self.domain, nameserver=self.args.nameserver, dns_tcp=False, dns_timeout=3)
collect = resolve_collection_methods('Default' if not self.args.collection else self.args.collection)
if not collect:
return
self.logger.highlight('Resolved collection methods: %s', ', '.join(list(collect)))
logging.debug('Using DNS to retrieve domain information')
ad.dns_resolve(domain=self.domain)
if self.args.kerberos:
self.logger.highlight("Using kerberos auth without ccache, getting TGT")
auth.get_tgt()
if self.args.use_kcache:
self.logger.highlight("Using kerberos auth from ccache")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_"
bloodhound = BloodHound(ad, self.hostname, self.host, self.args.port)
bloodhound.connect()
# root_logger = logging.getLogger()
# root_logger.setLevel(logging.INFO)
# bad bad coding but it's late and I don't have much time
from termcolor import colored
debug_output_string = "{:<24} {:<15} {:<6} {:<16} \033[1m\x1b[33;20m%(message)s \x1b[0m".format(colored(self.logger.extra['protocol'], 'blue', attrs=['bold']),
self.logger.extra['host'],
self.logger.extra['port'],
self.logger.extra['hostname'] if self.logger.extra['hostname'] else 'NONE')
formatter = logging.Formatter(debug_output_string)
streamHandler = logging.StreamHandler()
streamHandler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.handlers = []
root_logger.addHandler(streamHandler)
root_logger.setLevel(logging.INFO)
bloodhound.run(collect=collect,
num_workers=10,
disable_pooling=False,
timestamp=timestamp,
computerfile=None,
cachefile=None,
exclude_dcs=False)
self.logger.highlight("Compressing output into " + self.output_filename + "bloodhound.zip")
list_of_files = os.listdir(os.getcwd())
with ZipFile(self.output_filename + "bloodhound.zip",'w') as zip:
for each_file in list_of_files:
if each_file.startswith(timestamp) and each_file.endswith("json"):
zip.write(each_file)
os.remove(each_file)

2823
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,8 @@ pywerview = "^0.3.3"
minikerberos = "0.3.5"
aardwolf = "0.2.5"
masky = "^0.1.1"
bloodhound = { git = "https://github.com/fox-it/BloodHound.py", rev = "815684ba8a06d4e8b5bcc69be9bdc071ea9bf1c4" }
asyauth = "^0.0.12"
[tool.poetry.dev-dependencies]
flake8 = "*"