Class: RubySMB::Dcerpc::Client
- Inherits:
-
Object
- Object
- RubySMB::Dcerpc::Client
- Includes:
- Epm
- 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
Instance Attribute Summary collapse
- #default_domain ⇒ String
- #default_name ⇒ String
- #dns_domain_name ⇒ String
- #dns_host_name ⇒ String
- #dns_tree_name ⇒ String
- #domain ⇒ String
- #local_workstation ⇒ String
- #max_buffer_size ⇒ Integer
- #ntlm_client ⇒ String
- #os_version ⇒ String
- #password ⇒ String
- #tcp_socket ⇒ TcpSocket
- #username ⇒ String
Instance Method Summary collapse
-
#add_auth_verifier(req, auth, auth_type, auth_level) ⇒ Object
Add the authentication verifier to the packet.
-
#bind(endpoint: @endpoint, auth_level: RPC_C_AUTHN_LEVEL_NONE, auth_type: nil) ⇒ BindAck
Bind to the remote server interface endpoint.
-
#close ⇒ Object
Close the TCP Socket.
-
#connect(port: nil) ⇒ TcpSocket
Connect to the RPC endpoint.
-
#dcerpc_request(stub_packet, auth_level: nil, auth_type: nil) ⇒ Object
Send a DCERPC request with the provided stub packet.
-
#extract_os_version(version) ⇒ String
Extract the peer/server version number from the NTLM Type 2 (challenge) Version field.
-
#handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false) ⇒ Object
Process the security context received in a response.
-
#initialize(host, endpoint, tcp_socket: nil, read_timeout: READ_TIMEOUT, username: '', password: '', domain: '.', local_workstation: 'WORKSTATION', ntlm_flags: NTLM::DEFAULT_CLIENT_FLAGS) ⇒ Client
constructor
A new instance of Client.
- #process_ntlm_type2(type2_message) ⇒ Object
-
#recv_struct(struct) ⇒ Object
Receive a packet from the remote host and parse it according to
struct
. -
#send_auth3(response, auth_type, auth_level) ⇒ Object
Send a rpc_auth3 PDU that ends the authentication handshake.
-
#send_packet(packet) ⇒ Object
Send a packet to the remote host.
-
#set_integrity_privacy(dcerpc_req, auth_level:, auth_type:) ⇒ Object
Add the authentication verifier to a Request packet.
-
#store_target_info(target_info_str) ⇒ Object
Extract and store useful information about the peer/server from the NTLM Type 2 (challenge) TargetInfo fields.
Methods included from Epm
#get_host_port_from_ept_mapper
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.
100 101 102 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 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 100 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.encode('utf-8') @password = password.encode('utf-8') @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 = Net::NTLM::Client.new( @username, @password, workstation: @local_workstation, domain: @domain, flags: ntlm_flags ) end end |
Instance Attribute Details
#default_domain ⇒ String
54 55 56 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 54 def default_domain @default_domain end |
#default_name ⇒ String
49 50 51 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 49 def default_name @default_name end |
#dns_domain_name ⇒ String
64 65 66 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 64 def dns_domain_name @dns_domain_name end |
#dns_host_name ⇒ String
59 60 61 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 59 def dns_host_name @dns_host_name end |
#dns_tree_name ⇒ String
69 70 71 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 69 def dns_tree_name @dns_tree_name end |
#domain ⇒ String
24 25 26 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 24 def domain @domain end |
#local_workstation ⇒ String
29 30 31 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 29 def local_workstation @local_workstation end |
#max_buffer_size ⇒ Integer
80 81 82 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 80 def max_buffer_size @max_buffer_size end |
#ntlm_client ⇒ String
34 35 36 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 34 def ntlm_client @ntlm_client end |
#os_version ⇒ String
74 75 76 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 74 def os_version @os_version end |
#password ⇒ String
44 45 46 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 44 def password @password end |
#tcp_socket ⇒ TcpSocket
85 86 87 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 85 def tcp_socket @tcp_socket end |
#username ⇒ String
39 40 41 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 39 def username @username end |
Instance Method Details
#add_auth_verifier(req, auth, auth_type, auth_level) ⇒ Object
Add the authentication verifier to the packet. This includes a sec trailer and the actual authentication data.
182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 182 def add_auth_verifier(req, auth, auth_type, auth_level) req.sec_trailer = { auth_type: auth_type, auth_level: auth_level, auth_context_id: @ctx_id + @auth_ctx_id_base } req.auth_value = auth req.pdu_header.auth_length = auth.length nil end |
#bind(endpoint: @endpoint, auth_level: RPC_C_AUTHN_LEVEL_NONE, auth_type: nil) ⇒ BindAck
Bind to the remote server interface endpoint. It takes care of adding
the necessary authentication verifier if :auth_level
is set to
anything different than RPC_C_AUTHN_LEVEL_NONE
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 251 def bind(endpoint: @endpoint, auth_level: RPC_C_AUTHN_LEVEL_NONE, auth_type: nil) bind_req = Bind.new(endpoint: endpoint) bind_req.pdu_header.call_id = @call_id # TODO: evasion: generate random UUIDs for bogus binds if auth_level && auth_level != RPC_C_AUTHN_LEVEL_NONE case auth_type when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client = @ntlm_client.init_context auth = .serialize when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL # TODO raise NotImplementedError else raise ArgumentError, "Unsupported Auth Type: #{auth_type}" end add_auth_verifier(bind_req, auth, auth_type, auth_level) end send_packet(bind_req) bindack_response = recv_struct(BindAck) # TODO: see if BindNack response should be handled too res_list = bindack_response.p_result_list if res_list.n_results == 0 || res_list.p_results[0].result != BindAck::ACCEPTANCE raise Error::BindError, "Bind Failed (Result: #{res_list.p_results[0].result}, Reason: #{res_list.p_results[0].reason})" end @max_buffer_size = bindack_response.max_xmit_frag @call_id = bindack_response.pdu_header.call_id if auth_level && auth_level != RPC_C_AUTHN_LEVEL_NONE # The number of legs needed to build the security context is defined # by the security provider # (see [2.2.1.1.7 Security Providers](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36)) case auth_type when RPC_C_AUTHN_WINNT send_auth3(bindack_response, auth_type, auth_level) when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE # TODO raise NotImplementedError end end nil end |
#close ⇒ Object
Close the TCP Socket
171 172 173 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 171 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 Enpoint Mapper Interface the port used by the given endpoint provided in #initialize and connect a TCP socket
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 144 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..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.
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 393 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) 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 |
#extract_os_version(version) ⇒ String
Extract the peer/server version number from the NTLM Type 2 (challenge) Version field.
328 329 330 331 332 333 334 335 336 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 328 def extract_os_version(version) #version.unpack('CCS').join('.') begin os_version = NTLM::OSVersion.read(version) rescue IOError return '' end return "#{os_version.major}.#{os_version.minor}.#{os_version.build}" end |
#handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false) ⇒ Object
Process the security context received in a response. It decrypts the
encrypted stub if :auth_level
is set to anything different than
RPC_C_AUTHN_LEVEL_PKT_PRIVACY. It also checks the packet signature and
raises an InvalidPacket error if it fails. Note that the exception is
disabled by default and can be enabled with the
:raise_signature_error
option
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 499 def handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false) decrypted_stub = '' if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY encrypted_stub = dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s case auth_type when RPC_C_AUTHN_NONE when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT decrypted_stub = @ntlm_client.session.(encrypted_stub) when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS # TODO raise NotImplementedError else raise ArgumentError, "Unsupported Auth Type: #{auth_type}" end end unless decrypted_stub.empty? pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)] dcerpc_response.auth_pad = decrypted_stub[-(pad_length)..-1] end signature = dcerpc_response.auth_value data_to_check = dcerpc_response.stub.to_binary_s if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0 data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)] end unless @ntlm_client.session.verify_signature(signature, data_to_check) if raise_signature_error raise Error::InvalidPacket.new( "Wrong packet signature received (set `raise_signature_error` to false to ignore)" ) end end @call_id += 1 nil end |
#process_ntlm_type2(type2_message) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 194 def process_ntlm_type2() ntlmssp_offset = .index('NTLMSSP') type2_blob = .slice(ntlmssp_offset..-1) = [type2_blob].pack('m') = @ntlm_client.init_context() auth3 = .serialize @session_key = @ntlm_client.session_key = @ntlm_client.session. store_target_info(.target_info) if .has_flag?(:TARGET_INFO) @os_version = extract_os_version(.os_version.to_s) unless .os_version.empty? auth3 end |
#recv_struct(struct) ⇒ Object
Receive a packet from the remote host and parse it according to struct
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 465 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.}" end |
#send_auth3(response, auth_type, auth_level) ⇒ Object
Send a rpc_auth3 PDU that ends the authentication handshake.
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 215 def send_auth3(response, auth_type, auth_level) case auth_type when RPC_C_AUTHN_NONE when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT auth3 = process_ntlm_type2(response.auth_value) when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS # TODO raise NotImplementedError else raise ArgumentError, "Unsupported Auth Type: #{auth_type}" end rpc_auth3 = RpcAuth3.new add_auth_verifier(rpc_auth3, auth3, auth_type, auth_level) rpc_auth3.pdu_header.call_id = @call_id # The server should not respond send_packet(rpc_auth3) @call_id += 1 nil end |
#send_packet(packet) ⇒ Object
Send a packet to the remote host
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 445 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.}" end nil end |
#set_integrity_privacy(dcerpc_req, auth_level:, auth_type:) ⇒ Object
Add the authentication verifier to a Request packet. This includes a
sec trailer and the signature of the packet. This also encrypts the
Request stub if privacy is required (:auth_level
option is
RPC_C_AUTHN_LEVEL_PKT_PRIVACY).
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 347 def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:) dcerpc_req.sec_trailer = { auth_type: auth_type, auth_level: auth_level, auth_context_id: @ctx_id + @auth_ctx_id_base } dcerpc_req.auth_value = ' ' * 16 dcerpc_req.pdu_header.auth_length = 16 data_to_sign = plain_stub = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0 data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)] end encrypted_stub = '' if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY case auth_type when RPC_C_AUTHN_NONE when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT encrypted_stub = @ntlm_client.session.(plain_stub) when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS # TODO raise NotImplementedError else raise ArgumentError, "Unsupported Auth Type: #{auth_type}" end end signature = @ntlm_client.session.(data_to_sign) unless encrypted_stub.empty? pad_length = dcerpc_req.sec_trailer.auth_pad_length.to_i dcerpc_req.enable_encrypted_stub dcerpc_req.stub = encrypted_stub[0..-(pad_length + 1)] dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1] end dcerpc_req.auth_value = signature dcerpc_req.pdu_header.auth_length = signature.size end |
#store_target_info(target_info_str) ⇒ Object
Extract and store useful information about the peer/server from the NTLM Type 2 (challenge) TargetInfo fields.
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/ruby_smb/dcerpc/client.rb', line 306 def store_target_info(target_info_str) target_info = Net::NTLM::TargetInfo.new(target_info_str) { Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => :@default_name, Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => :@default_domain, Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => :@dns_host_name, Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => :@dns_domain_name, Net::NTLM::TargetInfo::MSV_AV_DNS_TREE_NAME => :@dns_tree_name }.each do |constant, attribute| if target_info.av_pairs[constant] value = target_info.av_pairs[constant].dup value.force_encoding('UTF-16LE') instance_variable_set(attribute, value.encode('UTF-8')) end end end |