From af91d617c382e1eb132506159debcbc10da7a567 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 13 Dec 2024 05:20:59 +1000 Subject: [PATCH 1/9] SMB Server fix filename offsets (#1831) Correctly set the NextEntryOffset for the SMBFindFileNamesInfo results when querying the names of files. The current logic does not set this value so will be set to 0 making the client believe there are no more entries in the result if going by the MS-SMB2 logic. --- impacket/smbserver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 649228c58..70955abc1 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -502,6 +502,9 @@ def findFirst2(path, fileName, level, searchAttributes, pktFlags=smb.SMB.FLAGS2_ item['LastAccessTime'] = getSMBTime(atime) item['LastWriteDate'] = getSMBDate(mtime) item['LastWriteTime'] = getSMBTime(mtime) + elif level in [smb.SMB_FIND_FILE_NAMES_INFO, smb2.SMB2_FILE_NAMES_INFO]: + padLen = (8 - (len(item) % 8)) % 8 + item['NextEntryOffset'] = len(item) + padLen searchResult.append(item) # No more files From 67e19240fa66ee9f461b506d7f50723e5960cb7c Mon Sep 17 00:00:00 2001 From: alexisbalbachan Date: Mon, 16 Dec 2024 17:28:07 -0300 Subject: [PATCH 2/9] dhcp: convert options from bytearray to bytes before unpacking (#1858) --- impacket/dhcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/dhcp.py b/impacket/dhcp.py index aeabb4013..dde0c3909 100644 --- a/impacket/dhcp.py +++ b/impacket/dhcp.py @@ -178,7 +178,7 @@ def unpackOptions(self, options): # size = self.calcUnpackSize(format, options[i+1:]) size = options[i+1] # print i, name, format, size - value = self.unpack(format, options[i+2:i+2+size]) + value = self.unpack(format, bytes(options[i+2:i+2+size])) answer.append((name, value)) i += 2+size From c1a53aaa133fc0b20258bb00c7abb3b0cc0b3929 Mon Sep 17 00:00:00 2001 From: marcobarlottini Date: Fri, 20 Dec 2024 20:36:08 +0100 Subject: [PATCH 3/9] feat in net.py: add functionality to enable and disable user accounts (#1801) * feat in net.py: add functionality to enable and disable user accounts * fix: do not overwrite flags when enabling/disabling accounts * call function create account with according flags in net.py * refactor keep uac flags out of parameters of enable,disable function definitions --- examples/net.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/examples/net.py b/examples/net.py index 2d8eb3de3..85500b602 100644 --- a/examples/net.py +++ b/examples/net.py @@ -24,6 +24,8 @@ # python net.py Administrator:password@targetMachine group -name "Domain Admins" # python net.py Administrator:password@targetMachine computer -name DC$ # python net.py Administrator:password@targetMachine group -name "Domain Admins" -join EvilUs3r +# python net.py Administrator:password@targetMachine user -enable EvilUs3r +# python net.py Administrator:password@targetMachine user -disable EvilUs3r # # Author: # Alex Romero (@NtAlexio2) @@ -215,11 +217,32 @@ def Remove(self, name): self._close_domain() def _hEnableAccount(self, user_handle): + user_account_control = samr.hSamrQueryInformationUser2(self._dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)['Buffer']['All']['UserAccountControl'] buffer = samr.SAMPR_USER_INFO_BUFFER() buffer['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation - buffer['Control']['UserAccountControl'] = samr.USER_ALL_ADMINCOMMENT + buffer['Control']['UserAccountControl'] = user_account_control ^ samr.USER_ACCOUNT_DISABLED samr.hSamrSetInformationUser2(self._dce, user_handle, buffer) + def _hDisableAccount(self, user_handle): + user_account_control = samr.hSamrQueryInformationUser2(self._dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)['Buffer']['All']['UserAccountControl'] + buffer = samr.SAMPR_USER_INFO_BUFFER() + buffer['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation + buffer['Control']['UserAccountControl'] = samr.USER_ACCOUNT_DISABLED | user_account_control + samr.hSamrSetInformationUser2(self._dce, user_handle, buffer) + + def SetUserAccountControl(self, name, action): + info = self.Query(name) + domain_handle = self._open_domain() + try: + user_handle = self._get_user_handle(domain_handle, name) + if action == 'enable': + self._hEnableAccount(user_handle) + else: + self._hDisableAccount(user_handle) + finally: + self._close_domain() + + class Computer(User): def __init__(self, smbConnection): @@ -358,6 +381,16 @@ def run(self, remoteName, remoteHost): actionObject.Remove(self.__options.remove) print("[+] {} account deleted succesfully!".format(self.__action)) + elif self.__is_option_present(self.__options, 'enable'): + print("[*] Enabling {} account '{}'".format(self.__action, self.__options.enable)) + actionObject.SetUserAccountControl(self.__options.enable, "enable") + print("[+] {} account enabled succesfully!".format(self.__action)) + + elif self.__is_option_present(self.__options, 'disable'): + print("[*] Disabling {} account '{}'".format(self.__action, self.__options.disable)) + actionObject.SetUserAccountControl(self.__options.disable, "disable") + print("[+] {} account disabled succesfully!".format(self.__action)) + elif self.__is_option_present(self.__options, 'join'): print("[*] Adding user account '{}' to group '{}'".format(self.__options.join,self.__options.name)) actionObject.Join(self.__options.name, self.__options.join) @@ -466,12 +499,16 @@ def __is_option_present(self, options, option): user_parser.add_argument('-create', action="store", metavar = "NAME", help='Add new user account to domain/computer.') user_parser.add_argument('-remove', action="store", metavar = "NAME", help='Remove existing user account from domain/computer.') user_parser.add_argument('-newPasswd', action="store", metavar = "PASSWORD", help='New password to set for creating account.') + user_parser.add_argument('-enable', action="store", metavar = "NAME", help='Enables account.') + user_parser.add_argument('-disable', action="store", metavar = "NAME", help='Disables account.') computer_parser = subparsers.add_parser('computer', help='Enumerate all computers in domain level') computer_parser.add_argument('-name', action="store", metavar = "NAME", help='Display single computer information.') computer_parser.add_argument('-create', action="store", metavar = "NAME", help='Add new computer account to domain.') computer_parser.add_argument('-remove', action="store", metavar = "NAME", help='Remove existing computer account from domain.') computer_parser.add_argument('-newPasswd', action="store", metavar = "PASSWORD", help='New password to set for creating account.') + computer_parser.add_argument('-enable', action="store", metavar = "NAME", help='Enables account.') + computer_parser.add_argument('-disable', action="store", metavar = "NAME", help='Disables account.') localgroup_parser = subparsers.add_parser('localgroup', help='Enumerate local groups (aliases) of local computer') localgroup_parser.add_argument('-name', action="store", metavar = "NAME", help='Operate on single specific domain group account.') From 9c8e4083e86cb005481daa834c858c17da9a734b Mon Sep 17 00:00:00 2001 From: Pierre Milioni <43686727+b1two@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:24:33 +0100 Subject: [PATCH 4/9] Add support for LDAP and LDAPS protocols in ntlmrelayx SOCKS (#1825) * Add support for LDAP and LDAPS in ntlmrelayx SOCKS Should fix #514 * Use real NTLM Challenge message during LDAP socks relay * Reply to generic LDAP messages that comes before authentication and drop unbind LDAP messages * Fix missing imports * LDAP socks code cleaning * Better handling of initial LDAP bind request in ntlmrelayx LDAP socks * Better handling of clients' closing connections in ntlmrelayx LDAP socks --- .../ntlmrelayx/clients/ldaprelayclient.py | 8 + .../ntlmrelayx/servers/socksplugins/ldap.py | 313 ++++++++++++++++++ .../ntlmrelayx/servers/socksplugins/ldaps.py | 47 +++ 3 files changed, 368 insertions(+) create mode 100644 impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py create mode 100644 impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py diff --git a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py index 9f69a71be..63536bae2 100644 --- a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py @@ -99,6 +99,7 @@ def sendNegotiate(self, negotiateMessage): if result['result'] == RESULT_SUCCESS: challenge = NTLMAuthChallenge() challenge.fromString(result['server_creds']) + self.sessionData['CHALLENGE_MESSAGE'] = challenge return challenge else: raise LDAPRelayClientException('Server did not offer NTLM authentication!') @@ -156,6 +157,13 @@ def create_authenticate_message(self): def parse_challenge_message(self, message): pass + def keepAlive(self): + # Basic LDAP query to keep the connection alive + self.session.search(search_base='', + search_filter='(objectClass=*)', + search_scope='BASE', + attributes=['namingContexts']) + class LDAPSRelayClient(LDAPRelayClient): PLUGIN_NAME = "LDAPS" MODIFY_ADD = MODIFY_ADD diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py new file mode 100644 index 000000000..889fc951d --- /dev/null +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -0,0 +1,313 @@ +import select +from pyasn1.codec.ber import encoder, decoder +from pyasn1.error import SubstrateUnderrunError +from pyasn1.type import univ + +from impacket import LOG, ntlm +from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay +from impacket.ldap.ldap import LDAPSessionError +from impacket.ldap.ldapasn1 import KNOWN_NOTIFICATIONS, LDAPDN, NOTIFICATION_DISCONNECT, BindRequest, BindResponse, SearchRequest, SearchResultEntry, SearchResultDone, LDAPMessage, LDAPString, ResultCode, PartialAttributeList, PartialAttribute, AttributeValue, UnbindRequest, ExtendedRequest +from impacket.ntlm import NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_SEAL + +PLUGIN_CLASS = 'LDAPSocksRelay' + +class LDAPSocksRelay(SocksRelay): + PLUGIN_NAME = 'LDAP Socks Plugin' + PLUGIN_SCHEME = 'LDAP' + + MSG_SIZE = 4096 + + def __init__(self, targetHost, targetPort, socksSocket, activeRelays): + SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) + + @staticmethod + def getProtocolPort(): + return 389 + + def initConnection(self): + # No particular action required to initiate the connection + pass + + def skipAuthentication(self): + # Faking an NTLM authentication with the client + while True: + messages = self.recv_ldap_msg() + if messages is None: + LOG.warning('LDAP: Client did not send ldap messages or closed connection') + return False + LOG.debug(f'LDAP: Received {len(messages)} message(s)') + + for message in messages: + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(BindRequest): + # BindRequest received + + if msg_component['authentication'] == univ.OctetString(''): + # First bind message without authentication + # Replying with a request for NTLM authentication + + LOG.debug('LDAP: Got empty bind request') + + bindresponse = BindResponse() + bindresponse['resultCode'] = ResultCode('success') + bindresponse['matchedDN'] = LDAPDN('NTLM') + bindresponse['diagnosticMessage'] = LDAPString('') + self.send_ldap_msg(bindresponse, message['messageID']) + + # Let's receive next messages + continue + + elif 'sicilyNegotiate' in msg_component['authentication']: + # Requested NTLM authentication + + LOG.debug('LDAP: Got NTLM bind request') + + # Load negotiate message + negotiateMessage = ntlm.NTLMAuthNegotiate() + negotiateMessage.fromString(msg_component['authentication']['sicilyNegotiate'].asOctets()) + + # Reuse the challenge message from the real authentication with the server + challengeMessage = self.sessionData['CHALLENGE_MESSAGE'] + # We still remove the annoying flags + challengeMessage['flags'] &= ~(NTLMSSP_NEGOTIATE_SIGN) + challengeMessage['flags'] &= ~(NTLMSSP_NEGOTIATE_SEAL) + + # Building the LDAP bind response message + bindresponse = BindResponse() + bindresponse['resultCode'] = ResultCode('success') + bindresponse['matchedDN'] = LDAPDN(challengeMessage.getData()) + bindresponse['diagnosticMessage'] = LDAPString('') + + # Sending the response + self.send_ldap_msg(bindresponse, message['messageID']) + + elif 'sicilyResponse' in msg_component['authentication']: + # Received an NTLM auth bind request + + # Parsing authentication method + chall_response = ntlm.NTLMAuthChallengeResponse() + chall_response.fromString(msg_component['authentication']['sicilyResponse'].asOctets()) + + username = chall_response['user_name'].decode('utf-16le') + domain = chall_response['domain_name'].decode('utf-16le') + self.username = f'{domain}/{username}' + + # Checking for the two formats the domain can have (taken from both HTTP and SMB socks plugins) + if f'{domain}/{username}'.upper() in self.activeRelays: + self.username = f'{domain}/{username}'.upper() + elif f'{domain.split(".", 1)[0]}/{username}'.upper() in self.activeRelays: + self.username = f'{domain.split(".", 1)[0]}/{username}'.upper() + else: + # Username not in active relays + LOG.error('LDAP: No session for %s@%s(%s) available' % ( + username, self.targetHost, self.targetPort)) + return False + + if self.activeRelays[self.username]['inUse'] is True: + LOG.error('LDAP: Connection for %s@%s(%s) is being used at the moment!' % ( + self.username, self.targetHost, self.targetPort)) + return False + else: + LOG.info('LDAP: Proxying client session for %s@%s(%s)' % ( + self.username, self.targetHost, self.targetPort)) + self.activeRelays[self.username]['inUse'] = True + self.session = self.activeRelays[self.username]['protocolClient'].session.socket + + # Building successful LDAP bind response + bindresponse = BindResponse() + bindresponse['resultCode'] = ResultCode('success') + bindresponse['matchedDN'] = LDAPDN('') + bindresponse['diagnosticMessage'] = LDAPString('') + + # Sending successful response + self.send_ldap_msg(bindresponse, message['messageID']) + + return True + else: + LOG.error('LDAP: Received an unknown LDAP binding request, cannot continue') + return False + + else: + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(SearchRequest): + # Pre-auth search request + + if msg_component['attributes'][0] == LDAPString('supportedCapabilities'): + # supportedCapabilities + response = SearchResultEntry() + response['objectName'] = LDAPDN('') + response['attributes'] = PartialAttributeList() + + attribs = PartialAttribute() + attribs.setComponentByName('type', 'supportedCapabilities') + attribs.setComponentByName('vals', univ.SetOf(componentType=AttributeValue())) + # LDAP_CAP_ACTIVE_DIRECTORY_OID + attribs.getComponentByName('vals').setComponentByPosition(0, AttributeValue('1.2.840.113556.1.4.800')) + # LDAP_CAP_ACTIVE_DIRECTORY_V51_OID + attribs.getComponentByName('vals').setComponentByPosition(1, AttributeValue('1.2.840.113556.1.4.1670')) + # LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID + attribs.getComponentByName('vals').setComponentByPosition(2, AttributeValue('1.2.840.113556.1.4.1791')) + # ISO assigned OIDs + attribs.getComponentByName('vals').setComponentByPosition(3, AttributeValue('1.2.840.113556.1.4.1935')) + attribs.getComponentByName('vals').setComponentByPosition(4, AttributeValue('1.2.840.113556.1.4.2080')) + attribs.getComponentByName('vals').setComponentByPosition(5, AttributeValue('1.2.840.113556.1.4.2237')) + + response['attributes'].append(attribs) + + elif msg_component['attributes'][0] == LDAPString('supportedSASLMechanisms'): + # supportedSASLMechanisms + response = SearchResultEntry() + response['objectName'] = LDAPDN('') + response['attributes'] = PartialAttributeList() + + attribs = PartialAttribute() + attribs.setComponentByName('type', 'supportedSASLMechanisms') + attribs.setComponentByName('vals', univ.SetOf(componentType=AttributeValue())) + # Force NTLMSSP to avoid parsing every type of authentication + attribs.getComponentByName('vals').setComponentByPosition(0, AttributeValue('NTLM')) + + response['attributes'].append(attribs) + else: + # Any other message triggers the closing of client connection + return False + + # Sending message + self.send_ldap_msg(response, message['messageID']) + # Sending searchResDone + result_done = SearchResultDone() + result_done['resultCode'] = ResultCode('success') + result_done['matchedDN'] = LDAPDN('') + result_done['diagnosticMessage'] = LDAPString('') + self.send_ldap_msg(result_done, message['messageID']) + + def recv_ldap_msg(self): + '''Receive LDAP messages during the SOCKS client LDAP authentication.''' + + data = b'' + done = False + while not done: + recvData = self.socksSocket.recv(self.MSG_SIZE) + if recvData == b'': + # Connection got closed + return None + if len(recvData) < self.MSG_SIZE: + done = True + data += recvData + + response = [] + while len(data) > 0: + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + except SubstrateUnderrunError: + # We need more data + new_data = self.socksSocket.recv(self.MSG_SIZE) + if new_data == b'': + # Connection got closed + return None + remaining = data + new_data + else: + response.append(message) + data = remaining + + return response + + def send_ldap_msg(self, response, message_id, controls=None): + '''Send LDAP messages during the SOCKS client LDAP authentication.''' + + message = LDAPMessage() + message['messageID'] = message_id + message['protocolOp'].setComponentByType(response.getTagSet(), response) + if controls is not None: + message['controls'].setComponents(*controls) + + data = encoder.encode(message) + + return self.socksSocket.sendall(data) + + def wait_for_data(self, socket1, socket2): + return select.select([socket1, socket2], [], [])[0] + + def passthrough_sockets(self, client_sock, server_sock): + while True: + rready = self.wait_for_data(client_sock, server_sock) + + for sock in rready: + + if sock == client_sock: + # Data received from client + try: + read = client_sock.recv(self.MSG_SIZE) + except Exception: + read = '' + if not read: + return + + if not self.is_allowed_request(read): + # Stop client connection when unallowed requests are made + return + + if not self.is_forwardable_request(read): + # Do not forward unbind requests, otherwise we would loose the SOCKS + continue + + try: + server_sock.send(read) + except Exception: + raise BrokenPipeError('Broken pipe: LDAP server is gone') + + elif sock == server_sock: + # Data received from server + try: + read = server_sock.recv(self.MSG_SIZE) + except Exception: + read = '' + if not read: + raise BrokenPipeError('Broken pipe: LDAP server is gone') + + try: + client_sock.send(read) + except Exception: + return + + def tunnelConnection(self): + '''Charged of tunneling the rest of the connection.''' + + self.passthrough_sockets(self.socksSocket, self.session) + + # Free the relay so that it can be reused + self.activeRelays[self.username]['inUse'] = False + + LOG.debug('LDAP: Finished tunnelling') + + return True + + def is_forwardable_request(self, data): + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() + + # Search for unbind requests + if msg_component.isSameTypeWith(UnbindRequest): + LOG.warning('LDAP: Client tried to unbind LDAP connection, skipping message') + return False + except Exception: + # Is probably not an unbind LDAP message + pass + + return True + + def is_allowed_request(self, data): + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() + + # Search for START_TLS LDAP extendedReq OID + if msg_component.isSameTypeWith(ExtendedRequest) and msg_component['requestName'].asOctets() == b'1.3.6.1.4.1.1466.20037': + # 1.3.6.1.4.1.1466.20037 is LDAP_START_TLS_OID + LOG.warning('LDAP: Client tried to initiate Start TLS, closing connection') + return False + except Exception: + # Is probably not a ExtendedReq message + pass + + return True diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py new file mode 100644 index 000000000..001b65adb --- /dev/null +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py @@ -0,0 +1,47 @@ +import select +from impacket import LOG +from impacket.examples.ntlmrelayx.servers.socksplugins.ldap import LDAPSocksRelay +from impacket.examples.ntlmrelayx.utils.ssl import SSLServerMixin +from OpenSSL import SSL + +PLUGIN_CLASS = "LDAPSSocksRelay" + +class LDAPSSocksRelay(SSLServerMixin, LDAPSocksRelay): + PLUGIN_NAME = 'LDAPS Socks Plugin' + PLUGIN_SCHEME = 'LDAPS' + + def __init__(self, targetHost, targetPort, socksSocket, activeRelays): + LDAPSocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) + + @staticmethod + def getProtocolPort(): + return 636 + + def skipAuthentication(self): + LOG.debug('Wrapping client connection in TLS/SSL') + self.wrapClientConnection() + + # Skip authentication using the same technique as LDAP + try: + if not LDAPSocksRelay.skipAuthentication(self): + # Shut down TLS connection + self.socksSocket.shutdown() + return False + except SSL.SysCallError: + LOG.warning('Cannot wrap client socket in TLS/SSL') + return False + + return True + + def wait_for_data(self, socket1, socket2): + rready = [] + + if socket1.pending(): + rready.append(socket1) + if socket2.pending(): + rready.append(socket2) + + if not rready: + rready, _, exc = select.select([socket1, socket2], [], []) + + return rready From 06863aeca181b4dbb593ddb3b530e6aaf7d7ef70 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 3 Jan 2025 22:01:11 +1000 Subject: [PATCH 5/9] Add SMB2 attribute tag and fix SMB2 query results (#1835) Adds the FILE_ATTRIBUTE_TAG_INFORMATION structure that can be requested in an SMB2 query info request. Returns the SMB2_FILE_BASIC_INFO result for SMB2 queries and return the SMB2 not the SMB1 structure for SMB2_FILE_STANDARD_INFO. --- impacket/smb3structs.py | 6 ++++++ impacket/smbserver.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/impacket/smb3structs.py b/impacket/smb3structs.py index efd1ec740..556e3e6f0 100644 --- a/impacket/smb3structs.py +++ b/impacket/smb3structs.py @@ -1496,6 +1496,12 @@ class FILE_ALL_INFORMATION(Structure): ('NameInformation',':',FILE_NAME_INFORMATION), ) +class FILE_ATTRIBUTE_TAG_INFORMATION(Structure): + structure = ( + ('FileAttributes',' Date: Tue, 7 Jan 2025 03:19:32 +1000 Subject: [PATCH 6/9] Add NTLM SIGN flag for SMB Server (#1826) Adds the NTLMSSP_NEGOTIATE_SIGN flag to the NTLM CHALLENGE message returned by the SMB server. This is needeed for clients that generate a SPNEGO mechListMIC which require signing to be enabled on the NTLM context. --- impacket/smbserver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 5cf877ffa..581c40988 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -2926,6 +2926,8 @@ def smb2SessionSetup(connId, smbServer, recvPacket): ansFlags |= ntlm.NTLMSSP_NEGOTIATE_UNICODE if negotiateMessage['flags'] & ntlm.NTLM_NEGOTIATE_OEM: ansFlags |= ntlm.NTLM_NEGOTIATE_OEM + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_SIGN: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_SIGN ansFlags |= ntlm.NTLMSSP_NEGOTIATE_VERSION | ntlm.NTLMSSP_NEGOTIATE_TARGET_INFO | ntlm.NTLMSSP_TARGET_TYPE_SERVER | ntlm.NTLMSSP_NEGOTIATE_NTLM | ntlm.NTLMSSP_REQUEST_TARGET From 72648af3780f144d5f071227174944fc590b5bef Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 8 Jan 2025 21:34:47 +1000 Subject: [PATCH 7/9] Fix SMB2 compoud response signing (#1834) * Fix SMB2 compoud response signing Fix the signing the logic when responding with an SMB2 compount response. The signature will include the padding of each compound element and include the next offset value before signing the data. * Pad all SMB2 packets, even the last in a compound response --- impacket/smbserver.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 581c40988..605a2b463 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -4403,10 +4403,11 @@ def signSMBv1(self, connData, packet, signingSessionKey, signingChallengeRespons packet['SecurityFeatures'] = m.digest()[:8] connData['SignSequenceNumber'] += 2 - def signSMBv2(self, packet, signingSessionKey): + def signSMBv2(self, packet, signingSessionKey, padLength=0): packet['Signature'] = b'\x00' * 16 packet['Flags'] |= smb2.SMB2_FLAGS_SIGNED - signature = hmac.new(signingSessionKey, packet.getData(), hashlib.sha256).digest() + packetData = packet.getData() + b'\x00' * padLength + signature = hmac.new(signingSessionKey, packetData, hashlib.sha256).digest() packet['Signature'] = signature[:16] # print "%s" % packet['Signature'].encode('hex') @@ -4624,34 +4625,29 @@ def processRequest(self, connId, data): else: respPacket['Data'] = str(respCommand) - if connData['SignatureEnabled']: - self.signSMBv2(respPacket, connData['SigningSessionKey']) - packetsToSend.append(respPacket) else: # The SMBCommand took care of building the packet packetsToSend = respPackets if isSMB2 is True: - # Let's build a compound answer - finalData = b'' - i = 0 - for i in range(len(packetsToSend) - 1): - packet = packetsToSend[i] - # Align to 8-bytes - padLen = (8 - (len(packet) % 8)) % 8 - packet['NextCommand'] = len(packet) + padLen + # Let's build a compound answer and sign it + finalData = [] + totalPackets = len(packetsToSend) + for idx, packet in enumerate(packetsToSend): + padLen = -len(packet) % 8 + if idx + 1 < totalPackets: + packet['NextCommand'] = len(packet) + padLen + + if connData['SignatureEnabled']: + self.signSMBv2(packet, connData['SigningSessionKey'], padLength=padLen) + if hasattr(packet, 'getData'): - finalData += packet.getData() + padLen * b'\x00' + finalData.append(packet.getData() + padLen * b'\x00') else: - finalData += packet + padLen * b'\x00' + finalData.append(packet + padLen * b'\x00') - # Last one - if hasattr(packetsToSend[len(packetsToSend) - 1], 'getData'): - finalData += packetsToSend[len(packetsToSend) - 1].getData() - else: - finalData += packetsToSend[len(packetsToSend) - 1] - packetsToSend = [finalData] + packetsToSend = [b"".join(finalData)] # We clear the compound requests connData['LastRequest'] = {} From bfa7b939f8af47d4205531eddf0d5696be655d88 Mon Sep 17 00:00:00 2001 From: d0gkiller87 <32069685+d0gkiller87@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:14:49 +0800 Subject: [PATCH 8/9] Fix directory traversal for smbComDeleteDirectory() (#1864) --- impacket/smbserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/smbserver.py b/impacket/smbserver.py index 605a2b463..968cb9196 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -1771,7 +1771,7 @@ def smbComDeleteDirectory(connId, smbServer, SMBCommand, recvPacket): smbServer.log("Path not in current working directory", logging.ERROR) errorCode = STATUS_OBJECT_PATH_SYNTAX_BAD - if os.path.exists(pathName) is not True: + elif not os.path.exists(pathName): errorCode = STATUS_NO_SUCH_FILE else: From ac02e0ee493b4f219e44d92bf9a159a08ab0c7df Mon Sep 17 00:00:00 2001 From: Jannik Vieten Date: Thu, 9 Jan 2025 13:17:05 +0100 Subject: [PATCH 9/9] changepasswd.py: improved error handling (#1865) --- examples/changepasswd.py | 67 ++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/examples/changepasswd.py b/examples/changepasswd.py index d18fcfdca..50cc2f004 100755 --- a/examples/changepasswd.py +++ b/examples/changepasswd.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# Copyright Fortra, LLC and its affiliated companies +# Copyright Fortra, LLC and its affiliated companies # # All rights reserved. # @@ -126,12 +126,14 @@ from impacket import version from impacket.dcerpc.v5 import transport, samr, epm -from impacket.krb5 import kpasswd +from impacket.krb5 import kerberosv5, kpasswd from impacket.ldap import ldap, ldapasn1 from impacket.examples import logger from impacket.examples.utils import parse_target +import OpenSSL + EMPTY_LM_HASH = "aad3b435b51404eeaad3b435b51404ee" @@ -287,12 +289,12 @@ def _changePassword( aesKey=self.aesKey, kdcHost=self.kdcHost, ) - except kpasswd.KPasswdError as e: + except (kerberosv5.KerberosError, kpasswd.KPasswdError) as e: logging.error(f"Password not changed: {e}") return False - else: - logging.info("Password was changed successfully.") - return True + + logging.info("Password was changed successfully.") + return True def _setPassword(self, targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT): if not newPassword: @@ -312,10 +314,12 @@ def _setPassword(self, targetUsername, targetDomain, newPassword, newPwdHashLM, aesKey=self.aesKey, kdcHost=self.kdcHost, ) - except kpasswd.KPasswdError as e: + except (kerberosv5.KerberosError, kpasswd.KPasswdError) as e: logging.error(f"Password not changed for {targetDomain}\\{targetUsername}: {e}") - else: - logging.info(f"Password was set successfully for {targetDomain}\\{targetUsername}.") + return False + + logging.info(f"Password was set successfully for {targetDomain}\\{targetUsername}.") + return True class SamrPassword(PasswordHandler): @@ -414,6 +418,10 @@ def connect(self, retry_if_expired=False): ) logging.debug(str(e)) return False + elif "STATUS_ACCOUNT_DISABLED" in str(e): + logging.critical("The account is currently disabled.") + logging.debug(str(e)) + return False else: raise e @@ -442,6 +450,10 @@ def hSamrOpenUser(self, username): ) logging.debug(str(e)) return False + elif "STATUS_ACCESS_DENIED" in str(e): + logging.critical("Access denied") + logging.debug(str(e)) + return False else: raise e @@ -541,7 +553,7 @@ def _changePassword( targetUsername, oldPassword, "", oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT ) if res: - logging.warning("User will need to change their password on next logging because we are using hashes.") + logging.warning("User might need to change their password at next logon because we set hashes (unless password never expires is set).") return res def _setPassword(self, targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT): @@ -569,7 +581,7 @@ def _changePassword( logging.warning( "MS-RPC transport requires new password in plaintext in default Active Directory configuration. Trying anyway." ) - super()._changePassword( + return super()._changePassword( targetUsername, targetDomain, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT ) @@ -577,7 +589,7 @@ def _setPassword(self, targetUsername, targetDomain, newPassword, newPwdHashLM, logging.warning( "MS-RPC transport does not allow password reset in default Active Directory configuration. Trying anyway." ) - super()._setPassword(targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT) + return super()._setPassword(targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT) class SmbPassword(SamrPassword): @@ -615,7 +627,7 @@ def connect(self, targetDomain): self.aesKey, kdcHost=self.kdcHost, ) - except ldap.LDAPSessionError as e: + except (ldap.LDAPSessionError, OpenSSL.SSL.SysCallError) as e: logging.error(f"Cannot connect to {ldapURI} as {self.domain}\\{self.username}: {e}") return False @@ -872,7 +884,12 @@ def parse_args(): elif options.no_pass: logging.info("Current password not given: will use KRB5CCNAME") else: - oldPassword = getpass("Current password: ") + try: + oldPassword = getpass("Current password: ") + except KeyboardInterrupt: + print() + logging.warning("Cancelled") + sys.exit(130) if options.newhashes is not None: newPassword = "" @@ -887,10 +904,15 @@ def parse_args(): newPwdHashLM = "" newPwdHashNT = "" if options.newpass is None: - newPassword = getpass("New password: ") - if newPassword != getpass("Retype new password: "): - logging.critical("Passwords do not match, try again.") - sys.exit(1) + try: + newPassword = getpass("New password: ") + if newPassword != getpass("Retype new password: "): + logging.critical("Passwords do not match, try again.") + sys.exit(1) + except KeyboardInterrupt: + print() + logging.warning("Cancelled") + sys.exit(130) else: newPassword = options.newpass @@ -949,7 +971,7 @@ def parse_args(): # Attempt the password change/reset if options.reset: - handler.setPassword(targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT) + ret = handler.setPassword(targetUsername, targetDomain, newPassword, newPwdHashLM, newPwdHashNT) else: if (authDomain, authUsername) != (targetDomain, targetUsername): logging.warning( @@ -957,6 +979,11 @@ def parse_args(): "You may want to use '-reset' to *reset* the password of the target." ) - handler.changePassword( + ret = handler.changePassword( targetUsername, targetDomain, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT ) + + if ret: + sys.exit(0) + else: + sys.exit(1)