Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions examples/epm_client.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/ruby_smb/dcerpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
41 changes: 22 additions & 19 deletions lib/ruby_smb/dcerpc/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -146,27 +145,31 @@ 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
begin
epm_client.bind
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
ensure
epm_client.close
end

port = towers.first[:port]
end
port = host_port[:port]
@tcp_socket.close
@tcp_socket = nil
end

@tcp_socket = TCPSocket.new(@host, port)
end

Expand Down
136 changes: 100 additions & 36 deletions lib/ruby_smb/dcerpc/epm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,91 @@ 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'

# 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)
# Map a service to a connection end point.
#
# @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
}
)
# @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<Hash<Symbol,>>] 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,
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
decoded_tower = EpmDecodedTowerOctetString.new(
interface_identifier: interface_identifier,
data_representation: data_representation,
pipe_or_port: {
identifier: 7, # 0x07: DOD TCP port
pipe_or_port: 0
},
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

tower = EpmTwrt.new(decoded_tower)
ept_map_request = EpmEptMapRequest.new(
obj: Uuid.new,
Expand All @@ -53,21 +107,31 @@ 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

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(',')}"
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

process_tower.(decoded_tower)
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
Expand Down
Loading
Loading