Skip to content

Commit b220b3c

Browse files
authored
Merge pull request #76 from zeroSteiner/feat/proxy/socks5h
Add SOCKS5H Support
2 parents 660bc06 + a19f90f commit b220b3c

File tree

6 files changed

+283
-105
lines changed

6 files changed

+283
-105
lines changed

lib/rex/socket/comm/local.rb

Lines changed: 123 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -122,40 +122,31 @@ def self.create_ip(param)
122122
# Creates a socket using the supplied Parameter instance.
123123
#
124124
def self.create_by_type(param, type, proto = 0)
125-
126125
# Detect IPv6 addresses and enable IPv6 accordingly
127126
if Rex::Socket.support_ipv6?
128-
129-
local = Rex::Socket.resolv_nbo(param.localhost) if param.localhost
130-
peer = Rex::Socket.resolv_nbo(param.peerhost) if param.peerhost
131-
132127
# Enable IPv6 dual-bind mode for unbound UDP sockets on Linux
133-
if type == ::Socket::SOCK_DGRAM && Rex::Compat.is_linux && !local && !peer
128+
if type == ::Socket::SOCK_DGRAM && Rex::Compat.is_linux && !param.localhost && !param.peerhost
134129
param.v6 = true
135130

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

141136
if param.v6
142-
if local && local.length == 4
143-
if local == "\x00\x00\x00\x00"
137+
if param.localhost && Rex::Socket.is_ipv4?(param.localhost)
138+
if Rex::Socket.addr_atoi(param.localhost) == 0
144139
param.localhost = '::'
145-
elsif local == "\x7f\x00\x00\x01"
146-
param.localhost = '::1'
147140
else
148-
param.localhost = '::ffff:' + Rex::Socket.getaddress(param.localhost, true)
141+
param.localhost = '::ffff:' + param.localhost
149142
end
150143
end
151144

152-
if peer && peer.length == 4
153-
if peer == "\x00\x00\x00\x00"
145+
if param.peerhost && Rex::Socket.is_ipv4?(param.peerhost)
146+
if Rex::Socket.addr_atoi(param.peerhost) == 0
154147
param.peerhost = '::'
155-
elsif peer == "\x7f\x00\x00\x01"
156-
param.peerhost = '::1'
157148
else
158-
param.peerhost = '::ffff:' + Rex::Socket.getaddress(param.peerhost, true)
149+
param.peerhost = '::ffff:' + param.peerhost
159150
end
160151
end
161152
end
@@ -179,7 +170,7 @@ def self.create_by_type(param, type, proto = 0)
179170
if param.localport || param.localhost
180171
begin
181172

182-
# SO_REUSEADDR has undesired semantics on Windows, intead allowing
173+
# SO_REUSEADDR has undesired semantics on Windows, instead allowing
183174
# sockets to be stolen without warning from other unprotected
184175
# processes.
185176
unless Rex::Compat.is_windows
@@ -210,7 +201,7 @@ def self.create_by_type(param, type, proto = 0)
210201
klass = Rex::Socket::SslTcpServer
211202
end
212203
elsif param.proto == 'sctp'
213-
klass = Rex::Socket::SctpServer
204+
klass = Rex::Socket::SctpServer
214205
else
215206
raise Rex::BindFailed.new(param.localhost, param.localport), caller
216207
end
@@ -220,8 +211,6 @@ def self.create_by_type(param, type, proto = 0)
220211
end
221212
# Otherwise, if we're creating a client...
222213
else
223-
chain = []
224-
225214
# If we were supplied with host information
226215
if param.peerhost
227216

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

264253
ip6_scope_idx = 0
265-
ip = Rex::Socket.getaddress(param.peerhost)
266-
port = param.peerport
267-
268-
if param.proxies
269-
chain = param.proxies.dup
270-
chain.push(['host',param.peerhost,param.peerport])
271-
ip = chain[0][1]
272-
port = chain[0][2].to_i
254+
255+
if param.proxies?
256+
ip = param.proxies.first.host
257+
port = param.proxies.first.port
258+
else
259+
ip = Rex::Socket.getaddress(param.peerhost)
260+
port = param.peerport
273261
end
274262

275263
begin
@@ -326,24 +314,20 @@ def self.create_by_type(param, type, proto = 0)
326314
end
327315
end
328316

329-
if chain.size > 1
330-
chain.each_with_index {
331-
|proxy, i|
332-
next_hop = chain[i + 1]
333-
if next_hop
334-
proxy(sock, proxy[0], next_hop[1], next_hop[2])
335-
end
336-
}
317+
if param.proxies?
318+
param.proxies.each_cons(2) do |current_proxy, next_proxy|
319+
proxy(sock, current_proxy.scheme, next_proxy.host, next_proxy.port)
320+
end
321+
current_proxy = param.proxies.last
322+
proxy(sock, current_proxy.scheme, param.peerhost, param.peerport)
337323
end
338324

339325
# Now extend the socket with SSL and perform the handshake
340-
if(param.bare? == false and param.ssl)
326+
if !param.bare? && param.ssl
341327
klass = Rex::Socket::SslTcp
342328
sock.extend(klass)
343329
sock.initsock(param)
344330
end
345-
346-
347331
end
348332

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

443-
resp = Rex::Proto::Http::Response.new
444-
resp.update_cmd_parts(ret.split(/\r?\n/)[0])
427+
if (match = ret.match(/HTTP\/.+?\s+(\d+)\s?.+?\r?\n?$/))
428+
status_code = match[1].to_i
429+
end
445430

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

457-
begin
458-
ret = sock.get_once(8, 30)
459-
rescue IOError
460-
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
461-
end
440+
begin
441+
address = Rex::Socket.getaddress(host, false)
442+
rescue ::SocketError
443+
raise Rex::ConnectionProxyError.new(host, port, type, "The SOCKS4 target '#{host}' could not be resolved to an IP address"), caller
444+
end
462445

463-
if ret.nil? || ret.length < 8
464-
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
465-
end
466-
if ret[1,1] != "\x5a"
467-
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{ret[0,1].unpack("C")[0]}"), caller
468-
end
469-
when Rex::Socket::Proxies::ProxyType::SOCKS5
470-
auth_methods = [5,1,0].pack('CCC')
471-
size = sock.put(auth_methods)
472-
if size != auth_methods.length
473-
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
474-
end
475-
ret = sock.get_once(2,30)
476-
if ret[1,1] == "\xff"
477-
raise Rex::ConnectionProxyError.new(host, port, type, "The proxy requires authentication"), caller
446+
host = address
478447
end
479448

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

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

501-
begin
502-
response = sock.get_once(10, 30)
503-
rescue IOError
504-
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
463+
host = address
505464
end
506465

507-
if response.nil? || response.length < 10
508-
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
509-
end
510-
if response[1,1] != "\x00"
511-
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{response[1,1].unpack("C")[0]}"), caller
512-
end
466+
self.proxy_socks5h(sock, type, host, port)
467+
when Rex::Socket::Proxies::ProxyType::SOCKS5H
468+
# follow the unofficial convention where SOCKS5H has the proxy server resolve the hostname to and IP address
469+
self.proxy_socks5h(sock, type, host, port)
513470
else
514471
raise RuntimeError, "The proxy type specified is not valid", caller
515472
end
@@ -531,4 +488,72 @@ def self.deregister_event_handler(handler) # :nodoc:
531488
def self.each_event_handler(handler) # :nodoc:
532489
self.instance.each_event_handler(handler)
533490
end
491+
492+
private
493+
494+
def self.proxy_socks4a(sock, type, host, port)
495+
setup = [4,1,port.to_i].pack('CCn') + Rex::Socket.resolv_nbo(host, false) + Rex::Text.rand_text_alpha(rand(8)+1) + "\x00"
496+
size = sock.put(setup)
497+
if size != setup.length
498+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller
499+
end
500+
501+
begin
502+
ret = sock.get_once(8, 30)
503+
rescue IOError
504+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller
505+
end
506+
507+
if ret.nil? || ret.length < 8
508+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller
509+
end
510+
if ret[1,1] != "\x5a"
511+
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{ret[0,1].unpack("C")[0]}"), caller
512+
end
513+
end
514+
515+
def self.proxy_socks5h(sock, type, host, port)
516+
auth_methods = [5,1,0].pack('CCC')
517+
size = sock.put(auth_methods)
518+
if size != auth_methods.length
519+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller(1)
520+
end
521+
ret = sock.get_once(2,30)
522+
if ret[1,1] == "\xff"
523+
raise Rex::ConnectionProxyError.new(host, port, type, "The proxy requires authentication"), caller(1)
524+
end
525+
526+
if Rex::Socket.is_ipv4?(host)
527+
accepts_ipv6 = false
528+
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
529+
setup = [5,1,0,1].pack('C4') + addr + [port.to_i].pack('n')
530+
elsif Rex::Socket.is_ipv6?(host)
531+
raise Rex::RuntimeError.new('Rex::Socket does not support IPv6') unless Rex::Socket.support_ipv6?
532+
533+
accepts_ipv6 = true
534+
addr = Rex::Socket.resolv_nbo(host, accepts_ipv6)
535+
setup = [5,1,0,4].pack('C4') + addr + [port.to_i].pack('n')
536+
else
537+
# Then it must be a domain name.
538+
setup = [5,1,0,3].pack('C4') + [host.length].pack('C') + host + [port.to_i].pack('n')
539+
end
540+
541+
size = sock.put(setup)
542+
if size != setup.length
543+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to send the entire request to the proxy"), caller(1)
544+
end
545+
546+
begin
547+
response = sock.get_once(10, 30)
548+
rescue IOError
549+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a response from the proxy"), caller(1)
550+
end
551+
552+
if response.nil? || response.length < 10
553+
raise Rex::ConnectionProxyError.new(host, port, type, "Failed to receive a complete response from the proxy"), caller(1)
554+
end
555+
if response[1,1] != "\x00"
556+
raise Rex::ConnectionProxyError.new(host, port, type, "Proxy responded with error code #{response[1,1].unpack("C")[0]}"), caller(1)
557+
end
558+
end
534559
end

lib/rex/socket/parameters.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ def peerport
323323
# The local host. Equivalent to the LocalHost parameter hash key.
324324
# @return [String]
325325
attr_writer :localhost
326+
326327
def localhost
327328
return @localhost if @localhost
328329

@@ -365,7 +366,9 @@ def comm
365366
best_comm = nil
366367
# If no comm was explicitly specified, try to use the comm that is best fit
367368
# to handle the provided host based on the current routing table.
368-
if server and localhost
369+
if proxies?
370+
best_comm = Rex::Socket::Comm::Local
371+
elsif server && localhost
369372
best_comm = Rex::Socket::SwitchBoard.best_comm(localhost)
370373
elsif peerhost
371374
best_comm = Rex::Socket::SwitchBoard.best_comm(peerhost)
@@ -483,6 +486,10 @@ def v6
483486
# @return [Array]
484487
attr_accessor :proxies
485488

489+
def proxies?
490+
proxies && !proxies.empty?
491+
end
492+
486493
alias peeraddr peerhost
487494
alias localaddr localhost
488495
end

lib/rex/socket/proxies.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,41 @@ module ProxyType
88
HTTP = 'http'
99
SOCKS4 = 'socks4'
1010
SOCKS5 = 'socks5'
11+
SOCKS5H = 'socks5h'
1112
end
1213

1314
# @param [String,nil] value A proxy chain of format {type:host:port[,type:host:port][...]}
14-
# @return [Array] The array of proxies, i.e. {[['type', 'host', 'port']]}
15+
# @return [Array<URI>] The array of proxies
1516
def self.parse(value)
16-
value.to_s.strip.split(',').map { |a| a.strip }.map { |a| a.split(':').map { |b| b.strip } }
17+
proxies = []
18+
value.to_s.strip.split(',').each do |proxy|
19+
proxy = proxy.strip
20+
21+
# replace the first : with :// so it can be parsed as a URI
22+
# URIs will offer more flexibility long term, but we'll keep backwards compatibility for now by treating : as ://
23+
proxy = proxy.sub(/\A(\w+):(\w+)/, '\1://\2')
24+
uri = URI(proxy)
25+
26+
unless supported_types.include?(uri.scheme)
27+
raise Rex::RuntimeError.new("Unsupported proxy scheme: #{uri.scheme}")
28+
end
29+
30+
if uri.host.nil? || uri.host.empty?
31+
raise Rex::RuntimeError.new("A proxy URI must include a valid host.")
32+
end
33+
34+
if uri.port.nil? && uri.scheme.start_with?('socks')
35+
uri.port = 1080
36+
end
37+
38+
if uri.port.nil? || uri.port.zero?
39+
raise Rex::RuntimeError.new("A proxy URI must include a valid port.")
40+
end
41+
42+
proxies << uri
43+
end
44+
45+
proxies
1746
end
1847

1948
def self.supported_types

0 commit comments

Comments
 (0)