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/2] 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 9f69a71be6..db24d09a4d 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 0000000000..aaaba3ee66 --- /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 0000000000..545281d8b3 --- /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/2] 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 db24d09a4d..63536bae23 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 aaaba3ee66..36aaa2ccbd 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('