Land #10102, SOCKS5 updates for BIND, parsing specs, refactoring
commit
829e1c306a
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue