Skip to content
87 changes: 87 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?
Comment thread
smcintyre-r7 marked this conversation as resolved.
raise Rex::BindFailed.new(param.localhost, param.localport,
reason: 'Interface option is incompatible with proxy use'), caller
end

# On Windows, interface binding is handled by resolving the interface
# name to an IP address and overriding localhost before socket creation.
# No setsockopt-based interface binding is used.
if param.interface && !param.interface.empty? && Rex::Compat.is_windows
iface_ip = nil
begin
ifaddrs = ::Socket.getifaddrs.select { |ifaddr| ifaddr.name == param.interface }
iface = ifaddrs.find do |ifaddr|
if param.v6
ifaddr.addr&.ipv6?
else
ifaddr.addr&.ipv4?
end
end
iface_ip = iface&.addr&.ip_address
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 ifaddrs.empty?
"Interface #{param.interface} not found"
else
"Interface #{param.interface} has no #{param.v6 ? 'IPv6' : 'IPv4'} address"
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
Comment thread
smcintyre-r7 marked this conversation as resolved.
end

# Create the socket
sock = nil
if param.v6
Expand All @@ -166,6 +207,52 @@ def self.create_by_type(param, type, proto = 0)
sock = ::Socket.new(::Socket::AF_INET, type, proto)
end

# Apply interface binding BEFORE sock.bind() so the socket appears in netstat
# and accepts connections on the specified interface
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_macosx
begin
# IP_BOUND_IF may not be defined in Ruby builds on macOS,
# so we fallback to raw value 25 which is stable across versions.
ip_bound_if = defined?(::Socket::IP_BOUND_IF) ? ::Socket::IP_BOUND_IF : 25
iface = ::Socket.getifaddrs.find { |ifaddr| ifaddr.name == param.interface }
idx = iface&.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, 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

# Bind to a given local address and/or port if they are supplied
if param.localport || param.localhost
begin
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
Loading