# SECUREAUTH LABS. Copyright 2020 SecureAuth Corporation. All rights reserved. # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: # Initial [MS-RCPH] Interface implementation # # Authors: # Arseniy Sharoglazov / Positive Technologies (https://www.ptsecurity.com/) # import re import binascii from struct import unpack from impacket import uuid, ntlm, system_errors, nt_errors, LOG from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.uuid import EMPTY_UUID from impacket.http import HTTPClientSecurityProvider, AUTH_BASIC from impacket.structure import Structure from impacket.dcerpc.v5.rpcrt import MSRPCHeader, \ MSRPC_RTS, PFC_FIRST_FRAG, PFC_LAST_FRAG class RPCProxyClientException(DCERPCException): parser = re.compile(r'RPC Error: ([a-fA-F0-9]{1,8})') def __init__(self, error_string=None, proxy_error=None): rpc_error_code = None if proxy_error is not None: try: search = self.parser.search(proxy_error) rpc_error_code = int(search.group(1), 16) except: error_string += ': ' + proxy_error DCERPCException.__init__(self, error_string, rpc_error_code) def __str__(self): if self.error_code is not None: key = self.error_code if key in system_errors.ERROR_MESSAGES: error_msg_short = system_errors.ERROR_MESSAGES[key][0] return '%s, code: 0x%x - %s' % (self.error_string, self.error_code, error_msg_short) elif key in nt_errors.ERROR_MESSAGES: error_msg_short = nt_errors.ERROR_MESSAGES[key][0] return '%s, code: 0x%x - %s' % (self.error_string, self.error_code, error_msg_short) else: return '%s: unknown code: 0x%x' % (self.error_string, self.error_code) else: return self.error_string ################################################################################ # CONSTANTS ################################################################################ RPC_OVER_HTTP_v1 = 1 RPC_OVER_HTTP_v2 = 2 # Errors which might need handling # RPCProxyClient internal errors RPC_PROXY_REMOTE_NAME_NEEDED_ERR = 'Basic authentication in RPC proxy is used, ' \ 'so coudn\'t obtain a target NetBIOS name from NTLMSSP to connect.' # Errors below contain a part of server responses RPC_PROXY_INVALID_RPC_PORT_ERR = 'Invalid RPC Port' RPC_PROXY_CONN_A1_0X6BA_ERR = 'RPC Proxy CONN/A1 request failed, code: 0x6ba' RPC_PROXY_CONN_A1_404_ERR = 'CONN/A1 request failed: HTTP/1.1 404 Not Found' RPC_PROXY_RPC_OUT_DATA_404_ERR = 'RPC_OUT_DATA channel: HTTP/1.1 404 Not Found' RPC_PROXY_CONN_A1_401_ERR = 'CONN/A1 request failed: HTTP/1.1 401 Unauthorized' RPC_PROXY_HTTP_IN_DATA_401_ERR = 'RPC_IN_DATA channel: HTTP/1.1 401 Unauthorized' # 2.2.3.3 Forward Destinations FDClient = 0x00000000 FDInProxy = 0x00000001 FDServer = 0x00000002 FDOutProxy = 0x00000003 RTS_FLAG_NONE = 0x0000 RTS_FLAG_PING = 0x0001 RTS_FLAG_OTHER_CMD = 0x0002 RTS_FLAG_RECYCLE_CHANNEL = 0x0004 RTS_FLAG_IN_CHANNEL = 0x0008 RTS_FLAG_OUT_CHANNEL = 0x0010 RTS_FLAG_EOF = 0x0020 RTS_FLAG_ECHO = 0x0040 # 2.2.3.5 RTS Commands RTS_CMD_RECEIVE_WINDOW_SIZE = 0x00000000 RTS_CMD_FLOW_CONTROL_ACK = 0x00000001 RTS_CMD_CONNECTION_TIMEOUT = 0x00000002 RTS_CMD_COOKIE = 0x00000003 RTS_CMD_CHANNEL_LIFETIME = 0x00000004 RTS_CMD_CLIENT_KEEPALIVE = 0x00000005 RTS_CMD_VERSION = 0x00000006 RTS_CMD_EMPTY = 0x00000007 RTS_CMD_PADDING = 0x00000008 RTS_CMD_NEGATIVE_ANCE = 0x00000009 RTS_CMD_ANCE = 0x0000000A RTS_CMD_CLIENT_ADDRESS = 0x0000000B RTS_CMD_ASSOCIATION_GROUP_ID = 0x0000000C RTS_CMD_DESTINATION = 0x0000000D RTS_CMD_PING_TRAFFIC_SENT_NOTIFY = 0x0000000E ################################################################################ # STRUCTURES ################################################################################ # 2.2.3.1 RTS Cookie class RTSCookie(Structure): structure = ( ('Cookie','16s=b"\\x00"*16'), ) # 2.2.3.2 Client Address class EncodedClientAddress(Structure): structure = ( ('AddressType',' 0: buffer = self.__readBuffer self.__readBuffer = b'' else: # Let's read RECV_SIZE bytes and not amt bytes. # We would need to check the answer for HTTP errors, as # they can just appear in the middle of the stream. buffer = sock.recv(self.RECV_SIZE) self.check_http_error(buffer) if len(buffer) <= amt: return buffer # We received more than we need self.__readBuffer = buffer[amt:] return buffer[:amt] # Check if the previous chunk is still there if self.__chunkLeft > 0: # If the previous chunk is still there, # just give the caller what we already have if amt >= self.__chunkLeft: buffer = self.__readBuffer[:self.__chunkLeft] # We may have recieved a part of a new chunk self.__readBuffer = self.__readBuffer[self.__chunkLeft + 2:] self.__chunkLeft = 0 return buffer else: buffer = self.__readBuffer[:amt] self.__readBuffer = self.__readBuffer[amt:] self.__chunkLeft -= amt return buffer # Let's start to process a new chunk buffer = self.__readBuffer self.__readBuffer = b'' self.check_http_error(buffer) # Let's receive a chunk size field which ends with CRLF # For Microsoft TMG 2010 it can cause more than one read while buffer.find(b'\r\n') == -1: buffer += sock.recv(self.RECV_SIZE) self.check_http_error(buffer) chunksize = int(buffer[:buffer.find(b'\r\n')], 16) buffer = buffer[buffer.find(b'\r\n') + 2:] # Let's read at least our chunk including final CRLF while len(buffer) - 2 < chunksize: buffer += sock.recv(chunksize - len(buffer) + 2) # We should not be using any information from # the TCP level to determine HTTP boundaries. # So, we may have received more than we need. if len(buffer) - 2 > chunksize: self.__readBuffer = buffer[chunksize + 2:] buffer = buffer[:chunksize + 2] # Checking the amt if len(buffer) - 2 > amt: self.__chunkLeft = chunksize - amt # We may have recieved a part of a new chunk before, # so the concatenation is crucual self.__readBuffer = buffer[amt:] + self.__readBuffer return buffer[:amt] else: # Removing CRLF return buffer[:-2] def send(self, data, forceWriteAndx=0, forceRecv=0): # We don't use chunked encoding for IN channel as # Microsoft software is developed this way. # If you do this, it may fail. self.get_socket_in().send(data) def rpc_out_read_pkt(self, handle_rts=False): while True: response_data = b'' # Let's receive common RPC header and no more # # C706 # 12.4 Common Fields # Header encodings differ between connectionless and connection-oriented PDUs. # However, certain fields use common sets of values with a consistent # interpretation across the two protocols. # # This MUST recv MSRPCHeader._SIZE bytes, and not MSRPCRespHeader._SIZE bytes! # while len(response_data) < MSRPCHeader._SIZE: response_data += self.rpc_out_recv1(MSRPCHeader._SIZE - len(response_data)) response_header = MSRPCHeader(response_data) # frag_len contains the full length of the packet for both # MSRPC and RTS frag_len = response_header['frag_len'] # Receiving the full pkt and no more while len(response_data) < frag_len: response_data += self.rpc_out_recv1(frag_len - len(response_data)) # We need to do the Flow Control procedures # # 3.2.1.1.4 # This protocol specifies that only RPC PDUs are subject to the flow control abstract data # model. RTS PDUs and the HTTP request and response headers are not subject to flow control. if response_header['type'] != MSRPC_RTS: self.flow_control(frag_len) if handle_rts is True and response_header['type'] == MSRPC_RTS: self.handle_out_of_sequence_rts(response_data) else: return response_data def recv(self, forceRecv=0, count=0): return self.rpc_out_read_pkt(handle_rts=True) def handle_out_of_sequence_rts(self, response_data): packet = RTSHeader(response_data) #print("=========== RTS PKT ===========") #print("RAW: %s" % binascii.hexlify(response_data)) #packet.dump() # #pduData = packet['pduData'] #numberOfCommands = packet['NumberOfCommands'] # #server_cmds = [] #while numberOfCommands > 0: # numberOfCommands -= 1 # # cmd_type = unpack('