diff --git a/lib/rex/proto/proxy/socks5.rb b/lib/rex/proto/proxy/socks5.rb index 9437c513f8..a4afbce353 100644 --- a/lib/rex/proto/proxy/socks5.rb +++ b/lib/rex/proto/proxy/socks5.rb @@ -1,645 +1,13 @@ - # -*- coding: binary -*- # -# sf - Sept 2010 -# surefire - May 2018 +# sf - Sept 2010 (original socks4a code) +# zeroSteiner - March 2018 (socks 5 update) +# surefire - May 2018 (socks 5 update) # -# TODO: Add support for required SOCKS username+password authentication -# TODO: Support multiple connection requests within a single session -# -require 'thread' -require 'rex/logging' -require 'rex/socket' - -module Rex -module Proto -module Proxy - -# -# A Socks5 proxy server. -# -class Socks5 - - # - # A client connected to the Socks5 server. - # - class Client - - # COMMON HEADER FIELDS - - RESERVED = 0 - - # ADDRESS TYPES - - ADDRESS_TYPE_IPV4 = 1 - ADDRESS_TYPE_DOMAINNAME = 3 - ADDRESS_TYPE_IPV6 = 4 - - # AUTHENTICATION TYPES - AUTH_PROTOCOL_VERSION = 0x01 - - AUTH_METHOD_TYPE_NONE = 0x00 - AUTH_METHOD_TYPE_USER_PASS = 0x02 - - AUTH_METHODS_REJECTED = 0xFF - - AUTH_SUCCESS = 0x00 - AUTH_FAILURE = 0x01 - - # REQUEST HEADER FIELDS - - REQUEST_VERSION = 5 - - REQUEST_AUTH_METHOD_COUNT = 1 - - REQUEST_COMMAND_CONNECT = 1 - REQUEST_COMMAND_BIND = 2 - REQUEST_COMMAND_UDP_ASSOCIATE = 3 # TODO: support UDP associate - - # RESPONSE HEADER FIELDS - - REPLY_VERSION = 5 - REPLY_FIELD_SUCCEEDED = 0 - REPLY_FIELD_SOCKS_SERVER_FAILURE = 1 - REPLY_FIELD_NOT_ALLOWED_BY_RULESET = 2 - REPLY_FIELD_NETWORK_UNREACHABLE = 3 - REPLY_FIELD_HOST_UNREACHABLE = 4 - REPLY_FIELD_CONNECTION_REFUSED = 5 - REPLY_FIELD_TTL_EXPIRED = 6 - REPLY_FIELD_COMMAND_NOT_SUPPORTED = 7 - REPLY_FIELD_ADDRESS_TYPE_NOT_SUPPORTED = 8 - - # RPEER INDEXES - - HOST = 1 - PORT = 2 - - class Response - - def initialize( sock ) - @version = REQUEST_VERSION - @command = nil - @reserved = RESERVED - @atyp = nil - @dest_port = 0 - @dest_ip = '0.0.0.0' - @sock = sock - end - - # convert IPv6 hex-encoded, colon-delimited string (0000:1111:...) into a 128-bit address - def ipv6_atoi(ip) - raw = "" - ip.scan(/....:/).each do |quad| - raw += quad[0,2].hex.chr - raw += quad[2,4].hex.chr - end - return raw - end - - # Pack a packet into raw bytes for transmitting on the wire. - def to_r - begin - - if @atyp == ADDRESS_TYPE_DOMAINNAME - if @dest_ip.include? '.' # stupid check for IPv4 addresses - @atyp = ADDRESS_TYPE_IPV4 - elsif @dest_ip.include? ':' # stupid check for IPv4 addresses - @atyp = ADDRESS_TYPE_IPV6 - else - raise "Malformed dest_ip while sending SOCKS5 response packet" - end - end - - if @atyp == ADDRESS_TYPE_IPV4 - raw = [ @version, @command, @reserved, @atyp, Rex::Socket.addr_atoi(@dest_ip), @dest_port ].pack( 'CCCCNn' ) - elsif @atyp == ADDRESS_TYPE_IPV6 - raw = [ @version, @command, @reserved, @atyp ].pack ( 'CCCC') - raw += ipv6_atoi(@dest_ip) - raw += [ @dest_port ].pack( 'n' ) - else - raise "Invalid address type field encountered while sending SOCKS5 response packet" - end - - return raw - - rescue TypeError - raise "Invalid field conversion while sending SOCKS5 response packet" - end - end - - def send - @sock.put(self.to_r) - end - - attr_writer :version, :command, :dest_port, :dest_ip, :hostname, :atyp - end - - class Request - - def initialize( sock ) - @version = REQUEST_VERSION - @command = nil - @atyp = nil - @dest_port = nil - @dest_ip = nil - @sock = sock - @username = nil - @password = nil - @serverAuthMethods = [ 0x00 ] - end - - def requireAuthentication( username, password ) - @username = username - @password = password - @serverAuthMethods = [ AUTH_METHOD_TYPE_USER_PASS ] - end - - # The first packet sent by the client is a session request - # +----+----------+----------+ - # |VER | NMETHODS | METHODS | - # +----+----------+----------+ - # | 1 | 1 | 1 to 255 | METHOD (\x00) = NO AUTHENTICATION REQUIRED - # +----+----------+----------+ - def parseIncomingSession() - raw = '' - - version = @sock.read( 1 ) - raise "Invalid Socks5 request packet received." if not - ( version.unpack( 'C' ).first == REQUEST_VERSION ) - - nMethods = @sock.read( 1 ).unpack( 'C' ).first - - unpackFormatStr = 'C' + nMethods.to_s # IS THIS REALLY WHAT I'M DOING?! - clientAuthMethods = @sock.read( nMethods ).unpack( unpackFormatStr ) - authMethods = ( clientAuthMethods & @serverAuthMethods ) - - if ( authMethods.empty? ) - raw = [ REQUEST_VERSION, AUTH_METHODS_REJECTED ].pack ( 'CC' ) - @sock.put( raw ) - raise "No matching authentication methods agreed upon in session request" - else - raw = [REQUEST_VERSION, authMethods[0]].pack ( 'CC' ) - @sock.put( raw ) - - parseIncomingCredentials() if authMethods[0] == AUTH_METHOD_TYPE_USER_PASS - end - end - - def parseIncomingCredentials() - # Based on RFC1929: https://tools.ietf.org/html/rfc1929 - # +----+------+----------+------+----------+ - # |VER | ULEN | UNAME | PLEN | PASSWD | - # +----+------+----------+------+----------+ - # | 1 | 1 | 1 to 255 | 1 | 1 to 255 | VERSION: 0x01 - # +----+------+----------+------+----------+ - - version = @sock.read( 1 ) - raise "Invalid SOCKS5 authentication packet received." if not - ( version.unpack( 'C' ).first == 0x01 ) - - usernameLength = @sock.read( 1 ).unpack( 'C' ).first - username = @sock.read( usernameLength ) - - passwordLength = @sock.read( 1 ).unpack( 'C' ).first - password = @sock.read( passwordLength ) - - # +----+--------+ - # |VER | STATUS | - # +----+--------+ VERSION: 0x01 - # | 1 | 1 | STATUS: 0x00=SUCCESS, otherwise FAILURE - # +----+--------+ - - if (username == @username && password == @password) - raw = [ AUTH_PROTOCOL_VERSION, AUTH_SUCCESS ].pack ( 'CC' ) - ilog("SOCKS5: Successfully authenticated") - @sock.put( raw ) - return true - else - raw = [ AUTH_PROTOCOL_VERSION, AUTH_FAILURE ].pack ( 'CC' ) - @sock.put( raw ) - raise "Invalid SOCKS5 credentials provided" - end - - end - - def parseIncomingConnectionRequest() - raw = @sock.read ( 262 ) # MAX LENGTH OF REQUEST WITH 256 BYTE HOSTNAME - - # fail if the incoming request is less than 8 bytes (malformed) - raise "Client closed connection while expecting SOCKS connection request" if( raw == nil ) - raise "Client sent malformed packet expecting SOCKS connection request" if( raw.length < 8 ) - - # Per RFC1928, the lengths of the SOCKS5 request header are: - # +----+-----+-------+------+----------+----------+ - # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | - # +----+-----+-------+------+----------+----------+ - # | 1 | 1 | X'00' | 1 | Variable | 2 | - # +----+-----+-------+------+----------+----------+ - - @version = raw[0..0].unpack( 'C' ).first - # fail if the incoming request is an unsupported version (not '0x05') - raise "Invalid SOCKS version received from client" if( @version != REQUEST_VERSION ) - - @command = raw[1..1].unpack( 'C' ).first - # fail if the incoming request is an unsupported command (currently only CONNECT) - raise "Invalid SOCKS proxy command received from client" if ( @command != REQUEST_COMMAND_CONNECT ) - - # "address type of following address" - @atyp = raw[3..3].unpack( 'C' ).first - - if (@atyp == ADDRESS_TYPE_IPV4) - # "the address is a version-4 IP address, with a length of 4 octets" - addressLen = 4 - addressEnd = 3 + addressLen - - hostname = nil - @dest_ip = Rex::Socket.addr_itoa( raw[4..7].unpack('N').first ) - elsif (@atyp == ADDRESS_TYPE_IPV6) - # "the address is a version-6 IP address, with a length of 16 octets" - addressLen = 16 - addressEnd = 3 + addressLen - - hostname = nil - @dest_ip = raw[4..19].unpack( 'H4H4H4H4H4H4H4H4' ).join(':') # Workaround because Rex::Socket.addr_itoa hurts too much - elsif (@atyp == ADDRESS_TYPE_DOMAINNAME) - # "the address field contains a fully-qualified domain name. The first - # octet of the address field contains the number of octets of name that - # follow, there is no terminating NUL octet." - - addressLen = raw[4..4].unpack( 'C' ).first - addressStart = 5 - addressEnd = 4+addressLen - - @hostname = raw[addressStart..addressEnd] - - @dest_ip = self.resolve( @hostname ) - ilog("SOCKS5: Resolved '#{@hostname}' to #{@dest_ip.to_s}") - - # fail if we couldnt resolve the hostname - if( not @dest_ip ) - wlog("SOCKS5: Failed to resolve '#{@hostname}'...") - end - - else - raise 'Invalid address type requested in connection request' - end - - @dest_port = raw[addressEnd+1 .. addressEnd+3].unpack('n').first - - return true - end - - def is_connect? - @command == REQUEST_COMMAND_CONNECT ? true : false - end - - def is_bind? - @command == REQUEST_COMMAND_BIND ? true : false - end - - attr_reader :version, :command, :dest_port, :dest_ip, :hostname, :atyp - - protected - - # Resolve the given hostname into a dotted IP address. - def resolve( hostname ) - if( not hostname.empty? ) - begin - return Rex::Socket.addr_itoa( Rex::Socket.gethostbyname( hostname )[3].unpack( 'N' ).first ) - rescue ::SocketError - return nil - end - end - return nil - end - end - - # A mixin for a socket to perform a relay to another socket. - module Relay - - # - # Relay 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("SOCKS5ProxyServerRelay", false) do - loop do - closed = false - buf = nil - - begin - s = Rex::ThreadSafe.select( [ @relay_sock ], nil, nil, 0.2 ) - if( s == nil || s[0] == nil ) - next - end - rescue - closed = true - end - - if( closed == false ) - begin - buf = @relay_sock.sysread( 32768 ) - closed = true if( buf == nil ) - rescue - closed = true - end - end - - if( closed == false ) - total_sent = 0 - total_length = buf.length - while( total_sent < total_length ) - begin - data = buf[total_sent, buf.length] - sent = self.write( data ) - if( sent > 0 ) - total_sent += sent - end - rescue - closed = true - break - end - end - end - - if( closed ) - @relay_client.stop - ::Thread.exit - end - end - end - - end - end - - # Create a new client connected to the server. - def initialize( server, sock, opts ) - @username = opts['USERNAME'] - @password = opts['PASSWORD'] - @server = server - @lsock = sock - @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 - request = Request.new ( @lsock ) - if not (@username.nil? or @password.nil?) - request.requireAuthentication( @username, @password ) - end - - # negotiate authentication - request.parseIncomingSession() - - # negotiate authentication - request.parseIncomingConnectionRequest() - - # handle the request - begin - # handle CONNECT requests - if( request.is_connect? ) - # perform the connection request - params = { - 'PeerHost' => request.dest_ip, - 'PeerPort' => request.dest_port, - } - params['Context'] = @server.opts['Context'] if @server.opts.has_key?('Context') - - @rsock = Rex::Socket::Tcp.create( params ) - # and send back success to the client - response = Response.new ( @lsock ) - response.version = REPLY_VERSION - response.command = REPLY_FIELD_SUCCEEDED - response.atyp = request.atyp - response.hostname = request.hostname - response.dest_port = request.dest_port - response.dest_ip = request.dest_ip - ilog("SOCKS5: request accepted to " + request.dest_ip.to_s + request.dest_port.to_s) - response.send() - # handle BIND requests - elsif( request.is_bind? ) # TODO: Test the BIND code with SOCKS5 (this is the old SOCKS4 code) - # create a server socket for this request - params = { - 'LocalHost' => '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 = Response.new ( @lsock ) - response.version = REPLY_VERSION - response.command = REPLY_FIELD_SUCCEEDED - response.atyp = request.atyp - response.hostname = request.hostname - response.dest_ip = '0.0.0.0' - response.dest_port = bsock.getlocalname()[PORT] - response.send() - ilog("SOCKS5: BIND request accepted to " + request.dest_ip.to_s + request.dest_port.to_s) - # accept a client connection (2 minute timeout as per 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 - # verify the connection is from the dest_ip origionally specified by the client - rpeer = @rsock.getpeername_as_array - raise "Got connection from an invalid peer." if( rpeer[HOST] != request.dest_ip ) - # send back the client connect success to the client - # sf: according to the spec we send this response back to the client, however - # I have seen some clients who bawk if they get this second response. - response = Response.new ( @lsock ) - response.version = REPLY_VERSION - response.command = REPLY_FIELD_SUCCEEDED - response.atyp = request.atyp - response.hostname = request.hostname - response.dest_ip = rpeer[HOST] - response.dest_port = rpeer[PORT] - response.send() - else - raise "Unknown request command received #{request.command} received." - end - rescue Rex::ConnectionRefused, Rex::HostUnreachable, Rex::InvalidDestination, Rex::ConnectionTimeout => e - # send back failure to the client - response = Response.new ( @lsock ) - response.version = REPLY_VERSION - response.atyp = request.atyp - response.dest_port = request.dest_port - response.dest_ip = request.dest_ip - if e.class == Rex::ConnectionRefused - response.command = REPLY_FIELD_CONNECTION_REFUSED - response.send() - raise "Connection refused by destination (#{request.dest_ip}:#{request.dest_port})" - elsif e.class == Rex::ConnectionTimeout - response.command = REPLY_FIELD_HOST_UNREACHABLE - response.send() - raise "Connection attempt timed out (#{request.dest_ip}:#{request.dest_port})" - elsif e.class == Rex::HostUnreachable - response.command = REPLY_FIELD_HOST_UNREACHABLE - response.send() - raise "Host Unreachable (#{request.dest_ip}:#{request.dest_port})" - elsif e.class == Rex::NetworkUnreachable - response.command = REPLY_FIELD_NETWORK_UNREACHABLE - response.send() - raise "Network unreachable (#{request.dest_ip}:#{request.dest_port})" - end - rescue RuntimeError - raise - # TODO: This happens when we get a connection refused for an IPv6 connection. :-( - # It's unknown if that's the only error case. - rescue => e - raise - response = Response.new ( @lsock ) - response.version = REPLY_VERSION - response.atyp = request.atyp - response.dest_port = request.dest_port - response.dest_ip = request.dest_ip - response.hostname = request.hostname - response.command = REPLY_FIELD_SOCKS_SERVER_FAILURE - response.send() - # raise an exception to close this client connection - raise e - end - # setup the two way relay for full duplex io - @lsock.extend( Relay ) - @rsock.extend( Relay ) - # start the socket relays... - @lsock.relay( self, @rsock ) - @rsock.relay( self, @lsock ) - rescue - #raise # UNCOMMENT FOR DEBUGGING - wlog( "SOCKS5: #{$!}" ) - wlog( "SOCKS5: #{$!.message}" ) - self.stop - end - end - end - - # Stop handling the client connection. - def stop - @mutex.synchronize do - if( not @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 - - # Create a new Socks5 server. - def initialize( opts={} ) - @opts = { 'SRVHOST' => '0.0.0.0', 'SRVPORT' => 1080, - 'USERNAME' => nil, 'PASSWORD' => nil } - @opts = @opts.merge( opts['Context']['MsfExploit'].datastore ) - @server = nil - @clients = ::Array.new - @running = false - @server_thread = nil - end - - # - # Check if the server is running. - # - def is_running? - return @running - end - - # - # Start the Socks5 server. - # - def start - begin - # create the servers main socket (ignore the context here because we don't want a remote bind) - @server = Rex::Socket::TcpServer.create( 'LocalHost' => @opts['SRVHOST'], 'LocalPort' => @opts['SRVPORT'] ) - # signal we are now running - @running = true - # start the servers main thread to pick up new clients - @server_thread = Rex::ThreadFactory.spawn("SOCKS5ProxyServer", false) do - while( @running ) do - begin - # accept the client connection - sock = @server.accept - # and fire off a new client instance to handle it - Client.new( self, sock, @opts ).start - rescue - wlog( "Socks5.start - server_thread - #{$!}" ) - end - end - end - rescue - wlog( "Socks5.start - #{$!}" ) - return false - end - return true - end - - # - # Block while the server is running. - # - def join - @server_thread.join if @server_thread - end - - # - # Stop the Socks5 server. - # - def stop - if( @running ) - # signal we are no longer running - @running = false - # stop any clients we have (create a new client array as client.stop will delete from @clients) - clients = [] - clients.concat( @clients ) - clients.each do | client | - client.stop - end - # close the server socket - @server.close if @server - # if the server thread did not terminate gracefully, kill it. - @server_thread.kill if( @server_thread and @server_thread.alive? ) - end - return !@running - end - - def add_client( client ) - @clients << client - end - - def remove_client( client ) - @clients.delete( client ) - end - - attr_reader :opts - -end - -end; end; end +# references: +# - SOCKS Protocol Version 5 +# https://tools.ietf.org/html/rfc1928 +# - Username/Password Authentication for SOCKS V5 +# https://tools.ietf.org/html/rfc1929 +require 'rex/proto/proxy/socks5/server' diff --git a/lib/rex/proto/proxy/socks5/packet.rb b/lib/rex/proto/proxy/socks5/packet.rb new file mode 100644 index 0000000000..2f76186baa --- /dev/null +++ b/lib/rex/proto/proxy/socks5/packet.rb @@ -0,0 +1,109 @@ +# -*- coding: binary -*- + +require 'bindata' +require 'rex/socket' + +module Rex +module Proto +module Proxy + +module Socks5 + SOCKS_VERSION = 5 + + # + # Mixin for socks5 packets to include an address field. + # + module Address + ADDRESS_TYPE_IPV4 = 1 + ADDRESS_TYPE_DOMAINNAME = 3 + ADDRESS_TYPE_IPV6 = 4 + + def address + addr = address_array.to_ary.pack('C*') + if address_type == ADDRESS_TYPE_IPV4 || address_type == ADDRESS_TYPE_IPV6 + addr = Rex::Socket.addr_ntoa(addr) + end + addr + end + + def address=(value) + if Rex::Socket.is_ipv4?(value) + address_type.assign(ADDRESS_TYPE_IPV4) + domainname_length.assign(0) + value = Rex::Socket.addr_aton(value) + elsif Rex::Socket.is_ipv6?(value) + address_type.assign(ADDRESS_TYPE_IPV6) + domainname_length.assign(0) + value = Rex::Socket.addr_aton(value) + else + address_type.assign(ADDRESS_TYPE_DOMAINNAME) + domainname_length.assign(value.length) + end + address_array.assign(value.unpack('C*')) + end + + def address_length + case address_type + when ADDRESS_TYPE_IPV4 + 4 + when ADDRESS_TYPE_DOMAINNAME + domainname_length + when ADDRESS_TYPE_IPV6 + 16 + else + 0 + end + end + end + + class AuthRequestPacket < BinData::Record + endian :big + + uint8 :version, :initial_value => SOCKS_VERSION + uint8 :supported_methods_length + array :supported_methods, :type => :uint8, :initial_length => :supported_methods_length + end + + class AuthResponsePacket < BinData::Record + endian :big + + uint8 :version, :initial_value => SOCKS_VERSION + uint8 :chosen_method + end + + class Packet < BinData::Record + include Address + endian :big + hide :reserved, :domainname_length + + uint8 :version, :initial_value => SOCKS_VERSION + uint8 :command + uint8 :reserved + uint8 :address_type + uint8 :domainname_length, :onlyif => lambda { address_type == ADDRESS_TYPE_DOMAINNAME } + array :address_array, :type => :uint8, :initial_length => lambda { address_length } + uint16 :port + end + + class RequestPacket < Packet + end + + class ResponsePacket < Packet + end + + class UdpPacket < BinData::Record + include Address + endian :big + hide :reserved, :domainname_length + + uint16 :reserved + uint8 :frag + uint8 :address_type + uint8 :domainname_length, :onlyif => lambda { address_type == ADDRESS_TYPE_DOMAINNAME } + array :address_array, :type => :uint8, :initial_length => lambda { address_length } + uint16 :port + end +end +end +end +end diff --git a/lib/rex/proto/proxy/socks5/server.rb b/lib/rex/proto/proxy/socks5/server.rb new file mode 100644 index 0000000000..ad7c141eeb --- /dev/null +++ b/lib/rex/proto/proxy/socks5/server.rb @@ -0,0 +1,105 @@ +# -*- coding: binary -*- + +require 'thread' +require 'rex/logging' +require 'rex/socket' +require 'rex/proto/proxy/socks5/server_client' + +module Rex +module Proto +module Proxy + +module Socks5 + # + # A SOCKS5 proxy server. + # + class Server + # + # Create a new SOCKS5 server. + # + def initialize(opts={}) + @opts = { 'ServerHost' => '0.0.0.0', 'ServerPort' => 1080 } + @opts = @opts.merge(opts) + @server = nil + @clients = ::Array.new + @running = false + @server_thread = nil + end + + # + # Check if the server is running. + # + def is_running? + return @running + end + + # + # Start the SOCKS5 server. + # + def start + begin + # create the servers main socket (ignore the context here because we don't want a remote bind) + @server = Rex::Socket::TcpServer.create('LocalHost' => @opts['ServerHost'], 'LocalPort' => @opts['ServerPort']) + # signal we are now running + @running = true + # start the servers main thread to pick up new clients + @server_thread = Rex::ThreadFactory.spawn("SOCKS5ProxyServer", false) do + while @running + begin + # accept the client connection + sock = @server.accept + # and fire off a new client instance to handle it + ServerClient.new(self, sock, @opts).start + rescue + wlog("SOCKS5.start - server_thread - #{$!}") + end + end + end + rescue + wlog("SOCKS5.start - #{$!}") + return false + end + return true + end + + # + # Block while the server is running. + # + def join + @server_thread.join if @server_thread + end + + # + # Stop the SOCKS5 server. + # + def stop + if @running + # signal we are no longer running + @running = false + # stop any clients we have (create a new client array as client.stop will delete from @clients) + clients = @clients.dup + clients.each do | client | + client.stop + end + # close the server socket + @server.close if @server + # if the server thread did not terminate gracefully, kill it. + @server_thread.kill if @server_thread and @server_thread.alive? + end + return !@running + end + + def add_client(client) + @clients << client + end + + def remove_client(client) + @clients.delete(client) + end + + attr_reader :opts + end +end +end +end +end diff --git a/lib/rex/proto/proxy/socks5/server_client.rb b/lib/rex/proto/proxy/socks5/server_client.rb new file mode 100644 index 0000000000..69a3021052 --- /dev/null +++ b/lib/rex/proto/proxy/socks5/server_client.rb @@ -0,0 +1,299 @@ +# -*- 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] + 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 diff --git a/modules/auxiliary/server/socks5.rb b/modules/auxiliary/server/socks5.rb index d6688758bb..dc3fa07373 100644 --- a/modules/auxiliary/server/socks5.rb +++ b/modules/auxiliary/server/socks5.rb @@ -3,8 +3,6 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -# TODO: Find a way to background this (commenting out join() below causes it to stop immediately) - require 'thread' require 'rex/proto/proxy/socks5' @@ -13,11 +11,14 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Socks5 Proxy Server', - 'Description' => 'This module provides a socks5 proxy server that uses the builtin Metasploit routing to relay connections.', - 'Author' => 'sf', - 'License' => MSF_LICENSE, - 'Actions' => + 'Name' => 'Socks5 Proxy Server', + 'Description' => %q{ + This module provides a socks5 proxy server that uses the builtin + Metasploit routing to relay connections. + }, + 'Author' => [ 'sf', 'Spencer McIntyre', 'surefire' ], + 'License' => MSF_LICENSE, + 'Actions' => [ [ 'Proxy' ] ], @@ -28,27 +29,26 @@ class MetasploitModule < Msf::Auxiliary 'DefaultAction' => 'Proxy' ) - register_options( - [ - OptString.new( 'USERNAME', [ false, "Proxy username for SOCKS5 listener" ] ), - OptString.new( 'PASSWORD', [ false, "Proxy password for SOCKS5 listener" ] ), - OptString.new( 'SRVHOST', [ true, "The address to listen on", '127.0.0.1' ] ), - OptPort.new( 'SRVPORT', [ true, "The port to listen on.", 1080 ] ) - ]) + register_options([ + OptString.new('USERNAME', [false, 'Proxy username for SOCKS5 listener']), + OptString.new('PASSWORD', [false, 'Proxy password for SOCKS5 listener']), + OptString.new('SRVHOST', [true, 'The address to listen on', '0.0.0.0']), + OptPort.new('SRVPORT', [true, 'The port to listen on', 1080]) + ]) end def setup super @mutex = ::Mutex.new - @socks5 = nil + @socks_proxy = nil end def cleanup @mutex.synchronize do - if( @socks5 ) - print_status( "Stopping the socks5 proxy server" ) - @socks5.stop - @socks5 = nil + if @socks_proxy + print_status('Stopping the socks5 proxy server') + @socks_proxy.stop + @socks_proxy = nil end end super @@ -56,19 +56,16 @@ class MetasploitModule < Msf::Auxiliary def run opts = { - 'ServerHost' => datastore['SRVHOST'], - 'ServerPort' => datastore['SRVPORT'], + 'ServerHost' => datastore['SRVHOST'], + 'ServerPort' => datastore['SRVPORT'], 'ServerUsername' => datastore['USERNAME'], 'ServerPassword' => datastore['PASSWORD'], - 'Context' => {'Msf' => framework, 'MsfExploit' => self} + 'Context' => {'Msf' => framework, 'MsfExploit' => self} } + @socks_proxy = Rex::Proto::Proxy::Socks5::Server.new(opts) - @socks5 = Rex::Proto::Proxy::Socks5.new( opts ) - - print_status( "Starting the socks5 proxy server" ) - - @socks5.start - - @socks5.join + print_status('Starting the socks5 proxy server') + @socks_proxy.start + @socks_proxy.join end end diff --git a/spec/lib/rex/proto/http/client_spec.rb b/spec/lib/rex/proto/http/client_spec.rb index 6100b96a82..561b9c4e34 100644 --- a/spec/lib/rex/proto/http/client_spec.rb +++ b/spec/lib/rex/proto/http/client_spec.rb @@ -43,7 +43,7 @@ RSpec.describe Rex::Proto::Http::Client do end - it "should respond to intialize" do + it "should respond to initialize" do expect(cli).to be end diff --git a/spec/lib/rex/proto/proxy/socks5/packet_spec.rb b/spec/lib/rex/proto/proxy/socks5/packet_spec.rb new file mode 100644 index 0000000000..237960b4e4 --- /dev/null +++ b/spec/lib/rex/proto/proxy/socks5/packet_spec.rb @@ -0,0 +1,92 @@ +# -*- coding:binary -*- +require 'rex/proto/proxy/socks5/packet' + +RSpec.describe Rex::Proto::Proxy::Socks5::Packet do + Socks5 = Rex::Proto::Proxy::Socks5 + + describe "#address" do + it "should parse an IPv4 address" do + packet = Socks5::Packet.read("\x05\x02\x00\x01\x7f\x00\x00\x01\x00\x00") + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_IPV4) + expect(packet.address).to eq('127.0.0.1') + end + + it "should parse an IPv6 address" do + packet = Socks5::Packet.read("\x05\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00") + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_IPV6) + expect(packet.address).to eq('::1') + end + + it "should parse a domain name" do + packet = Socks5::Packet.read("\x05\x02\x00\x03\x12www.metasploit.com\x00\x00") + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_DOMAINNAME) + expect(packet.address).to eq('www.metasploit.com') + end + end + + describe "#address=" do + it "should set an IPv4 address" do + packet = Socks5::Packet.new + packet.address = '127.0.0.1' + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_IPV4) + expect(packet.address_array).to eq([0x7f, 0x00, 0x00, 0x01]) + end + + it "should set an IPv6 address" do + packet = Socks5::Packet.new + packet.address = '::1' + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_IPV6) + expect(packet.address_array).to eq([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) + end + + it "should set a domain name" do + packet = Socks5::Packet.new + packet.address = 'www.metasploit.com' + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_DOMAINNAME) + expect(packet.address_array).to eq([0x77, 0x77, 0x77, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x73, 0x70, 0x6c, 0x6f, 0x69, 0x74, 0x2e, 0x63, 0x6f, 0x6d]) + end + end + + describe "#command" do + it "should parse a connect command" do + packet = Socks5::Packet.read("\x05\x01\x00\x01\x7f\x00\x00\x01\x00\x00") + expect(packet.command).to eq(Socks5::ServerClient::COMMAND_CONNECT) + end + + it "should parse a bind command" do + packet = Socks5::Packet.read("\x05\x02\x00\x01\x7f\x00\x00\x01\x00\x00") + expect(packet.command).to eq(Socks5::ServerClient::COMMAND_BIND) + end + + it "should parse a UDP associate command" do + packet = Socks5::Packet.read("\x05\x03\x00\x01\x7f\x00\x00\x01\x00\x00") + expect(packet.command).to eq(Socks5::ServerClient::COMMAND_UDP_ASSOCIATE) + end + end + + describe "#read" do + it "should parse all fields" do + packet = Socks5::Packet.read("\x05\x01\x00\x01\x7f\x00\x00\x01\x00\x50") + expect(packet.version).to eq(Socks5::SOCKS_VERSION) + expect(packet.command).to eq(Socks5::ServerClient::COMMAND_CONNECT) + expect(packet.address_type).to eq(Socks5::Address::ADDRESS_TYPE_IPV4) + expect(packet.address).to eq('127.0.0.1') + expect(packet.port).to eq(80) + end + end + + describe "#to_binary_s" do + it "should pack the data to a binary string" do + packet = Socks5::Packet.new + expect(packet.to_binary_s).to eq("\x05\x00\x00\x00\x00\x00") + end + end + + describe "#version" do + it "should have the SOCKS5 version set by default" do + packet = Socks5::Packet.new + packet.version = Socks5::SOCKS_VERSION + end + end + +end diff --git a/spec/lib/rex/proto/proxy/socks5/server_spec.rb b/spec/lib/rex/proto/proxy/socks5/server_spec.rb new file mode 100644 index 0000000000..0b5409b40c --- /dev/null +++ b/spec/lib/rex/proto/proxy/socks5/server_spec.rb @@ -0,0 +1,17 @@ +# -*- coding:binary -*- +require 'rex/proto/proxy/socks5' + +RSpec.describe Rex::Proto::Proxy::Socks5::Server do + + subject(:server) do + Rex::Proto::Proxy::Socks5::Server.new + end + + describe "#is_running?" do + + it "should respond to #is_running?" do + expect(server.is_running?).to eq(false) + end + + end +end