# -*- 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