Class: RubySMB::Dcerpc::Client

Inherits:
Object
  • Object
show all
Includes:
RubySMB::Dcerpc, Epm, PeerInfo
Defined in:
lib/ruby_smb/dcerpc/client.rb

Overview

Represents DCERPC SMB client capable of talking to an RPC endpoint in stand-alone.

Constant Summary collapse

MAX_BUFFER_SIZE =

The default maximum size of a RPC message that the Client accepts (in bytes)

64512
READ_TIMEOUT =

The read timeout when receiving packets.

30
ENDPOINT_MAPPER_PORT =

The default Endpoint Mapper port

135

Constants included from Epm

Epm::EPT_MAP, Epm::UUID, Epm::VER_MAJOR, Epm::VER_MINOR

Constants included from RubySMB::Dcerpc

DCE_C_AUTHZ_DCE, DCE_C_AUTHZ_NAME, MAX_RECV_FRAG, MAX_XMIT_FRAG, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_AUTHN_LEVEL_NONE, RPC_C_AUTHN_LEVEL_PKT, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_NONE, RPC_C_AUTHN_WINNT

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from PeerInfo

#extract_os_version, #store_target_info

Methods included from Epm

#get_host_port_from_ept_mapper

Methods included from RubySMB::Dcerpc

#add_auth_verifier, #auth_provider_complete_handshake, #auth_provider_decrypt_and_verify, #auth_provider_encrypt_and_sign, #auth_provider_init, #bind, #force_set_auth_params, #get_auth_padding_length, #get_response_full_stub, #handle_integrity_privacy, #set_decrypted_packet, #set_encrypted_packet, #set_integrity_privacy, #set_signature_on_packet

Constructor Details

#initialize(host, endpoint, tcp_socket: nil, read_timeout: READ_TIMEOUT, username: '', password: '', domain: '.', local_workstation: 'WORKSTATION', ntlm_flags: NTLM::DEFAULT_CLIENT_FLAGS) ⇒ Client

Returns a new instance of Client.

Parameters:

  • host (String)

    The remote host

  • endpoint (Module)

    A module endpoint that defines UUID, VER_MAJOR and VER_MINOR constants (e.g. Drsr)

  • tcp_socket (TcpSocket) (defaults to: nil)

    The socket to use. If not provided, a new socket will be created when calling #connect

  • read_timeout (Integer) (defaults to: READ_TIMEOUT)

    The read timeout value to use

  • username (String) (defaults to: '')

    The username to authenticate with, if needed

  • password (String) (defaults to: '')

    The password to authenticate with, if needed. Note that a NTLM hash can be used instead of a password.

  • domain (String) (defaults to: '.')

    The domain to authenticate to, if needed

  • local_workstation (String) (defaults to: 'WORKSTATION')

    The workstation name to authenticate to, if needed

  • ntlm_flags (Integer) (defaults to: NTLM::DEFAULT_CLIENT_FLAGS)

    The flags to pass to the Net:NTLM client



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ruby_smb/dcerpc/client.rb', line 103

def initialize(host,
               endpoint,
               tcp_socket: nil,
               read_timeout: READ_TIMEOUT,
               username: '',
               password: '',
               domain: '.',
               local_workstation: 'WORKSTATION',
               ntlm_flags: NTLM::DEFAULT_CLIENT_FLAGS)

  @endpoint = endpoint
  extend @endpoint

  @host              = host
  @tcp_socket        = tcp_socket
  @read_timeout      = read_timeout
  @domain            = domain
  @local_workstation = local_workstation
  @username          = username
  @password          = password
  @max_buffer_size   = MAX_BUFFER_SIZE
  @call_id           = 1
  @ctx_id            = 0
  @auth_ctx_id_base  = rand(0xFFFFFFFF)

  unless username.empty? && password.empty?
    @ntlm_client = RubySMB::NTLM::Client.new(
      @username,
      @password,
      workstation: @local_workstation,
      domain: @domain,
      flags: ntlm_flags
    )
  end
end

Instance Attribute Details

#default_domainString

Returns:

  • (String)


57
58
59
# File 'lib/ruby_smb/dcerpc/client.rb', line 57

def default_domain
  @default_domain
end

#default_nameString

Returns:

  • (String)


52
53
54
# File 'lib/ruby_smb/dcerpc/client.rb', line 52

def default_name
  @default_name
end

#dns_domain_nameString

Returns:

  • (String)


67
68
69
# File 'lib/ruby_smb/dcerpc/client.rb', line 67

def dns_domain_name
  @dns_domain_name
end

#dns_host_nameString

Returns:

  • (String)


62
63
64
# File 'lib/ruby_smb/dcerpc/client.rb', line 62

def dns_host_name
  @dns_host_name
end

#dns_tree_nameString

Returns:

  • (String)


72
73
74
# File 'lib/ruby_smb/dcerpc/client.rb', line 72

def dns_tree_name
  @dns_tree_name
end

#domainString

Returns:

  • (String)


27
28
29
# File 'lib/ruby_smb/dcerpc/client.rb', line 27

def domain
  @domain
end

#local_workstationString

Returns:

  • (String)


32
33
34
# File 'lib/ruby_smb/dcerpc/client.rb', line 32

def local_workstation
  @local_workstation
end

#max_buffer_sizeInteger

Returns:

  • (Integer)


83
84
85
# File 'lib/ruby_smb/dcerpc/client.rb', line 83

def max_buffer_size
  @max_buffer_size
end

#ntlm_clientString

Returns:

  • (String)


37
38
39
# File 'lib/ruby_smb/dcerpc/client.rb', line 37

def ntlm_client
  @ntlm_client
end

#os_versionString

Returns:

  • (String)


77
78
79
# File 'lib/ruby_smb/dcerpc/client.rb', line 77

def os_version
  @os_version
end

#passwordString

Returns:

  • (String)


47
48
49
# File 'lib/ruby_smb/dcerpc/client.rb', line 47

def password
  @password
end

#tcp_socketTcpSocket

Returns:

  • (TcpSocket)


88
89
90
# File 'lib/ruby_smb/dcerpc/client.rb', line 88

def tcp_socket
  @tcp_socket
end

#usernameString

Returns:

  • (String)


42
43
44
# File 'lib/ruby_smb/dcerpc/client.rb', line 42

def username
  @username
end

Instance Method Details

#closeObject

Close the TCP Socket



174
175
176
# File 'lib/ruby_smb/dcerpc/client.rb', line 174

def close
  @tcp_socket.close if @tcp_socket && !@tcp_socket.closed?
end

#connect(port: nil) ⇒ TcpSocket

Connect to the RPC endpoint. If a TCP socket was not provided, it takes care of asking the Endpoint Mapper Interface the port used by the given endpoint provided in #initialize and connect a TCP socket

Parameters:

  • port (Integer) (defaults to: nil)

    An optional port number to connect to. If provided, it will not ask the Endpoint Mapper Interface for a port number.

Returns:

  • (TcpSocket)

    The connected TCP socket



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ruby_smb/dcerpc/client.rb', line 147

def connect(port: nil)
  return if @tcp_socket
  unless port
    @tcp_socket = TCPSocket.new(@host, ENDPOINT_MAPPER_PORT)
    bind(endpoint: Epm)
    begin
      host_port = get_host_port_from_ept_mapper(
        uuid: @endpoint::UUID,
        maj_ver: @endpoint::VER_MAJOR,
        min_ver: @endpoint::VER_MINOR
      )
    rescue RubySMB::Dcerpc::Error::DcerpcError => e
      e.message.prepend(
        "Cannot resolve the remote port number for endpoint #{@endpoint::UUID}. "\
        "Set @tcp_socket parameter to specify the service port number and bypass "\
        "EPM port resolution. Error: "
      )
      raise e
    end
    port = host_port[:port]
    @tcp_socket.close
    @tcp_socket = nil
  end
  @tcp_socket = TCPSocket.new(@host, port)
end

#dcerpc_request(stub_packet, auth_level: nil, auth_type: nil) ⇒ Object

Send a DCERPC request with the provided stub packet.

Parameters:

  • stub_packet (BinData::Record)

    the stub packet to be sent as part of a Request packet

  • opts (Hash)

    the authenticaiton options: :auth_type and :auth_level

Raises:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/ruby_smb/dcerpc/client.rb', line 192

def dcerpc_request(stub_packet, auth_level: nil, auth_type: nil)
  stub_class = stub_packet.class.name.split('::')
  #opts.merge!(endpoint: stub_class[-2])
  values = {
    opnum: stub_packet.opnum,
    p_cont_id: @ctx_id
  }
  dcerpc_req = Request.new(values, { endpoint: stub_class[-2] })
  dcerpc_req.pdu_header.call_id = @call_id
  dcerpc_req.stub.read(stub_packet.to_binary_s)
  # TODO: handle fragmentation
  # We should fragment PDUs if:
  # 1) Payload exceeds max_xmit_frag (@max_buffer_size) received during BIND response
  # 2) We'e explicitly fragmenting packets with lower values

  if auth_level &&
     [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
    set_integrity_privacy(dcerpc_req, auth_level: auth_level, auth_type: auth_type)
    # Per the spec (MS_RPCE 2.2.2.11): start of the trailer should be a multiple of 16 bytes offset from the start of the stub
    valid_offset = (((dcerpc_req.sec_trailer.abs_offset - dcerpc_req.stub.abs_offset) % 16))
    valid_auth_pad = (dcerpc_req.sec_trailer.auth_pad_length == dcerpc_req.auth_pad.length)
    raise Error::InvalidPacket unless valid_offset == 0 && valid_auth_pad
  end

  send_packet(dcerpc_req)

  dcerpc_res = recv_struct(Response)
  unless dcerpc_res.pdu_header.pfc_flags.first_frag == 1
    raise Error::InvalidPacket, "Not the first fragment"
  end

  if auth_level &&
     [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
    handle_integrity_privacy(dcerpc_res, auth_level: auth_level, auth_type: auth_type)
  end

  raw_stub = dcerpc_res.stub.to_binary_s
  loop do
    break if dcerpc_res.pdu_header.pfc_flags.last_frag == 1
    dcerpc_res = recv_struct(Response)

    if auth_level &&
       [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
      handle_integrity_privacy(dcerpc_res, auth_level: auth_level, auth_type: auth_type)
    end

    raw_stub << dcerpc_res.stub.to_binary_s
  end

  raw_stub
end

#process_ntlm_type2(type2_message) ⇒ Object



178
179
180
181
182
183
184
# File 'lib/ruby_smb/dcerpc/client.rb', line 178

def process_ntlm_type2(type2_message)
  auth3 = super
  challenge_message = @ntlm_client.session.challenge_message
  store_target_info(challenge_message.target_info) if challenge_message.has_flag?(:TARGET_INFO)
  @os_version = extract_os_version(challenge_message.os_version.to_s) unless challenge_message.os_version.empty?
  auth3
end

#recv_struct(struct) ⇒ Object

Receive a packet from the remote host and parse it according to struct

Parameters:

  • struct (Class)

    the structure class to parse the response with

Raises:



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/ruby_smb/dcerpc/client.rb', line 268

def recv_struct(struct)
  raise Error::CommunicationError, 'Connection has already been closed' if @tcp_socket.closed?
  if IO.select([@tcp_socket], nil, nil, @read_timeout).nil?
    raise Error::CommunicationError, "Read timeout expired when reading from the Socket (timeout=#{@read_timeout})"
  end

  begin
    response = struct.read(@tcp_socket)
  rescue IOError
    raise Error::InvalidPacket, "Error reading the #{struct} response"
  end
  unless response.pdu_header.ptype == struct::PTYPE
    raise Error::InvalidPacket, "Not a #{struct} packet"
  end

  response
rescue Errno::EINVAL, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE => e
  raise Error::CommunicationError, "An error occurred reading from the Socket: #{e.message}"
end

#send_packet(packet) ⇒ Object

Send a packet to the remote host

Parameters:

  • packet (BinData::Record)

    the packet to send

Raises:



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/ruby_smb/dcerpc/client.rb', line 248

def send_packet(packet)
  data = packet.to_binary_s
  bytes_written = 0
  begin
    loop do
      break unless bytes_written < data.size
      retval = @tcp_socket.write(data[bytes_written..-1])
      bytes_written += retval
    end

  rescue IOError, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE => e
    raise Error::CommunicationError, "An error occurred writing to the Socket: #{e.message}"
  end
  nil
end