300 lines
8.4 KiB
Ruby
300 lines
8.4 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'bindata'
|
|
require 'rex/socket'
|
|
require 'rex/proto/proxy/socks5/packet'
|
|
|
|
module Rex
|
|
module Proto
|
|
module Proxy
|
|
|
|
#
|
|
# A client connected to the proxy server.
|
|
#
|
|
module Socks5
|
|
#
|
|
# A mixin for a socket to perform a relay to another socket.
|
|
#
|
|
module TcpRelay
|
|
#
|
|
# TcpRelay data coming in from relay_sock to this socket.
|
|
#
|
|
def relay(relay_client, relay_sock)
|
|
@relay_client = relay_client
|
|
@relay_sock = relay_sock
|
|
# start the relay thread (modified from Rex::IO::StreamAbstraction)
|
|
@relay_thread = Rex::ThreadFactory.spawn("SOCKS5ProxyServerTcpRelay", false) do
|
|
loop do
|
|
closed = false
|
|
buf = nil
|
|
|
|
begin
|
|
s = Rex::ThreadSafe.select([@relay_sock], nil, nil, 0.2)
|
|
next if s.nil? || s[0].nil?
|
|
rescue
|
|
closed = true
|
|
end
|
|
|
|
unless closed
|
|
begin
|
|
buf = @relay_sock.sysread( 32768 )
|
|
closed = buf.nil?
|
|
rescue
|
|
closed = true
|
|
end
|
|
end
|
|
|
|
unless closed
|
|
total_sent = 0
|
|
total_length = buf.length
|
|
while total_sent < total_length
|
|
begin
|
|
data = buf[total_sent, buf.length]
|
|
sent = self.write(data)
|
|
total_sent += sent if sent > 0
|
|
rescue
|
|
closed = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if closed
|
|
@relay_client.stop
|
|
::Thread.exit
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# A client connected to the SOCKS5 server.
|
|
#
|
|
class ServerClient
|
|
AUTH_NONE = 0
|
|
AUTH_GSSAPI = 1
|
|
AUTH_CREDS = 2
|
|
AUTH_NO_ACCEPTABLE_METHODS = 255
|
|
|
|
AUTH_PROTOCOL_VERSION = 1
|
|
AUTH_RESULT_SUCCESS = 0
|
|
AUTH_RESULT_FAILURE = 1
|
|
|
|
COMMAND_CONNECT = 1
|
|
COMMAND_BIND = 2
|
|
COMMAND_UDP_ASSOCIATE = 3
|
|
|
|
REPLY_SUCCEEDED = 0
|
|
REPLY_GENERAL_FAILURE = 1
|
|
REPLY_NOT_ALLOWED = 2
|
|
REPLY_NET_UNREACHABLE = 3
|
|
REPLY_HOST_UNREACHABLE = 4
|
|
REPLY_CONNECTION_REFUSED = 5
|
|
REPLY_TTL_EXPIRED = 6
|
|
REPLY_CMD_NOT_SUPPORTED = 7
|
|
REPLY_ADDRESS_TYPE_NOT_SUPPORTED = 8
|
|
|
|
HOST = 1
|
|
PORT = 2
|
|
|
|
#
|
|
# Create a new client connected to the server.
|
|
#
|
|
def initialize(server, sock, opts={})
|
|
@server = server
|
|
@lsock = sock
|
|
@opts = opts
|
|
@rsock = nil
|
|
@client_thread = nil
|
|
@mutex = ::Mutex.new
|
|
end
|
|
|
|
# Start handling the client connection.
|
|
#
|
|
def start
|
|
# create a thread to handle this client request so as to not block the socks5 server
|
|
@client_thread = Rex::ThreadFactory.spawn("SOCKS5ProxyClient", false) do
|
|
begin
|
|
@server.add_client(self)
|
|
# get the initial client request packet
|
|
handle_authentication
|
|
|
|
# handle the request
|
|
handle_command
|
|
rescue => exception
|
|
# respond with a general failure to the client
|
|
response = ResponsePacket.new
|
|
response.command = REPLY_GENERAL_FAILURE
|
|
@lsock.put(response.to_binary_s)
|
|
|
|
wlog("Client.start - #{$!}")
|
|
self.stop
|
|
end
|
|
end
|
|
end
|
|
|
|
def handle_authentication
|
|
request = AuthRequestPacket.read(@lsock.get_once)
|
|
if @opts['ServerUsername'].nil? && @opts['ServerPassword'].nil?
|
|
handle_authentication_none(request)
|
|
else
|
|
handle_authentication_creds(request)
|
|
end
|
|
end
|
|
|
|
def handle_authentication_creds(request)
|
|
unless request.supported_methods.include? AUTH_CREDS
|
|
raise "Invalid SOCKS5 request packet received (no supported authentication methods)."
|
|
end
|
|
response = AuthResponsePacket.new
|
|
response.chosen_method = AUTH_CREDS
|
|
@lsock.put(response.to_binary_s)
|
|
|
|
version = @lsock.read(1)
|
|
raise "Invalid SOCKS5 authentication packet received." unless version.unpack('C').first == 0x01
|
|
|
|
username_length = @lsock.read(1).unpack('C').first
|
|
username = @lsock.read(username_length)
|
|
|
|
password_length = @lsock.read(1).unpack('C').first
|
|
password = @lsock.read(password_length)
|
|
|
|
# +-----+--------+
|
|
# | VER | STATUS |
|
|
# +-----+--------+ VERSION: 0x01
|
|
# | 1 | 1 | STATUS: 0x00=SUCCESS, otherwise FAILURE
|
|
# +-----+--------+
|
|
if username == @opts['ServerUsername'] && password == @opts['ServerPassword']
|
|
raw = [ AUTH_PROTOCOL_VERSION, AUTH_RESULT_SUCCESS ].pack ('CC')
|
|
ilog("SOCKS5: Successfully authenticated")
|
|
@lsock.put(raw)
|
|
else
|
|
raw = [ AUTH_PROTOCOL_VERSION, AUTH_RESULT_FAILURE ].pack ('CC')
|
|
@lsock.put(raw)
|
|
raise "Invalid SOCKS5 credentials provided"
|
|
end
|
|
end
|
|
|
|
def handle_authentication_none(request)
|
|
unless request.supported_methods.include? AUTH_NONE
|
|
raise "Invalid SOCKS5 request packet received (no supported authentication methods)."
|
|
end
|
|
response = AuthResponsePacket.new
|
|
response.chosen_method = AUTH_NONE
|
|
@lsock.put(response.to_binary_s)
|
|
end
|
|
|
|
def handle_command
|
|
request = RequestPacket.read(@lsock.get_once)
|
|
response = nil
|
|
case request.command
|
|
when COMMAND_BIND
|
|
response = handle_command_bind(request)
|
|
when COMMAND_CONNECT
|
|
response = handle_command_connect(request)
|
|
when COMMAND_UDP_ASSOCIATE
|
|
response = handle_command_udp_associate(request)
|
|
end
|
|
@lsock.put(response.to_binary_s) unless response.nil?
|
|
end
|
|
|
|
def handle_command_bind(request)
|
|
# create a server socket for this request
|
|
params = {
|
|
'LocalHost' => request.address_type == Address::ADDRESS_TYPE_IPV6 ? '::' : '0.0.0.0',
|
|
'LocalPort' => 0,
|
|
}
|
|
params['Context'] = @server.opts['Context'] if @server.opts.has_key?('Context')
|
|
bsock = Rex::Socket::TcpServer.create(params)
|
|
|
|
# send back the bind success to the client
|
|
response = ResponsePacket.new
|
|
response.command = REPLY_SUCCEEDED
|
|
response.address = bsock.getlocalname[HOST]
|
|
response.port = bsock.getlocalname[PORT]
|
|
@lsock.put(response.to_binary_s)
|
|
|
|
# accept a client connection (2 minute timeout as per the socks4a spec)
|
|
begin
|
|
::Timeout.timeout(120) do
|
|
@rsock = bsock.accept
|
|
end
|
|
rescue ::Timeout::Error
|
|
raise "Timeout reached on accept request."
|
|
end
|
|
|
|
# close the listening socket
|
|
bsock.close
|
|
|
|
setup_tcp_relay
|
|
response = ResponsePacket.new
|
|
response.command = REPLY_SUCCEEDED
|
|
response.address = @rsock.peerhost
|
|
response.port = @rsock.peerport
|
|
response
|
|
end
|
|
|
|
def handle_command_connect(request)
|
|
# perform the connection request
|
|
params = {
|
|
'PeerHost' => request.address,
|
|
'PeerPort' => request.port,
|
|
}
|
|
params['Context'] = @server.opts['Context'] if @server.opts.has_key?('Context')
|
|
@rsock = Rex::Socket::Tcp.create(params)
|
|
|
|
setup_tcp_relay
|
|
response = ResponsePacket.new
|
|
response.command = REPLY_SUCCEEDED
|
|
response.address = @rsock.getlocalname[HOST].split('-')[-1]
|
|
response.port = @rsock.getlocalname[PORT]
|
|
response
|
|
end
|
|
|
|
def handle_command_udp_associate(request)
|
|
response = ResponsePacket.new
|
|
response.command = REPLY_CMD_NOT_SUPPORTED
|
|
response
|
|
end
|
|
|
|
#
|
|
# Setup the TcpRelay between lsock and rsock.
|
|
#
|
|
def setup_tcp_relay
|
|
# setup the two way relay for full duplex io
|
|
@lsock.extend(TcpRelay)
|
|
@rsock.extend(TcpRelay)
|
|
# start the socket relays...
|
|
@lsock.relay(self, @rsock)
|
|
@rsock.relay(self, @lsock)
|
|
end
|
|
|
|
#
|
|
# Stop handling the client connection.
|
|
#
|
|
def stop
|
|
@mutex.synchronize do
|
|
unless @closed
|
|
begin
|
|
@lsock.close if @lsock
|
|
rescue
|
|
end
|
|
|
|
begin
|
|
@rsock.close if @rsock
|
|
rescue
|
|
end
|
|
|
|
@client_thread.kill if @client_thread and @client_thread.alive?
|
|
@server.remove_client(self)
|
|
@closed = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|