Skip to content
Open
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
81 changes: 81 additions & 0 deletions lib/rex/socket/comm/local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,47 @@ def self.create_by_type(param, type, proto = 0)
# Notify handlers of the before socket create event.
self.instance.notify_before_socket_create(self, param)

# Binding to a specific interface while routing through a proxy is not
# supported. The proxy comm handles its own socket creation and ignores
# the interface option entirely, so we fail fast here rather than
# silently binding to the wrong interface.
if param.interface && !param.interface.empty? && param.proxies?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind please adding a comment here summarizing why we're doing this? We/I will surely forget in the future how we landed on this solution.

raise Rex::BindFailed.new(param.localhost, param.localport,
reason: 'Interface option is incompatible with proxy use'), caller
end

if param.interface && !param.interface.empty? && Rex::Compat.is_windows
iface_ip = nil
iface_found = false
begin
::Socket.getifaddrs.each do |ifaddr|
next unless ifaddr.name == param.interface
iface_found = true
if param.v6
next unless ifaddr.addr&.ipv6?
else
next unless ifaddr.addr&.ipv4?
end
iface_ip = ifaddr.addr.ip_address
break
end
rescue ::SystemCallError, ::SocketError => e
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: "Failed to enumerate interfaces: #{e.message}"), caller
end
if iface_ip.nil?
reason = if iface_found
"Interface #{param.interface} has no #{param.v6 ? 'IPv6' : 'IPv4'} address"
else
"Interface #{param.interface} not found"
end
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: reason), caller
end
param = param.dup # avoid mutating the caller's instance
param.localhost = iface_ip
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach may need to be modified ever so slightly. If we do this, then the param the caller passed us will be mutated and after this function has returned, the #localhost address the caller had originally set will be different. I think the easiest fix might be to update this to:

Suggested change
param.localhost = iface_ip
param = param.dup # avoid mutating the caller's instance
param.localhost = iface_ip

Alternatively you'd need to use a localhost local variable in this scope, defaulting to param.localhost and bind to that directly.

end

# Create the socket
sock = nil
if param.v6
Expand Down Expand Up @@ -185,6 +226,46 @@ def self.create_by_type(param, type, proto = 0)
end
end

if param.interface && !param.interface.empty?
if Rex::Compat.is_linux
begin
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BINDTODEVICE, param.interface)
rescue ::Errno::ENODEV, ::Errno::ENXIO
sock.close
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: "Interface #{param.interface} not found"), caller
rescue ::Errno::EPERM
sock.close
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: "Binding to interface #{param.interface} requires elevated privileges"), caller
rescue ::SystemCallError
sock.close
raise
end
elsif Rex::Compat.is_osx && defined?(::Socket::IP_BOUND_IF)
begin
idx = ::Socket.getifaddrs.find { |ifaddr| ifaddr.name == param.interface }&.ifindex
if idx.nil?
sock.close
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: "Interface #{param.interface} not found"), caller
end
sock.setsockopt(::Socket::IPPROTO_IP, ::Socket::IP_BOUND_IF, [idx].pack('I'))
rescue ::SocketError, ::Errno::ENXIO
sock.close
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: "Interface #{param.interface} not found"), caller
rescue ::SystemCallError
sock.close
raise
end
elsif !Rex::Compat.is_windows
sock.close
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: 'Interface binding is not supported on this platform'), caller
end
end

# Configure broadcast support for all datagram sockets
if type == ::Socket::SOCK_DGRAM
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BROADCAST, true)
Expand Down
9 changes: 9 additions & 0 deletions lib/rex/socket/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def self.from_hash(hash)
# retried.
# @option hash [Fixnum] 'Timeout' The number of seconds before a connection
# should time out
# @option hash [String] 'Interface' The network interface name to bind the socket to
# (e.g. 'eth0'). Only honoured by the local Comm; raises Rex::BindFailed if combined
# with a proxy.
def initialize(hash = {})
if (hash['PeerHost'])
self.peerhost = hash['PeerHost']
Expand Down Expand Up @@ -203,6 +206,8 @@ def initialize(hash = {})
self.timeout = hash['Timeout'].to_i
end

self.interface = hash['Interface'].to_s.strip if hash['Interface'] && !hash['Interface'].strip.empty?

# Whether to force IPv6 addressing
if hash['IPv6'].nil?
# if IPv6 isn't specified and at least one host is an IPv6 address and the
Expand Down Expand Up @@ -486,6 +491,10 @@ def v6
# @return [Array]
attr_accessor :proxies

# The network interface name to bind the socket to (e.g. 'eth0'). nil means no binding.
# @return [String, nil]
attr_accessor :interface

def proxies?
proxies && !proxies.empty?
end
Expand Down
58 changes: 57 additions & 1 deletion spec/rex/socket/comm/local_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,60 @@
end
end
end
end

describe 'Interface option' do
context 'when Interface is set and a proxy is also configured' do
it 'raises Rex::BindFailed before creating a socket' do
params = Rex::Socket::Parameters.new(
'Proto' => 'udp',
'Interface' => 'lo',
'Proxies' => 'socks5:127.0.0.1:1080'
)
expect { described_class.create(params) }.to raise_error(Rex::BindFailed)
end
end

context 'when Interface is set to a name that cannot exist' do
it 'raises Rex::BindFailed' do
skip 'Linux only' unless Rex::Compat.is_linux
skip 'requires root (SO_BINDTODEVICE)' unless Process.uid == 0
params = Rex::Socket::Parameters.new(
'Proto' => 'udp',
'LocalHost' => '127.0.0.1',
'LocalPort' => 0,
'Interface' => 'xX_no_such_iface_Xx'
)
expect { described_class.create(params) }.to raise_error(Rex::BindFailed)
end
end

context 'when Interface is set to loopback on Linux' do
it 'creates the socket successfully' do
skip 'Linux only' unless Rex::Compat.is_linux
skip 'requires root (SO_BINDTODEVICE)' unless Process.uid == 0
params = Rex::Socket::Parameters.new(
'Proto' => 'udp',
'LocalHost' => '127.0.0.1',
'LocalPort' => 0,
'Interface' => 'lo'
)
sock = described_class.create(params)
expect(sock).not_to be_nil
sock.close
end
end

context 'when running on a non-Linux platform' do
it 'raises Rex::BindFailed' do
skip 'non-Linux only' if Rex::Compat.is_linux
params = Rex::Socket::Parameters.new(
'Proto' => 'udp',
'LocalHost' => '127.0.0.1',
'LocalPort' => 0,
'Interface' => 'lo'
)
expect { described_class.create(params) }.to raise_error(Rex::BindFailed)
end
end
end
end
23 changes: 23 additions & 0 deletions spec/rex/socket/parameters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,27 @@
end
end

describe '#interface' do
it 'is nil by default' do
params = described_class.new({})
expect(params.interface).to be_nil
end

it 'is set from the Interface hash key' do
params = described_class.new('Interface' => 'eth0')
expect(params.interface).to eq('eth0')
end

it 'strips leading and trailing whitespace' do
params = described_class.new('Interface' => ' lo ')
expect(params.interface).to eq('lo')
end

it 'is nil when the key is absent, not an empty string' do
params = described_class.new({})
expect(params.interface).to be_nil
expect(params.interface).not_to eq('')
end
end

end