# SECUREAUTH LABS. Copyright 2018 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. # # Author: Alberto Solino (@agsolino) # # Description: # Transport implementations for the DCE/RPC protocol. # from __future__ import division from __future__ import print_function import binascii import os import re import socket try: from urllib.parse import urlparse, urlunparse except ImportError: from urlparse import urlparse, urlunparse from impacket import ntlm from impacket.dcerpc.v5.rpcrt import DCERPCException, DCERPC_v5, DCERPC_v4 from impacket.dcerpc.v5.rpch import RPCProxyClient, RPCProxyClientException, RPC_OVER_HTTP_v1, RPC_OVER_HTTP_v2 from impacket.smbconnection import SMBConnection class DCERPCStringBinding: parser = re.compile(r'(?:([a-fA-F0-9-]{8}(?:-[a-fA-F0-9-]{4}){3}-[a-fA-F0-9-]{12})@)?' # UUID (opt.) +'([_a-zA-Z0-9]*):' # Protocol Sequence +'([^\[]*)' # Network Address (opt.) +'(?:\[([^\]]*)\])?') # Endpoint and options (opt.) def __init__(self, stringbinding): match = DCERPCStringBinding.parser.match(stringbinding) self.__uuid = match.group(1) self.__ps = match.group(2) self.__na = match.group(3) options = match.group(4) if options: options = options.split(',') self.__endpoint = options[0] try: self.__endpoint.index('endpoint=') self.__endpoint = self.__endpoint[len('endpoint='):] except: pass self.__options = {} for option in options[1:]: vv = option.split('=', 1) self.__options[vv[0]] = vv[1] if len(vv) > 1 else '' else: self.__endpoint = '' self.__options = {} def get_uuid(self): return self.__uuid def get_protocol_sequence(self): return self.__ps def get_network_address(self): return self.__na def set_network_address(self, addr): self.__na = addr def get_endpoint(self): return self.__endpoint def get_options(self): return self.__options def get_option(self, option_name): return self.__options[option_name] def is_option_set(self, option_name): return option_name in self.__options def unset_option(self, option_name): del self.__options[option_name] def __str__(self): return DCERPCStringBindingCompose(self.__uuid, self.__ps, self.__na, self.__endpoint, self.__options) def DCERPCStringBindingCompose(uuid=None, protocol_sequence='', network_address='', endpoint='', options={}): s = '' if uuid: s += uuid + '@' s += protocol_sequence + ':' if network_address: s += network_address if endpoint or options: s += '[' + endpoint if options: s += ',' + ','.join([key if str(val) == '' else "=".join([key, str(val)]) for key, val in options.items()]) s += ']' return s def DCERPCTransportFactory(stringbinding): sb = DCERPCStringBinding(stringbinding) na = sb.get_network_address() ps = sb.get_protocol_sequence() if 'ncadg_ip_udp' == ps: port = sb.get_endpoint() if port: rpctransport = UDPTransport(na, int(port)) else: rpctransport = UDPTransport(na) elif 'ncacn_ip_tcp' == ps: port = sb.get_endpoint() if port: rpctransport = TCPTransport(na, int(port)) else: rpctransport = TCPTransport(na) elif 'ncacn_http' == ps: port = sb.get_endpoint() if port: rpctransport = HTTPTransport(na, int(port)) else: rpctransport = HTTPTransport(na) elif 'ncacn_np' == ps: named_pipe = sb.get_endpoint() if named_pipe: named_pipe = named_pipe[len(r'\pipe'):] rpctransport = SMBTransport(na, filename = named_pipe) else: rpctransport = SMBTransport(na) elif 'ncalocal' == ps: named_pipe = sb.get_endpoint() rpctransport = LOCALTransport(filename = named_pipe) else: raise DCERPCException("Unknown protocol sequence.") rpctransport.set_stringbinding(sb) return rpctransport class DCERPCTransport: DCERPC_class = DCERPC_v5 def __init__(self, remoteName, dstport): self.__remoteName = remoteName self.__remoteHost = remoteName self.__dstport = dstport self._stringbinding = None self._max_send_frag = None self._max_recv_frag = None self._domain = '' self._lmhash = '' self._nthash = '' self.__connect_timeout = None self._doKerberos = False self._username = '' self._password = '' self._domain = '' self._aesKey = None self._TGT = None self._TGS = None self._kdcHost = None self.set_credentials('','') # Strict host validation - off by default and currently only for # SMBTransport self._strict_hostname_validation = False self._validation_allow_absent = True self._accepted_hostname = '' def connect(self): raise RuntimeError('virtual function') def send(self,data=0, forceWriteAndx = 0, forceRecv = 0): raise RuntimeError('virtual function') def recv(self, forceRecv = 0, count = 0): raise RuntimeError('virtual function') def disconnect(self): raise RuntimeError('virtual function') def get_socket(self): raise RuntimeError('virtual function') def get_connect_timeout(self): return self.__connect_timeout def set_connect_timeout(self, timeout): self.__connect_timeout = timeout def getRemoteName(self): return self.__remoteName def setRemoteName(self, remoteName): """This method only makes sense before connection for most protocols.""" self.__remoteName = remoteName def getRemoteHost(self): return self.__remoteHost def setRemoteHost(self, remoteHost): """This method only makes sense before connection for most protocols.""" self.__remoteHost = remoteHost def get_dport(self): return self.__dstport def set_dport(self, dport): """This method only makes sense before connection for most protocols.""" self.__dstport = dport def get_stringbinding(self): return self._stringbinding def set_stringbinding(self, stringbinding): self._stringbinding = stringbinding def get_addr(self): return self.getRemoteHost(), self.get_dport() def set_addr(self, addr): """This method only makes sense before connection for most protocols.""" self.setRemoteHost(addr[0]) self.set_dport(addr[1]) def set_kerberos(self, flag, kdcHost = None): self._doKerberos = flag self._kdcHost = kdcHost def get_kerberos(self): return self._doKerberos def get_kdcHost(self): return self._kdcHost def set_max_fragment_size(self, send_fragment_size): # -1 is default fragment size: 0 (don't fragment) # 0 is don't fragment # other values are max fragment size if send_fragment_size == -1: self.set_default_max_fragment_size() else: self._max_send_frag = send_fragment_size def set_hostname_validation(self, validate, accept_empty, hostname): self._strict_hostname_validation = validate self._validation_allow_absent = accept_empty self._accepted_hostname = hostname def set_default_max_fragment_size(self): # default is 0: don't fragment. # subclasses may override this method self._max_send_frag = 0 def get_credentials(self): return ( self._username, self._password, self._domain, self._lmhash, self._nthash, self._aesKey, self._TGT, self._TGS) def set_credentials(self, username, password, domain='', lmhash='', nthash='', aesKey='', TGT=None, TGS=None): self._username = username self._password = password self._domain = domain self._aesKey = aesKey self._TGT = TGT self._TGS = TGS if lmhash != '' or nthash != '': if len(lmhash) % 2: lmhash = '0%s' % lmhash if len(nthash) % 2: nthash = '0%s' % nthash try: # just in case they were converted already self._lmhash = binascii.unhexlify(lmhash) self._nthash = binascii.unhexlify(nthash) except: self._lmhash = lmhash self._nthash = nthash pass def doesSupportNTLMv2(self): # By default we'll be returning the library's default. Only on SMB Transports we might be able to know it beforehand return ntlm.USE_NTLMv2 def get_dce_rpc(self): return DCERPC_v5(self) class UDPTransport(DCERPCTransport): "Implementation of ncadg_ip_udp protocol sequence" DCERPC_class = DCERPC_v4 def __init__(self, remoteName, dstport = 135): DCERPCTransport.__init__(self, remoteName, dstport) self.__socket = 0 self.set_connect_timeout(30) self.__recv_addr = '' def connect(self): try: af, socktype, proto, canonname, sa = socket.getaddrinfo(self.getRemoteHost(), self.get_dport(), 0, socket.SOCK_DGRAM)[0] self.__socket = socket.socket(af, socktype, proto) self.__socket.settimeout(self.get_connect_timeout()) except socket.error as msg: self.__socket = None raise DCERPCException("Could not connect: %s" % msg) return 1 def disconnect(self): try: self.__socket.close() except socket.error: self.__socket = None return 0 return 1 def send(self,data, forceWriteAndx = 0, forceRecv = 0): self.__socket.sendto(data, (self.getRemoteHost(), self.get_dport())) def recv(self, forceRecv = 0, count = 0): buffer, self.__recv_addr = self.__socket.recvfrom(8192) return buffer def get_recv_addr(self): return self.__recv_addr def get_socket(self): return self.__socket class TCPTransport(DCERPCTransport): """Implementation of ncacn_ip_tcp protocol sequence""" def __init__(self, remoteName, dstport = 135): DCERPCTransport.__init__(self, remoteName, dstport) self.__socket = 0 self.set_connect_timeout(30) def connect(self): af, socktype, proto, canonname, sa = socket.getaddrinfo(self.getRemoteHost(), self.get_dport(), 0, socket.SOCK_STREAM)[0] self.__socket = socket.socket(af, socktype, proto) try: self.__socket.settimeout(self.get_connect_timeout()) self.__socket.connect(sa) except socket.error as msg: self.__socket.close() raise DCERPCException("Could not connect: %s" % msg) return 1 def disconnect(self): try: self.__socket.close() except socket.error: self.__socket = None return 0 return 1 def send(self,data, forceWriteAndx = 0, forceRecv = 0): if self._max_send_frag: offset = 0 while 1: toSend = data[offset:offset+self._max_send_frag] if not toSend: break self.__socket.send(toSend) offset += len(toSend) else: self.__socket.send(data) def recv(self, forceRecv = 0, count = 0): if count: buffer = b'' while len(buffer) < count: buffer += self.__socket.recv(count-len(buffer)) else: buffer = self.__socket.recv(8192) return buffer def get_socket(self): return self.__socket class HTTPTransport(TCPTransport, RPCProxyClient): """Implementation of ncacn_http protocol sequence""" def __init__(self, remoteName=None, dstport=593): self._useRpcProxy = False self._rpcProxyUrl = None self._transport = TCPTransport self._version = RPC_OVER_HTTP_v2 DCERPCTransport.__init__(self, remoteName, dstport) RPCProxyClient.__init__(self, remoteName, dstport) self.set_connect_timeout(30) def set_credentials(self, username, password, domain='', lmhash='', nthash='', aesKey='', TGT=None, TGS=None): return self._transport.set_credentials(self, username, password, domain, lmhash, nthash, aesKey, TGT, TGS) def rpc_proxy_init(self): self._useRpcProxy = True self._transport = RPCProxyClient def set_rpc_proxy_url(self, url): self.rpc_proxy_init() self._rpcProxyUrl = urlparse(url) def get_rpc_proxy_url(self): return urlunparse(self._rpcProxyUrl) def set_stringbinding(self, set_stringbinding): DCERPCTransport.set_stringbinding(self, set_stringbinding) if self._stringbinding.is_option_set("RpcProxy"): self.rpc_proxy_init() rpcproxy = self._stringbinding.get_option("RpcProxy").split(":") if rpcproxy[1] == '443': self.set_rpc_proxy_url('https://%s/rpc/rpcproxy.dll' % rpcproxy[0]) elif rpcproxy[1] == '80': self.set_rpc_proxy_url('http://%s/rpc/rpcproxy.dll' % rpcproxy[0]) else: # 2.1.2.1 # RPC over HTTP always uses port 80 for HTTP traffic and port 443 for HTTPS traffic. # But you can use set_rpc_proxy_url method to set any URL / query you want. raise DCERPCException("RPC Proxy port must be 80 or 443") def connect(self): if self._useRpcProxy == False: # Connecting directly to the ncacn_http port # # Here we using RPC over HTTPv1 instead complex RPC over HTTP v2 syntax # RPC over HTTP v2 here can be implemented in the future self._version = RPC_OVER_HTTP_v1 TCPTransport.connect(self) # Reading legacy server response data = self.get_socket().recv(8192) if data != b'ncacn_http/1.0': raise DCERPCException("%s:%s service is not ncacn_http" % (self.__remoteName, self.__dstport)) else: RPCProxyClient.connect(self) def send(self, data, forceWriteAndx=0, forceRecv=0): return self._transport.send(self, data, forceWriteAndx, forceRecv) def recv(self, forceRecv=0, count=0): return self._transport.recv(self, forceRecv, count) def get_socket(self): if self._useRpcProxy == False: return TCPTransport.get_socket(self) else: raise DCERPCException("This method is not supported for RPC Proxy connections") def disconnect(self): return self._transport.disconnect(self) class SMBTransport(DCERPCTransport): """Implementation of ncacn_np protocol sequence""" def __init__(self, remoteName, dstport=445, filename='', username='', password='', domain='', lmhash='', nthash='', aesKey='', TGT=None, TGS=None, remote_host='', smb_connection=0, doKerberos=False, kdcHost=None): DCERPCTransport.__init__(self, remoteName, dstport) self.__socket = None self.__tid = 0 self.__filename = filename self.__handle = 0 self.__pending_recv = 0 self.set_credentials(username, password, domain, lmhash, nthash, aesKey, TGT, TGS) self._doKerberos = doKerberos self._kdcHost = kdcHost if remote_host != '': self.setRemoteHost(remote_host) if smb_connection == 0: self.__existing_smb = False else: self.__existing_smb = True self.set_credentials(*smb_connection.getCredentials()) self.__prefDialect = None self.__smb_connection = smb_connection self.set_connect_timeout(30) def preferred_dialect(self, dialect): self.__prefDialect = dialect def setup_smb_connection(self): if not self.__smb_connection: self.__smb_connection = SMBConnection(self.getRemoteName(), self.getRemoteHost(), sess_port=self.get_dport(), preferredDialect=self.__prefDialect, timeout=self.get_connect_timeout()) if self._strict_hostname_validation: self.__smb_connection.setHostnameValidation(self._strict_hostname_validation, self._validation_allow_absent, self._accepted_hostname) def connect(self): # Check if we have a smb connection already setup if self.__smb_connection == 0: self.setup_smb_connection() if self._doKerberos is False: self.__smb_connection.login(self._username, self._password, self._domain, self._lmhash, self._nthash) else: self.__smb_connection.kerberosLogin(self._username, self._password, self._domain, self._lmhash, self._nthash, self._aesKey, kdcHost=self._kdcHost, TGT=self._TGT, TGS=self._TGS) self.__tid = self.__smb_connection.connectTree('IPC$') self.__handle = self.__smb_connection.openFile(self.__tid, self.__filename) self.__socket = self.__smb_connection.getSMBServer().get_socket() return 1 def disconnect(self): self.__smb_connection.disconnectTree(self.__tid) # If we created the SMB connection, we close it, otherwise # that's up for the caller if self.__existing_smb is False: self.__smb_connection.logoff() self.__smb_connection.close() self.__smb_connection = 0 def send(self,data, forceWriteAndx = 0, forceRecv = 0): if self._max_send_frag: offset = 0 while 1: toSend = data[offset:offset+self._max_send_frag] if not toSend: break self.__smb_connection.writeFile(self.__tid, self.__handle, toSend, offset = offset) offset += len(toSend) else: self.__smb_connection.writeFile(self.__tid, self.__handle, data) if forceRecv: self.__pending_recv += 1 def recv(self, forceRecv = 0, count = 0 ): if self._max_send_frag or self.__pending_recv: # _max_send_frag is checked because it's the same condition we checked # to decide whether to use write_andx() or send_trans() in send() above. if self.__pending_recv: self.__pending_recv -= 1 return self.__smb_connection.readFile(self.__tid, self.__handle, bytesToRead = self._max_recv_frag) else: return self.__smb_connection.readFile(self.__tid, self.__handle) def get_smb_connection(self): return self.__smb_connection def set_smb_connection(self, smb_connection): self.__smb_connection = smb_connection self.set_credentials(*smb_connection.getCredentials()) self.__existing_smb = True def get_smb_server(self): # Raw Access to the SMBServer (whatever type it is) return self.__smb_connection.getSMBServer() def get_socket(self): return self.__socket def doesSupportNTLMv2(self): return self.__smb_connection.doesSupportNTLMv2() class LOCALTransport(DCERPCTransport): """ Implementation of ncalocal protocol sequence, not the same as ncalrpc (I'm not doing LPC just opening the local pipe) """ def __init__(self, filename = ''): DCERPCTransport.__init__(self, '', 0) self.__filename = filename self.__handle = 0 def connect(self): if self.__filename.upper().find('PIPE') < 0: self.__filename = '\\PIPE\\%s' % self.__filename self.__handle = os.open('\\\\.\\%s' % self.__filename, os.O_RDWR|os.O_BINARY) return 1 def disconnect(self): os.close(self.__handle) def send(self,data, forceWriteAndx = 0, forceRecv = 0): os.write(self.__handle, data) def recv(self, forceRecv = 0, count = 0 ): data = os.read(self.__handle, 65535) return data