diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index dd8485d..b8fc8d1 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -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? + 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 + end + # Create the socket sock = nil if param.v6 @@ -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) diff --git a/lib/rex/socket/parameters.rb b/lib/rex/socket/parameters.rb index c161e2a..1f3a9a2 100644 --- a/lib/rex/socket/parameters.rb +++ b/lib/rex/socket/parameters.rb @@ -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'] @@ -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 @@ -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 diff --git a/spec/rex/socket/comm/local_spec.rb b/spec/rex/socket/comm/local_spec.rb index 59df386..d825660 100644 --- a/spec/rex/socket/comm/local_spec.rb +++ b/spec/rex/socket/comm/local_spec.rb @@ -107,4 +107,60 @@ end end end -end \ No newline at end of file + + 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 diff --git a/spec/rex/socket/parameters_spec.rb b/spec/rex/socket/parameters_spec.rb index 3d29196..5a14445 100644 --- a/spec/rex/socket/parameters_spec.rb +++ b/spec/rex/socket/parameters_spec.rb @@ -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