From 4be1a6a6d4a3561e096e38d685907609956ff641 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 10 Apr 2025 15:04:34 -0400 Subject: [PATCH 1/8] Add support for SOCKS5H proxies --- lib/rex/socket/comm/local.rb | 103 +++++++++++++++++++------------- lib/rex/socket/proxies.rb | 1 + spec/rex/socket/proxies_spec.rb | 1 + 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index 8c4b515..c630b29 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -179,7 +179,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 @@ -467,49 +467,25 @@ def self.proxy(sock, type, host, port) 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 - 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 + # 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 @@ -531,4 +507,51 @@ 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_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 diff --git a/lib/rex/socket/proxies.rb b/lib/rex/socket/proxies.rb index 5aa8794..08e2d4e 100644 --- a/lib/rex/socket/proxies.rb +++ b/lib/rex/socket/proxies.rb @@ -8,6 +8,7 @@ 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][...]} diff --git a/spec/rex/socket/proxies_spec.rb b/spec/rex/socket/proxies_spec.rb index 4405d31..82dac90 100644 --- a/spec/rex/socket/proxies_spec.rb +++ b/spec/rex/socket/proxies_spec.rb @@ -9,6 +9,7 @@ socks4 http socks5 + socks5h ] expect(subject.supported_types).to match_array expected end From 48e621822e52b204af3c7c3050d19cd3103041a7 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 May 2025 15:27:47 -0400 Subject: [PATCH 2/8] Add a test for Comm::Local --- lib/rex/socket/comm/local.rb | 12 ++++---- spec/rex/socket/comm/local_spec.rb | 46 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 spec/rex/socket/comm/local_spec.rb diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index c630b29..332f078 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -262,14 +262,15 @@ 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] + ip = chain[0][1] port = chain[0][2].to_i + else + ip = Rex::Socket.getaddress(param.peerhost) + port = param.peerport end begin @@ -326,12 +327,13 @@ def self.create_by_type(param, type, proto = 0) end end + # fixme: handle the chain object here 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]) + proxy(sock, proxy[0], next_hop[1], next_hop[2].to_i) end } end @@ -342,8 +344,6 @@ def self.create_by_type(param, type, proto = 0) sock.extend(klass) sock.initsock(param) end - - end # Notify handlers that a socket has been created. diff --git a/spec/rex/socket/comm/local_spec.rb b/spec/rex/socket/comm/local_spec.rb new file mode 100644 index 0000000..5878545 --- /dev/null +++ b/spec/rex/socket/comm/local_spec.rb @@ -0,0 +1,46 @@ +# -*- coding:binary -*- +require 'rex/socket/parameters' + +RSpec.describe Rex::Socket::Comm::Local do + describe '.create_by_type' do + let(:type) { ::Socket::SOCK_STREAM } + let(:proto) { ::Socket::IPPROTO_TCP } + let(:params) { Rex::Socket::Parameters.new({ 'PeerHost' => '192.0.2.1', 'PeerPort' => 1234 }) } + let(:sock) { RSpec::Mocks::Double.new('socket') } + + before(:each) do + allow(Rex::Socket).to receive(:support_ipv6?).with(no_args).and_return(true) + allow(::Socket).to receive(:new).with(any_args).and_return(sock) + allow(sock).to receive(:setsockopt).with(any_args) + allow(sock).to receive(:bind).with(any_args) + allow(sock).to receive(:connect).with(any_args).and_return(nil) + end + + it 'creates an IPv4 socket' do + expect(::Socket).to receive(:new).with(::Socket::AF_INET, type, proto).and_return(sock) + described_class.create_by_type(params, type, proto) + end + + it 'connects the new socket' do + expect(sock).to receive(:connect).with(Rex::Socket.to_sockaddr(params.peerhost, params.peerport)).and_return(nil) + described_class.create_by_type(params, type, proto) + end + + it 'connects directly to the target' do + expect(described_class).to_not receive(:proxy) + described_class.create_by_type(params, type, proto) + end + + context 'with proxies set' do + let(:params) { Rex::Socket::Parameters.new({ 'PeerHost' => '192.0.2.1', 'PeerPort' => 1234, 'Proxies' => 'http:192.0.2.2:8080, socks5:192.0.2.3:1080' }) } + + it 'connects to the target through a proxy' do + expect(sock).to receive(:connect).with(Rex::Socket.to_sockaddr('192.0.2.2', 8080)).and_return(nil) + + expect(described_class).to receive(:proxy).with(sock, 'http', '192.0.2.3', 1080).and_return(nil).ordered + expect(described_class).to receive(:proxy).with(sock, 'socks5', params.peerhost, params.peerport).and_return(nil).ordered + described_class.create_by_type(params, type, proto) + end + end + end +end \ No newline at end of file From 2bf32af1627a382242be5a0396dcf9079febfb20 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 May 2025 13:21:03 -0400 Subject: [PATCH 3/8] Use URIs for proxy strings and update specs --- lib/rex/socket/proxies.rb | 32 ++++++++++++++++++++++++++++-- spec/rex/socket/parameters_spec.rb | 6 +++--- spec/rex/socket/proxies_spec.rb | 8 +++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/rex/socket/proxies.rb b/lib/rex/socket/proxies.rb index 08e2d4e..e69c878 100644 --- a/lib/rex/socket/proxies.rb +++ b/lib/rex/socket/proxies.rb @@ -12,9 +12,37 @@ module ProxyType 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] 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 diff --git a/spec/rex/socket/parameters_spec.rb b/spec/rex/socket/parameters_spec.rb index 2e44f8d..3d29196 100644 --- a/spec/rex/socket/parameters_spec.rb +++ b/spec/rex/socket/parameters_spec.rb @@ -106,11 +106,11 @@ it 'should handle new proxy definitions' do expect(params.proxies).to eq nil - new_params = params.merge({ 'Proxies' => '1.2.3.4:1234, 5.6.7.8:5678' }) + new_params = params.merge({ 'Proxies' => 'http:1.2.3.4:1234, http:5.6.7.8:5678' }) expect(params.proxies).to eq nil expect(new_params.proxies).to eq [ - ['1.2.3.4', '1234'], - ['5.6.7.8', '5678'] + URI('http://1.2.3.4:1234'), + URI('http://5.6.7.8:5678') ] end end diff --git a/spec/rex/socket/proxies_spec.rb b/spec/rex/socket/proxies_spec.rb index 82dac90..f1b662e 100644 --- a/spec/rex/socket/proxies_spec.rb +++ b/spec/rex/socket/proxies_spec.rb @@ -1,4 +1,5 @@ # -*- coding:binary -*- +require 'uri' require 'rex/socket/proxies' RSpec.describe Rex::Socket::Proxies do @@ -20,7 +21,12 @@ { value: nil, expected: [] }, { value: '', expected: [] }, { value: ' ', expected: [] }, - { value: 'sapni:198.51.100.1:8080, socks4:198.51.100.1:1080 ', expected: [['sapni', '198.51.100.1', '8080'], ['socks4', '198.51.100.1', '1080']] }, + { value: 'http://localhost', expected: [URI('http://localhost:80')]}, + { value: 'http:localhost:8080', expected: [URI('http://localhost:8080')]}, + { value: 'socks4://localhost', expected: [URI('socks4://localhost:1080')] }, + { value: 'socks5://localhost', expected: [URI('socks5://localhost:1080')] }, + { value: 'socks5h://localhost', expected: [URI('socks5h://localhost:1080')] }, + { value: 'sapni:198.51.100.1:8080, socks4:198.51.100.1:1080 ', expected: [URI('sapni://198.51.100.1:8080'), URI('socks4://198.51.100.1:1080')] }, ].each do |test| it "correctly parses #{test[:value]} as #{test[:expected]}" do expect(described_class.parse(test[:value])).to eq test[:expected] From 5a4c8680cafb122793f9078ef516d10f75db91e7 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 6 May 2025 15:39:33 -0400 Subject: [PATCH 4/8] Update the proxy chain handling for URIs --- lib/rex/socket/comm/local.rb | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index 332f078..c745c70 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -263,11 +263,9 @@ def self.create_by_type(param, type, proto = 0) ip6_scope_idx = 0 - 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 && !param.proxies.empty? + ip = param.proxies.first.host + port = param.proxies.first.port else ip = Rex::Socket.getaddress(param.peerhost) port = param.peerport @@ -327,19 +325,16 @@ def self.create_by_type(param, type, proto = 0) end end - # fixme: handle the chain object here - 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].to_i) - end - } + if param.proxies && !param.proxies.empty? + 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) From e647ecedc50d0b21d613c5858598c74e6d34e382 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 May 2025 09:33:31 -0400 Subject: [PATCH 5/8] Use the local comm when proxies are specified Only the local comm supports proxies. This makes the priority: 1. The explicit comm that was set 2. Local if any proxies where set 3. The comm selected from the switchboard 4. The local comm --- lib/rex/socket/parameters.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rex/socket/parameters.rb b/lib/rex/socket/parameters.rb index 50e749e..41222d4 100644 --- a/lib/rex/socket/parameters.rb +++ b/lib/rex/socket/parameters.rb @@ -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 @@ -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 best_comm = Rex::Socket::SwitchBoard.best_comm(localhost) elsif peerhost best_comm = Rex::Socket::SwitchBoard.best_comm(peerhost) @@ -472,6 +475,10 @@ def v6 # @return [Array] attr_accessor :proxies + def proxies? + proxies && !proxies.empty? + end + alias peeraddr peerhost alias localaddr localhost end From e1d4b3ac034805aafb5944862d5f60983853bbea Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 May 2025 09:45:15 -0400 Subject: [PATCH 6/8] Remove some instances where hosts are resolved --- lib/rex/socket/comm/local.rb | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index c745c70..dd2bf32 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -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 @@ -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 @@ -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 @@ -263,7 +252,7 @@ def self.create_by_type(param, type, proto = 0) ip6_scope_idx = 0 - if param.proxies && !param.proxies.empty? + if param.proxies? ip = param.proxies.first.host port = param.proxies.first.port else @@ -325,7 +314,7 @@ def self.create_by_type(param, type, proto = 0) end end - if param.proxies && !param.proxies.empty? + 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 From 9de00d8596fbe6912a96a3ce6eb2fea17e3e4f8b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 May 2025 12:21:40 -0400 Subject: [PATCH 7/8] Remove a dependency on Rex::Proto::HTTP::Response It doesn't exist here, so just do some simple parsing like the other proxy protocol handlers do. --- lib/rex/socket/comm/local.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index dd2bf32..4a11764 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -424,10 +424,11 @@ 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 - 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 From a19f90fd8e5298c998d677b709534f4f7e4adf37 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 7 May 2025 12:34:37 -0400 Subject: [PATCH 8/8] Add tests for proxy handling --- lib/rex/socket/comm/local.rb | 49 +++++++++++++++-------- spec/rex/socket/comm/local_spec.rb | 64 ++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/lib/rex/socket/comm/local.rb b/lib/rex/socket/comm/local.rb index 4a11764..5bc439e 100644 --- a/lib/rex/socket/comm/local.rb +++ b/lib/rex/socket/comm/local.rb @@ -432,25 +432,21 @@ def self.proxy(sock, type, host, port) 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 + host = address 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) @@ -495,6 +491,27 @@ def self.each_event_handler(handler) # :nodoc: 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) diff --git a/spec/rex/socket/comm/local_spec.rb b/spec/rex/socket/comm/local_spec.rb index 5878545..59df386 100644 --- a/spec/rex/socket/comm/local_spec.rb +++ b/spec/rex/socket/comm/local_spec.rb @@ -34,6 +34,10 @@ context 'with proxies set' do let(:params) { Rex::Socket::Parameters.new({ 'PeerHost' => '192.0.2.1', 'PeerPort' => 1234, 'Proxies' => 'http:192.0.2.2:8080, socks5:192.0.2.3:1080' }) } + it 'does not resolve the hostname' do + expect(Rex::Socket).to_not receive(:getaddresses) + end + it 'connects to the target through a proxy' do expect(sock).to receive(:connect).with(Rex::Socket.to_sockaddr('192.0.2.2', 8080)).and_return(nil) @@ -43,4 +47,64 @@ end end end + + describe '.proxy' do + let(:sock) { RSpec::Mocks::Double.new('socket') } + let(:host) { '192.0.2.1' } + let(:port) { 8080 } + + context 'when type is http' do + let(:type) { 'http' } + + it 'connects via HTTP' do + data = "CONNECT #{host}:#{port} HTTP/1.0\r\n\r\n" + expect(sock).to receive(:put).with(data).and_return(data.length) + expect(sock).to receive(:get_once).and_return("HTTP/1.1 200 Connection Established\r\n\r\n") + described_class.proxy(sock, type, host, port) + end + end + + context 'when type is invalid' do + let(:type) { 'invalid' } + + it 'raises an error' do + expect { + described_class.proxy(sock, type, host, port) + }.to raise_error(RuntimeError, /proxy type specified is not valid/) + end + end + + context 'when type is socks4' do + let(:type) { 'socks4' } + let(:host) { 'localhost' } + + it 'resolves the hostname to an address' do + expect(Rex::Socket).to receive(:getaddress).with(host, false).and_return('127.0.0.1') + expect(described_class).to receive(:proxy_socks4a).with(sock, type, '127.0.0.1', port) + described_class.proxy(sock, type, host, port) + end + end + + context 'when type is socks5' do + let(:type) { 'socks5' } + let(:host) { 'localhost' } + + it 'resolves the hostname to an address' do + expect(Rex::Socket).to receive(:getaddress).with(host, Rex::Socket.support_ipv6?).and_return('127.0.0.1') + expect(described_class).to receive(:proxy_socks5h).with(sock, type, '127.0.0.1', port) + described_class.proxy(sock, type, host, port) + end + end + + context 'when type is socks5h' do + let(:type) { 'socks5h' } + let(:host) { 'localhost' } + + it 'does not resolve the hostname to an address' do + expect(Rex::Socket).to_not receive(:getaddress) + expect(described_class).to receive(:proxy_socks5h).with(sock, type, host, port) + described_class.proxy(sock, type, host, port) + end + end + end end \ No newline at end of file