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
221 changes: 123 additions & 98 deletions lib/rex/socket/comm/local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,40 +122,31 @@ def self.create_ip(param)
# Creates a socket using the supplied Parameter instance.
#
def self.create_by_type(param, type, proto = 0)

# Detect IPv6 addresses and enable IPv6 accordingly
if Rex::Socket.support_ipv6?

local = Rex::Socket.resolv_nbo(param.localhost) if param.localhost
peer = Rex::Socket.resolv_nbo(param.peerhost) if param.peerhost

# Enable IPv6 dual-bind mode for unbound UDP sockets on Linux
if type == ::Socket::SOCK_DGRAM && Rex::Compat.is_linux && !local && !peer
if type == ::Socket::SOCK_DGRAM && Rex::Compat.is_linux && !param.localhost && !param.peerhost
param.v6 = true

# Check if either of the addresses is 16 octets long
elsif (local && local.length == 16) || (peer && peer.length == 16)
elsif (param.localhost && Rex::Socket.is_ipv6?(param.localhost)) || (param.peerhost && Rex::Socket.is_ipv6?(param.peerhost))
param.v6 = true
end

if param.v6
if local && local.length == 4
if local == "\x00\x00\x00\x00"
if param.localhost && Rex::Socket.is_ipv4?(param.localhost)
if Rex::Socket.addr_atoi(param.localhost) == 0
param.localhost = '::'
elsif local == "\x7f\x00\x00\x01"
param.localhost = '::1'
else
param.localhost = '::ffff:' + Rex::Socket.getaddress(param.localhost, true)
param.localhost = '::ffff:' + param.localhost
end
end

if peer && peer.length == 4
if peer == "\x00\x00\x00\x00"
if param.peerhost && Rex::Socket.is_ipv4?(param.peerhost)
if Rex::Socket.addr_atoi(param.peerhost) == 0
param.peerhost = '::'
elsif peer == "\x7f\x00\x00\x01"
param.peerhost = '::1'
else
param.peerhost = '::ffff:' + Rex::Socket.getaddress(param.peerhost, true)
param.peerhost = '::ffff:' + param.peerhost
end
end
end
Expand All @@ -179,7 +170,7 @@ def self.create_by_type(param, type, proto = 0)
if param.localport || param.localhost
begin

# SO_REUSEADDR has undesired semantics on Windows, intead allowing
# SO_REUSEADDR has undesired semantics on Windows, instead allowing
# sockets to be stolen without warning from other unprotected
# processes.
unless Rex::Compat.is_windows
Expand Down Expand Up @@ -210,7 +201,7 @@ def self.create_by_type(param, type, proto = 0)
klass = Rex::Socket::SslTcpServer
end
elsif param.proto == 'sctp'
klass = Rex::Socket::SctpServer
klass = Rex::Socket::SctpServer
else
raise Rex::BindFailed.new(param.localhost, param.localport), caller
end
Expand All @@ -220,8 +211,6 @@ def self.create_by_type(param, type, proto = 0)
end
# Otherwise, if we're creating a client...
else
chain = []

# If we were supplied with host information
if param.peerhost

Expand Down Expand Up @@ -262,14 +251,13 @@ def self.create_by_type(param, type, proto = 0)
end

ip6_scope_idx = 0
ip = Rex::Socket.getaddress(param.peerhost)
port = param.peerport

if param.proxies
chain = param.proxies.dup
chain.push(['host',param.peerhost,param.peerport])
ip = chain[0][1]
port = chain[0][2].to_i

if param.proxies?
ip = param.proxies.first.host
port = param.proxies.first.port
else
ip = Rex::Socket.getaddress(param.peerhost)
port = param.peerport
end

begin
Expand Down Expand Up @@ -326,24 +314,20 @@ def self.create_by_type(param, type, proto = 0)
end
end

if chain.size > 1
chain.each_with_index {
|proxy, i|
next_hop = chain[i + 1]
if next_hop
proxy(sock, proxy[0], next_hop[1], next_hop[2])
end
}
if param.proxies?
param.proxies.each_cons(2) do |current_proxy, next_proxy|
proxy(sock, current_proxy.scheme, next_proxy.host, next_proxy.port)
end
current_proxy = param.proxies.last
proxy(sock, current_proxy.scheme, param.peerhost, param.peerport)
end

# Now extend the socket with SSL and perform the handshake
if(param.bare? == false and param.ssl)
if !param.bare? && param.ssl
klass = Rex::Socket::SslTcp
sock.extend(klass)
sock.initsock(param)
end


end

# Notify handlers that a socket has been created.
Expand Down Expand Up @@ -440,76 +424,49 @@ def self.proxy(sock, type, host, port)
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
end

resp = Rex::Proto::Http::Response.new
resp.update_cmd_parts(ret.split(/\r?\n/)[0])
if (match = ret.match(/HTTP\/.+?\s+(\d+)\s?.+?\r?\n?$/))
status_code = match[1].to_i
end
Comment on lines -443 to +429
Copy link
Contributor Author

@zeroSteiner zeroSteiner May 7, 2025

Choose a reason for hiding this comment

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

This fixed a bug where an exception would be raised if Rex::Socket was used out side the context of Metasploit because Rex::Proto::Http::Response is unavailable. The other proxies just do the bare minimum parsing necessary to ensure the connection was established, so that pattern was copied here as well. Removing this dependency also means the HTTP proxy can be tested in the specs.


if resp.code != 200
if status_code != 200
raise Rex::ConnectionProxyError.new(host, port, type, "The proxy returned a non-OK response"), caller
end
when Rex::Socket::Proxies::ProxyType::SOCKS4
supports_ipv6 = false
setup = [4,1,port.to_i].pack('CCn') + Rex::Socket.resolv_nbo(host, supports_ipv6) + Rex::Text.rand_text_alpha(rand(8)+1) + "\x00"
size = sock.put(setup)
if size != setup.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
end
if !Rex::Socket.is_ipv4?(host)
if !Rex::Socket.is_name?(host)
raise Rex::ConnectionProxyError.new(host, port, type, "The SOCKS4 target host must be an IPv4 address or a hostname"), caller
end

begin
ret = sock.get_once(8, 30)
rescue IOError
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
end
begin
address = Rex::Socket.getaddress(host, false)
rescue ::SocketError
raise Rex::ConnectionProxyError.new(host, port, type, "The SOCKS4 target '#{host}' could not be resolved to an IP address"), caller
end

if ret.nil? || ret.length < 8
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
end
if ret[1,1] != "\x5a"
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{ret[0,1].unpack("C")[0]}"), caller
end
when Rex::Socket::Proxies::ProxyType::SOCKS5
auth_methods = [5,1,0].pack('CCC')
size = sock.put(auth_methods)
if size != auth_methods.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
end
ret = sock.get_once(2,30)
if ret[1,1] == "\xff"
raise Rex::ConnectionProxyError.new(host, port, type, "The proxy requires authentication"), caller
host = address
end

if Rex::Socket.is_ipv4?(host)
accepts_ipv6 = false
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
setup = [5,1,0,1].pack('C4') + addr + [port.to_i].pack('n')
elsif Rex::Socket.support_ipv6? && Rex::Socket.is_ipv6?(host)
# IPv6 stuff all untested
accepts_ipv6 = true
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
setup = [5,1,0,4].pack('C4') + addr + [port.to_i].pack('n')
else
# Then it must be a domain name.
# Unfortunately, it looks like the host has always been
# resolved by the time it gets here, so this code never runs.
setup = [5,1,0,3].pack('C4') + [host.length].pack('C') + host + [port.to_i].pack('n')
end
self.proxy_socks4a(sock, type, host, port)
when Rex::Socket::Proxies::ProxyType::SOCKS5
# follow the unofficial convention where SOCKS5 handles the resolution locally (which leaks DNS)
if !Rex::Socket.is_ip_addr?(host)
if !Rex::Socket.is_name?(host)
raise Rex::ConnectionProxyError.new(host, port, type, "The SOCKS5 target host must be an IP address or a hostname"), caller
end

size = sock.put(setup)
if size != setup.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
end
begin
address = Rex::Socket.getaddress(host, Rex::Socket.support_ipv6?)
rescue ::SocketError
raise Rex::ConnectionProxyError.new(host, port, type, "The SOCKS5 target '#{host}' could not be resolved to an IP address"), caller
end

begin
response = sock.get_once(10, 30)
rescue IOError
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
host = address
end

if response.nil? || response.length < 10
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
end
if response[1,1] != "\x00"
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{response[1,1].unpack("C")[0]}"), caller
end
self.proxy_socks5h(sock, type, host, port)
when Rex::Socket::Proxies::ProxyType::SOCKS5H
# follow the unofficial convention where SOCKS5H has the proxy server resolve the hostname to and IP address
self.proxy_socks5h(sock, type, host, port)
else
raise RuntimeError, "The proxy type specified is not valid", caller
end
Expand All @@ -531,4 +488,72 @@ def self.deregister_event_handler(handler) # :nodoc:
def self.each_event_handler(handler) # :nodoc:
self.instance.each_event_handler(handler)
end

private

def self.proxy_socks4a(sock, type, host, port)
setup = [4,1,port.to_i].pack('CCn') + Rex::Socket.resolv_nbo(host, false) + Rex::Text.rand_text_alpha(rand(8)+1) + "\x00"
size = sock.put(setup)
if size != setup.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
end

begin
ret = sock.get_once(8, 30)
rescue IOError
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
end

if ret.nil? || ret.length < 8
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
end
if ret[1,1] != "\x5a"
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{ret[0,1].unpack("C")[0]}"), caller
end
end

def self.proxy_socks5h(sock, type, host, port)
auth_methods = [5,1,0].pack('CCC')
size = sock.put(auth_methods)
if size != auth_methods.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller(1)
end
ret = sock.get_once(2,30)
if ret[1,1] == "\xff"
raise Rex::ConnectionProxyError.new(host, port, type, "The proxy requires authentication"), caller(1)
end

if Rex::Socket.is_ipv4?(host)
accepts_ipv6 = false
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
setup = [5,1,0,1].pack('C4') + addr + [port.to_i].pack('n')
elsif Rex::Socket.is_ipv6?(host)
raise Rex::RuntimeError.new('Rex::Socket does not support IPv6') unless Rex::Socket.support_ipv6?

accepts_ipv6 = true
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
setup = [5,1,0,4].pack('C4') + addr + [port.to_i].pack('n')
else
# Then it must be a domain name.
setup = [5,1,0,3].pack('C4') + [host.length].pack('C') + host + [port.to_i].pack('n')
end

size = sock.put(setup)
if size != setup.length
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller(1)
end

begin
response = sock.get_once(10, 30)
rescue IOError
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller(1)
end

if response.nil? || response.length < 10
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller(1)
end
if response[1,1] != "\x00"
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{response[1,1].unpack("C")[0]}"), caller(1)
end
end
end
9 changes: 8 additions & 1 deletion lib/rex/socket/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def peerport
# The local host. Equivalent to the LocalHost parameter hash key.
# @return [String]
attr_writer :localhost

def localhost
return @localhost if @localhost

Expand Down Expand Up @@ -354,7 +355,9 @@ def comm
best_comm = nil
# If no comm was explicitly specified, try to use the comm that is best fit
# to handle the provided host based on the current routing table.
if server and localhost
if proxies?
best_comm = Rex::Socket::Comm::Local
elsif server && localhost
Comment on lines +358 to +360
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, only the local comm supports proxied connections. If a comm is specified, the proxy options are silently ignored. This seems like a problem. Solutions are probably to:

  1. Warn that proxies will be ignored.
  2. Raise an exception that the selected comm doesn't support proxies.
  3. Refactor proxy connection login into a mixin that can be included into any comm. The comm would be used to establish the connection to the first proxy in the chain, then it'd continue on from there. This would be the most difficult option and is thus left out of scope for this change set.

Kind of related is the fact that proxies are ignored for non-TCP socket types. Also seems like a problem, but supporting proxies in comms won't change that. SOCKS4 and HTTP simply don't support UDP and they never will.

best_comm = Rex::Socket::SwitchBoard.best_comm(localhost)
elsif peerhost
best_comm = Rex::Socket::SwitchBoard.best_comm(peerhost)
Expand Down Expand Up @@ -472,6 +475,10 @@ def v6
# @return [Array]
attr_accessor :proxies

def proxies?
proxies && !proxies.empty?
end

alias peeraddr peerhost
alias localaddr localhost
end
33 changes: 31 additions & 2 deletions lib/rex/socket/proxies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,41 @@ module ProxyType
HTTP = 'http'
SOCKS4 = 'socks4'
SOCKS5 = 'socks5'
SOCKS5H = 'socks5h'
end

# @param [String,nil] value A proxy chain of format {type:host:port[,type:host:port][...]}
# @return [Array] The array of proxies, i.e. {[['type', 'host', 'port']]}
# @return [Array<URI>] The array of proxies
def self.parse(value)
value.to_s.strip.split(',').map { |a| a.strip }.map { |a| a.split(':').map { |b| b.strip } }
proxies = []
value.to_s.strip.split(',').each do |proxy|
proxy = proxy.strip

# replace the first : with :// so it can be parsed as a URI
# URIs will offer more flexibility long term, but we'll keep backwards compatibility for now by treating : as ://
proxy = proxy.sub(/\A(\w+):(\w+)/, '\1://\2')
uri = URI(proxy)

unless supported_types.include?(uri.scheme)
raise Rex::RuntimeError.new("Unsupported proxy scheme: #{uri.scheme}")
end

if uri.host.nil? || uri.host.empty?
raise Rex::RuntimeError.new("A proxy URI must include a valid host.")
end

if uri.port.nil? && uri.scheme.start_with?('socks')
uri.port = 1080
end

if uri.port.nil? || uri.port.zero?
raise Rex::RuntimeError.new("A proxy URI must include a valid port.")
end

proxies << uri
end

proxies
end

def self.supported_types
Expand Down
Loading