From b95cd7e5d7cff53dcc49951b15d2c18e0ec8b8b0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 8 Apr 2025 15:18:20 -0400 Subject: [PATCH 1/5] Encapsulate the EPM functionality for reuse --- lib/ruby_smb/dcerpc.rb | 3 ++ lib/ruby_smb/dcerpc/client.rb | 39 +++++++++--------- lib/ruby_smb/dcerpc/epm.rb | 74 ++++++++++++++++++----------------- 3 files changed, 61 insertions(+), 55 deletions(-) diff --git a/lib/ruby_smb/dcerpc.rb b/lib/ruby_smb/dcerpc.rb index 682abcda7..439cf2412 100644 --- a/lib/ruby_smb/dcerpc.rb +++ b/lib/ruby_smb/dcerpc.rb @@ -28,6 +28,7 @@ module Dcerpc DCE_C_AUTHZ_DCE = 2 require 'windows_error/win32' + require 'ruby_smb/dcerpc/client' require 'ruby_smb/dcerpc/error' require 'ruby_smb/dcerpc/fault' require 'ruby_smb/dcerpc/uuid' @@ -189,6 +190,8 @@ def force_set_auth_params(auth_type, auth_level) # @raise [ArgumentError] if `:auth_type` is unknown # @raise [NotImplementedError] if `:auth_type` is not implemented (yet) def bind(options={}) + options = options.merge(endpoint: @endpoint) if !options[:endpoint] && defined?(:@endpoint) && @endpoint + @call_id ||= 1 bind_req = Bind.new(options) bind_req.pdu_header.call_id = @call_id diff --git a/lib/ruby_smb/dcerpc/client.rb b/lib/ruby_smb/dcerpc/client.rb index 526832318..4b6d45c4c 100644 --- a/lib/ruby_smb/dcerpc/client.rb +++ b/lib/ruby_smb/dcerpc/client.rb @@ -11,7 +11,6 @@ class Client require 'ruby_smb/peer_info' include Dcerpc - include Epm include PeerInfo # The default maximum size of a RPC message that the Client accepts (in bytes) @@ -146,27 +145,29 @@ def initialize(host, # @return [TcpSocket] The connected TCP socket 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 + if @endpoint == Epm + port = ENDPOINT_MAPPER_PORT + else + epm_client = Client.new(@host, Epm, read_timeout: @read_timeout) + epm_client.connect + epm_client.bind + begin + towers = epm_client.ept_map_endpoint(@endpoint) + 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 = towers.first[:port] end - port = host_port[:port] - @tcp_socket.close - @tcp_socket = nil end + @tcp_socket = TCPSocket.new(@host, port) end diff --git a/lib/ruby_smb/dcerpc/epm.rb b/lib/ruby_smb/dcerpc/epm.rb index 56fa105e4..cecc77a30 100644 --- a/lib/ruby_smb/dcerpc/epm.rb +++ b/lib/ruby_smb/dcerpc/epm.rb @@ -13,33 +13,26 @@ module Epm require 'ruby_smb/dcerpc/epm/epm_ept_map_request' require 'ruby_smb/dcerpc/epm/epm_ept_map_response' - # Retrieve the service port number given a DCERPC interface UUID - # See: - # [2.2.1.2.5 ept_map Method](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/ab744583-430e-4055-8901-3c6bc007e791) - # [https://pubs.opengroup.org/onlinepubs/9629399/apdxo.htm](https://pubs.opengroup.org/onlinepubs/9629399/apdxo.htm) - # - # @param uuid [String] The interface UUID - # @param maj_ver [Integer] The interface Major version - # @param min_ver [Integer] The interface Minor version - # @param max_towers [Integer] The maximum number of elements to be returned - # @return [Hash] A hash with the host and port - # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a - # EpmEptMap packet - # @raise [RubySMB::Dcerpc::Error::EpmError] if the response error status - # is not STATUS_SUCCESS - def get_host_port_from_ept_mapper(uuid:, maj_ver:, min_ver:, max_towers: 1) - decoded_tower = EpmDecodedTowerOctetString.new( - interface_identifier: { - interface: uuid, - major_version: maj_ver, - minor_version: min_ver - }, - data_representation: { - interface: Ndr::UUID, - major_version: Ndr::VER_MAJOR, - minor_version: Ndr::VER_MINOR - } - ) + def ept_map(uuid:, maj_ver:, min_ver:, max_towers: 1, protocol: :ncacn_ip_tcp) + case protocol + when :ncacn_ip_tcp + result_key = :port + decoded_tower = EpmDecodedTowerOctetString.new( + interface_identifier: { + interface: uuid, + major_version: maj_ver, + minor_version: min_ver + }, + data_representation: { + interface: Ndr::UUID, + major_version: Ndr::VER_MAJOR, + minor_version: Ndr::VER_MINOR + } + ) + else + raise NotImplementedError, "Unsupported protocol: #{protocol}" + end + tower = EpmTwrt.new(decoded_tower) ept_map_request = EpmEptMapRequest.new( obj: Uuid.new, @@ -53,21 +46,30 @@ def get_host_port_from_ept_mapper(uuid:, maj_ver:, min_ver:, max_towers: 1) rescue IOError raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EptMapResponse' end + unless ept_map_response.error_status == WindowsError::NTStatus::STATUS_SUCCESS raise RubySMB::Dcerpc::Error::EpmError, "Error returned with ept_map: "\ "#{WindowsError::NTStatus.find_by_retval(ept_map_response.error_status.value).join(',')}" end - tower_binary = ept_map_response.towers[0].tower_octet_string.to_binary_s - begin - decoded_tower = EpmDecodedTowerOctetString.read(tower_binary) - rescue IOError - raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EpmDecodedTowerOctetString' + + ept_map_response.towers.map do |tower| + tower_binary = tower.tower_octet_string.to_binary_s + begin + decoded_tower = EpmDecodedTowerOctetString.read(tower_binary) + rescue IOError + raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EpmDecodedTowerOctetString' + end + + { + result_key => decoded_tower.pipe_or_port.pipe_or_port.to_i, + :host => decoded_tower.host_or_addr.host_or_addr.to_i + } end - { - port: decoded_tower.pipe_or_port.pipe_or_port.to_i, - host: decoded_tower.host_or_addr.host_or_addr.to_i - } + end + + def ept_map_endpoint(endpoint, **kwargs) + ept_map(uuid: endpoint::UUID, maj_ver: endpoint::VER_MAJOR, min_ver: endpoint::VER_MINOR, **kwargs) end end end From accfb99b79cc52395dd740b47d833d15e6249d5c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 9 Apr 2025 09:09:40 -0400 Subject: [PATCH 2/5] Switch the tower byte array type The tower elements are not supposed to be NDR types and they're not allowing the values to be set. --- lib/ruby_smb/dcerpc/epm/epm_twrt.rb | 90 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/lib/ruby_smb/dcerpc/epm/epm_twrt.rb b/lib/ruby_smb/dcerpc/epm/epm_twrt.rb index e1887e50a..b94cd572d 100644 --- a/lib/ruby_smb/dcerpc/epm/epm_twrt.rb +++ b/lib/ruby_smb/dcerpc/epm/epm_twrt.rb @@ -27,39 +27,39 @@ class EpmFloorProtocolIdentifier < Ndr::NdrStruct uint16 :lhs_bytecount, byte_align: 1, initial_value: -> {prot_identifier.num_bytes} # Protocol Identifiers: - # 0x00: "OSI Object Identifier [OID]" - # 0x02: "DNA Session Control Phase 4" - # 0x03: "DNA Session Control V3 Phase 5" - # 0x04: "DNA NSP Transport" - # 0x05: "OSI TP4 [T-Selector]" - # 0x06: "OSI CLNS [NSAP]" - # 0x07: "DOD TCP port" - # 0x08: "DOD UDP port" - # 0x09: "DOD IP v4 big-endian" - # 0x0a: "RPC Connectionless v4" - # 0x0b: "RPC Connection-oriented v5" - # 0x0c: "MS Named Pipes" - # 0x0d: "UUID" - # 0x0e: "ncadg_ipx" - # 0x0f: "NetBIOS Named Pipes" - # 0x10: "MS Named Pipe Name" or "Local InterProcess Communication (LRPC)") - # 0x11: "MS NetBIOS" - # 0x12: "MS NetBEUI" - # 0x13: "Netware SPX" - # 0x14: "Netware IPX" - # 0x15: "NMP_TOWER_ID" - # 0x16: "Appletalk Stream [endpoint]" - # 0x17: "Appletalk Datagram [endpoint]" - # 0x18: "Appletalk [NBP-style Name]" - # 0x19: "NetBIOS [CL on all protocols]" - # 0x1a: "VINES SPP" - # 0x1b: "VINES IPC" - # 0x1c: "StreetTalk [name]" - # 0x1d: "MSMQ" - # 0x1f: "MS IIS (http)" - # 0x20: "Unix Domain socket [pathname]" - # 0x21: "null" - # 0x22: "NetBIOS name" + # 0x00: "OSI Object Identifier [OID]" + # 0x02: "DNA Session Control Phase 4" + # 0x03: "DNA Session Control V3 Phase 5" + # 0x04: "DNA NSP Transport" + # 0x05: "OSI TP4 [T-Selector]" + # 0x06: "OSI CLNS [NSAP]" + # 0x07: "DOD TCP port" + # 0x08: "DOD UDP port" + # 0x09: "DOD IP v4 big-endian" + # 0x0a: "RPC Connectionless v4" + # 0x0b: "RPC Connection-oriented v5" + # 0x0c: "MS Named Pipes" + # 0x0d: "UUID" + # 0x0e: "ncadg_ipx" + # 0x0f: "NetBIOS Named Pipes" + # 0x10: "MS Named Pipe Name" or "Local InterProcess Communication (LRPC)") + # 0x11: "MS NetBIOS" + # 0x12: "MS NetBEUI" + # 0x13: "Netware SPX" + # 0x14: "Netware IPX" + # 0x15: "NMP_TOWER_ID" + # 0x16: "Appletalk Stream [endpoint]" + # 0x17: "Appletalk Datagram [endpoint]" + # 0x18: "Appletalk [NBP-style Name]" + # 0x19: "NetBIOS [CL on all protocols]" + # 0x1a: "VINES SPP" + # 0x1b: "VINES IPC" + # 0x1c: "StreetTalk [name]" + # 0x1d: "MSMQ" + # 0x1f: "MS IIS (http)" + # 0x20: "Unix Domain socket [pathname]" + # 0x21: "null" + # 0x22: "NetBIOS name" uint8 :prot_identifier, byte_align: 1, initial_value: 0x0b uint16 :rhs_bytecount, byte_align: 1, initial_value: 2 uint16 :minor_version, byte_align: 1 @@ -77,7 +77,7 @@ class EpmFloorPipeOrHost < Ndr::NdrStruct # default: Host name uint8 :identifier, byte_align: 1 uint16 :rhs_bytecount, byte_align: 1, initial_value: -> { name.length } - ndr_fixed_byte_array :name, initial_length: :rhs_bytecount + uint8_array :name, initial_length: :rhs_bytecount, byte_align: 1 end class EpmFloorPipeOrPort < Ndr::NdrStruct @@ -100,9 +100,9 @@ class EpmFloorPipeOrPort < Ndr::NdrStruct uint8 :identifier, byte_align: 1, initial_value: 0x07 uint16 :rhs_bytecount, byte_align: 1, initial_value: -> { pipe_or_port.num_bytes } choice :pipe_or_port, selection: :identifier, byte_align: 1 do - ndr_fixed_byte_array 0x10, initial_length: :rhs_bytecount - ndr_fixed_byte_array 0x0c, initial_length: :rhs_bytecount - ndr_fixed_byte_array 0x0f, initial_length: :rhs_bytecount + uint8_array 0x10, initial_length: :rhs_bytecount, byte_align: 1 + uint8_array 0x0c, initial_length: :rhs_bytecount, byte_align: 1 + uint8_array 0x0f, initial_length: :rhs_bytecount, byte_align: 1 uint16be 0x07 uint16be 0x08 uint16be 0x13 @@ -110,7 +110,7 @@ class EpmFloorPipeOrPort < Ndr::NdrStruct uint16be 0x1a uint16be 0x1b uint16be 0x1f - ndr_fixed_byte_array :default, initial_length: :rhs_bytecount + uint8_array :default, initial_length: :rhs_bytecount, byte_align: 1 end end @@ -143,17 +143,17 @@ class EpmFloorHostOrAddr < Ndr::NdrStruct uint8 :identifier, byte_align: 1, initial_value: 0x09 uint16 :rhs_bytecount, byte_align: 1, initial_value: -> { host_or_addr.num_bytes } choice :host_or_addr, selection: :identifier, byte_align: 1 do - ndr_fixed_byte_array 0x11, initial_length: :rhs_bytecount - ndr_fixed_byte_array 0x12, initial_length: :rhs_bytecount - ndr_fixed_byte_array 0x22, initial_length: :rhs_bytecount + uint8_array 0x11, initial_length: :rhs_bytecount, byte_align: 1 + uint8_array 0x12, initial_length: :rhs_bytecount, byte_align: 1 + uint8_array 0x22, initial_length: :rhs_bytecount, byte_align: 1 epm_ipv4_address 0x09 epm_ipx_spx_address 0x13 epm_ipx_spx_address 0x14 - choice 0x00, selection: -> {rhs_bytecount.num_bytes} do - ndr_fixed_byte_array 16, initial_length: 16 - ndr_fixed_byte_array :default, initial_length: :rhs_bytecount + choice 0x00, selection: -> { rhs_bytecount.num_bytes } do + uint8_array 16, initial_length: 16, byte_align: 1 + uint8_array :default, initial_length: :rhs_bytecount, byte_align: 1 end - ndr_fixed_byte_array :default, initial_length: :rhs_bytecount + uint8_array :default, initial_length: :rhs_bytecount, byte_align: 1 end end From e8109e75a203ef7d0d9715d1e1245f636b939f22 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 9 Apr 2025 09:10:31 -0400 Subject: [PATCH 3/5] Add the examples/epm_client.rb script for testing --- examples/epm_client.rb | 65 +++++++++++++++++++++++++++++++++ lib/ruby_smb/dcerpc/client.rb | 4 ++- lib/ruby_smb/dcerpc/epm.rb | 68 ++++++++++++++++++++++++++++------- 3 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 examples/epm_client.rb diff --git a/examples/epm_client.rb b/examples/epm_client.rb new file mode 100644 index 000000000..d1d5f8bcf --- /dev/null +++ b/examples/epm_client.rb @@ -0,0 +1,65 @@ +#!/usr/bin/ruby + +require 'bundler/setup' +require 'ruby_smb' + +require 'optparse' +require 'pp' + +options = { + major_version: 1, + minor_version: 0, + max_towers: 1, +} + +parser = OptionParser.new do |opts| + opts.banner = "Usage: script.rb [options] TARGET PROTOCOL UUID" + + opts.on("--major-version N", Integer, "Specify major version number (default: #{options[:major_version]})") do |v| + options[:major_version] = v + end + + opts.on("--minor-version N", Integer, "Specify minor version number ((default: #{options[:minor_version]})") do |v| + options[:minor_version] = v + end + + opts.on("--max-towers N", Integer, "Set the maximum number of towers (default: #{options[:max_towers]})") do |v| + options[:max_towers] = v + end + + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end +end + +# Parse and extract positional arguments +begin + parser.order!(ARGV) + if ARGV.size != 3 + raise OptionParser::MissingArgument, "TARGET, PROTOCOL, and UUID are required" + end + + options[:target], options[:protocol], options[:uuid] = ARGV +rescue OptionParser::ParseError => e + puts e.message + puts parser + exit 1 +end + +dcerpc_client = RubySMB::Dcerpc::Client.new(options[:target], RubySMB::Dcerpc::Epm) +dcerpc_client.connect +dcerpc_client.bind +dcerpc_client.ept_map( + uuid: options[:uuid], + maj_ver: options[:major_version], + min_ver: options[:minor_version], + protocol: options[:protocol].to_sym, + max_towers: options[:max_towers] +).each do |tower| + puts "Tower: #{tower[:endpoint]}" + tower.each do |key, value| + next if key == :endpoint + puts " #{key}: #{value}" + end +end \ No newline at end of file diff --git a/lib/ruby_smb/dcerpc/client.rb b/lib/ruby_smb/dcerpc/client.rb index 4b6d45c4c..99bab0255 100644 --- a/lib/ruby_smb/dcerpc/client.rb +++ b/lib/ruby_smb/dcerpc/client.rb @@ -152,8 +152,8 @@ def connect(port: nil) else epm_client = Client.new(@host, Epm, read_timeout: @read_timeout) epm_client.connect - epm_client.bind begin + epm_client.bind towers = epm_client.ept_map_endpoint(@endpoint) rescue RubySMB::Dcerpc::Error::DcerpcError => e e.message.prepend( @@ -162,6 +162,8 @@ def connect(port: nil) "EPM port resolution. Error: " ) raise e + ensure + epm_client.close end port = towers.first[:port] diff --git a/lib/ruby_smb/dcerpc/epm.rb b/lib/ruby_smb/dcerpc/epm.rb index cecc77a30..ed5bdf7cd 100644 --- a/lib/ruby_smb/dcerpc/epm.rb +++ b/lib/ruby_smb/dcerpc/epm.rb @@ -14,21 +14,66 @@ module Epm require 'ruby_smb/dcerpc/epm/epm_ept_map_response' def ept_map(uuid:, maj_ver:, min_ver:, max_towers: 1, protocol: :ncacn_ip_tcp) + interface_identifier = { + interface: uuid, + major_version: maj_ver, + minor_version: min_ver + } + data_representation = { + interface: Ndr::UUID, + major_version: Ndr::VER_MAJOR, + minor_version: Ndr::VER_MINOR + } + case protocol when :ncacn_ip_tcp - result_key = :port decoded_tower = EpmDecodedTowerOctetString.new( - interface_identifier: { - interface: uuid, - major_version: maj_ver, - minor_version: min_ver + interface_identifier: interface_identifier, + data_representation: data_representation, + pipe_or_port: { + identifier: 7, # 0x07: DOD TCP port + pipe_or_port: 0 }, - data_representation: { - interface: Ndr::UUID, - major_version: Ndr::VER_MAJOR, - minor_version: Ndr::VER_MINOR + host_or_addr: { + identifier: 9, # 0x09: DOD IP v4 address (big-endian) + host_or_addr: 0 } ) + + process_tower = lambda do |tower| + port = tower.pipe_or_port.pipe_or_port.value + address = IPAddr.new(tower.host_or_addr.host_or_addr.value, Socket::AF_INET) + { + port: port, + address: address, + # https://learn.microsoft.com/en-us/windows/win32/midl/ncacn-ip-tcp + endpoint: "ncacn_ip_tcp:#{address}[#{port}]" + } + end + when :ncacn_np + decoded_tower = EpmDecodedTowerOctetString.new( + interface_identifier: interface_identifier, + data_representation: data_representation, + pipe_or_port: { + identifier: 0x0f, # 0x0f: NetBIOS pipe name + pipe_or_port: [0] + }, + host_or_addr: { + identifier: 0x11, # 0x11: MS NetBIOS host name + host_or_addr: [0] + } + ) + + process_tower = lambda do |tower| + pipe = tower.pipe_or_port.pipe_or_port[...-1].pack('C*') + host = tower.host_or_addr.host_or_addr[...-1].pack('C*') + { + pipe: pipe, + host: host, + # https://learn.microsoft.com/en-us/windows/win32/midl/ncacn-nb-nb + endpoint: "ncacn_np:#{host}[#{pipe}]" + } + end else raise NotImplementedError, "Unsupported protocol: #{protocol}" end @@ -61,10 +106,7 @@ def ept_map(uuid:, maj_ver:, min_ver:, max_towers: 1, protocol: :ncacn_ip_tcp) raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EpmDecodedTowerOctetString' end - { - result_key => decoded_tower.pipe_or_port.pipe_or_port.to_i, - :host => decoded_tower.host_or_addr.host_or_addr.to_i - } + process_tower.(decoded_tower) end end From 562da997eee3af4eef42f7b3f2411896853ddac0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 9 Apr 2025 09:53:27 -0400 Subject: [PATCH 4/5] Update the client spec --- spec/lib/ruby_smb/dcerpc/client_spec.rb | 43 ++++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/spec/lib/ruby_smb/dcerpc/client_spec.rb b/spec/lib/ruby_smb/dcerpc/client_spec.rb index 40b6e42d4..dcea6b16c 100644 --- a/spec/lib/ruby_smb/dcerpc/client_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/client_spec.rb @@ -1,8 +1,9 @@ require 'ruby_smb/dcerpc/client' +require 'ipaddr' RSpec.describe RubySMB::Dcerpc::Client do - it 'includes the RubySMB::Dcerpc::Epm class' do - expect(described_class < RubySMB::Dcerpc::Epm).to be true + it 'includes the RubySMB::PeerInfo class' do + expect(described_class < RubySMB::PeerInfo).to be true end let(:host) { '1.2.3.4' } @@ -75,42 +76,38 @@ end context 'without TCP port' do - let(:host_port) { {host: '0.9.8.7', port: 999} } + let(:host_port) { {host: '192.0.2.1', port: 999} } + let(:epm_client) { double('EPM DCERPC Client') } let(:epm_tcp_socket) { double('EPM TcpSocket') } before :example do - allow(TCPSocket).to receive(:new).with(host, 135).and_return(epm_tcp_socket) - allow(client).to receive(:bind) - allow(client).to receive(:get_host_port_from_ept_mapper).and_return(host_port) - allow(epm_tcp_socket).to receive(:close) - end - - it 'connects to port 135' do - client.connect - expect(TCPSocket).to have_received(:new).with(host, 135) + allow(described_class).to receive(:new).with(host, endpoint).and_call_original + allow(described_class).to receive(:new).with(host, RubySMB::Dcerpc::Epm, {:read_timeout => 30}).and_return(epm_client) + allow(epm_client).to receive(:connect) + allow(epm_client).to receive(:bind) + allow(epm_client).to receive(:ept_map_endpoint).with(endpoint).and_return( + [{address: IPAddr.new(host_port[:host], Socket::AF_INET), port: host_port[:port]}] + ) + allow(epm_client).to receive(:close) end - it 'binds to the Endpoint Mapper endpoint' do + it 'creates a new client bound to the Endpoint Mapper endpoint' do client.connect - expect(client).to have_received(:bind).with(endpoint: RubySMB::Dcerpc::Epm) + expect(described_class).to have_received(:new).with(host, RubySMB::Dcerpc::Epm, {:read_timeout => 30}) end it 'gets host and port information from the Endpoint Mapper' do client.connect - expect(client).to have_received(:get_host_port_from_ept_mapper).with( - uuid: endpoint::UUID, - maj_ver: endpoint::VER_MAJOR, - min_ver: endpoint::VER_MINOR - ) + expect(epm_client).to have_received(:ept_map_endpoint).with(endpoint) end - it 'closes the EPM socket' do + it 'closes the EPM client socket' do client.connect - expect(epm_tcp_socket).to have_received(:close) + expect(epm_client).to have_received(:close) end it 'connects to the endpoint and returns the socket' do expect(client.connect).to eq(tcp_socket) - expect(TCPSocket).to have_received(:new).with(host, 999) + expect(TCPSocket).to have_received(:new).with(host, host_port[:port]) end end end @@ -330,7 +327,7 @@ it 'sends an auth3 request' do client.bind(**kwargs) - expect(client).to have_received(:auth_provider_complete_handshake).with(bindack_response, auth_type: kwargs[:auth_type], auth_level: kwargs[:auth_level]) + expect(client).to have_received(:auth_provider_complete_handshake).with(bindack_response, auth_type: kwargs[:auth_type], auth_level: kwargs[:auth_level], endpoint: endpoint) end end end From cd2a57ba6ba2d0f83806f496ba60475333c5a7da Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 9 Apr 2025 10:52:52 -0400 Subject: [PATCH 5/5] Document the function and raise a specific error --- lib/ruby_smb/dcerpc/epm.rb | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/ruby_smb/dcerpc/epm.rb b/lib/ruby_smb/dcerpc/epm.rb index ed5bdf7cd..f4bb88151 100644 --- a/lib/ruby_smb/dcerpc/epm.rb +++ b/lib/ruby_smb/dcerpc/epm.rb @@ -9,11 +9,27 @@ module Epm # Operation numbers EPT_MAP = 0x0003 + # MS-RPCE specific error codes + STATUS_NO_ELEMENTS = 0x16C9A0D6 + require 'ruby_smb/dcerpc/epm/epm_twrt' require 'ruby_smb/dcerpc/epm/epm_ept_map_request' require 'ruby_smb/dcerpc/epm/epm_ept_map_response' - def ept_map(uuid:, maj_ver:, min_ver:, max_towers: 1, protocol: :ncacn_ip_tcp) + # Map a service to a connection end point. + # + # @param uuid [String] The object UUID of the interface. + # @param maj_ver [Integer] The major version number of the interface. + # @param min_ver [Integer] The minor version number of the interface. + # @param max_towers [Integer] The maximum number of results to obtain. + # @param protocol [Symbol] The protocol of endpoint to obtain. + # + # @return [Array>] The mapped endpoints. The hash keys will + # depend on the protocol that was selected but an endpoint key will + # always be present. + # @raise [NotImplementedError] Raised if the *protocol* argument is not + # supported. + def ept_map(uuid:, maj_ver:, min_ver: 0, max_towers: 1, protocol: :ncacn_ip_tcp) interface_identifier = { interface: uuid, major_version: maj_ver, @@ -92,7 +108,11 @@ def ept_map(uuid:, maj_ver:, min_ver:, max_towers: 1, protocol: :ncacn_ip_tcp) raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EptMapResponse' end - unless ept_map_response.error_status == WindowsError::NTStatus::STATUS_SUCCESS + if ept_map_response.error_status == STATUS_NO_ELEMENTS + raise RubySMB::Dcerpc::Error::EpmError, + "Error returned with ept_map: "\ + "(0x16c9a0d6) STATUS_NO_ELEMENTS: There are no elements that satisfy the specified search criteria." + elsif ept_map_response.error_status != WindowsError::NTStatus::STATUS_SUCCESS raise RubySMB::Dcerpc::Error::EpmError, "Error returned with ept_map: "\ "#{WindowsError::NTStatus.find_by_retval(ept_map_response.error_status.value).join(',')}"