327 lines
8.6 KiB
Ruby
327 lines
8.6 KiB
Ruby
# -*- coding: binary -*-
|
|
require 'rex/proto/ssh/hrr_rb_ssh'
|
|
|
|
module Rex
|
|
module Proto
|
|
module Ssh
|
|
|
|
##
|
|
# Whitelist-based access control scaffold
|
|
##
|
|
module AccessControlList
|
|
|
|
#
|
|
# Add permitted access control entry to access control list
|
|
# Create ACL if it does not yet exist
|
|
#
|
|
# @param host [String] Host/hostname for which to grant access
|
|
# @param port [Integer] Port for which to grant access
|
|
# @param bind [TrueClass,FalseClass] Whether this ACE is for servers
|
|
#
|
|
def permit=(host, port, bind = false)
|
|
@acl ||= { bind:[], connect:[] }
|
|
unless permit?(host, port, bind)
|
|
@acl[ bind ? :bind : :connect ] << "#{host}:#{port}"
|
|
end
|
|
end
|
|
|
|
#
|
|
# Delete permitted access control entry from access control list
|
|
#
|
|
# @param host [String] Host/hostname for which to grant access
|
|
# @param port [Integer] Port for which to grant access
|
|
# @param bind [TrueClass,FalseClass] Whether this ACE is for servers
|
|
#
|
|
def deny=(host, port, bind = false)
|
|
@acl[ bind ? :bind : :connect ].select! do |ent|
|
|
ent != "#{host}:#{port}"
|
|
end if @acl
|
|
end
|
|
|
|
#
|
|
# Check if access control entry exists in access control list
|
|
#
|
|
# @param host [String] Host/hostname for which to check access
|
|
# @param port [Integer] Port for which to check access
|
|
# @param bind [TrueClass,FalseClass] Whether this ACE is for servers
|
|
#
|
|
# @return [TrueClass,FalseClass] Permission boolean for access
|
|
def permit?(host, port, bind = false)
|
|
@acl and ["#{host}:#{port}", "*:*", "#{host}:*", "*:#{port}"].any? do |m|
|
|
@acl[ bind ? :bind : :connect ].include?(m)
|
|
end
|
|
end
|
|
end
|
|
|
|
##
|
|
# Encapsulation of Connection constructor for Rex use
|
|
# Provides ACLs for port forwarding and client (io) access hooks
|
|
##
|
|
class Connection < ::HrrRbSsh::Connection
|
|
include AccessControlList
|
|
def self.default_options
|
|
noneauth = HrrRbSsh::Authentication::Authenticator.new { |context| true }
|
|
return {
|
|
'authentication_none_authenticator' => noneauth,
|
|
'authentication_password_authenticator' => noneauth,
|
|
'authentication_publickey_authenticator' => noneauth,
|
|
'authentication_keyboard_interactive_authenticator' => noneauth,
|
|
'local_version' => 'SSH-2.0-RexProtoSsh'
|
|
}
|
|
end
|
|
#
|
|
# Create new Connection from an IO and options set, pull trans
|
|
# and auth from options if present, create from options set otherwise.
|
|
#
|
|
# Creates a default empty handler set for channel requests.
|
|
#
|
|
# @param io [IO] Socket, FD, or abstraction on which to build Connection
|
|
# @param options [Hash] Options for constructing Connection components
|
|
#
|
|
# @return [Rex::Proto::Ssh::Connection] a new connection object
|
|
def initialize(io = nil, options = self.default_options, context = {})
|
|
@context = context
|
|
@logger = Logger.new self.class.name
|
|
@server = options.delete(:ssh_server)
|
|
@mode = options.delete(:ssh_mode) || HrrRbSsh::Mode::SERVER
|
|
# Take a pre-built transport from the options or build one on the fly
|
|
@transport = options.delete(:ssh_transport) || HrrRbSsh::Transport.new(
|
|
io,
|
|
@mode,
|
|
options
|
|
)
|
|
# Take a pre-built authentication from the options or build one on the fly
|
|
@authentication = options.delete(:ssh_authentication) ||
|
|
HrrRbSsh::Authentication.new(@transport, @mode, options)
|
|
@global_request_handler = GlobalRequestHandler.new(self)
|
|
# Retain remaining options for later use
|
|
@options = options
|
|
|
|
@channels = Hash.new
|
|
@username = nil
|
|
@closed = nil
|
|
end
|
|
|
|
#
|
|
# Provide keys of explicitly not closed channels
|
|
#
|
|
# @param ctype [String] Channel type to select, nil for all
|
|
#
|
|
# @return [Array] Array of integers indexing open channels
|
|
def open_channel_keys(ctype = 'session')
|
|
channels.keys.sort.select do |cn|
|
|
channels[cn].closed? === false and (
|
|
ctype.nil? or channels[cn].channel_type == ctype
|
|
)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Provide IO from which to read remote-end inputs
|
|
#
|
|
# @param fd [Integer] Desired descriptor from which to read
|
|
# @param cn [Integer] Desired channel from which to take fd
|
|
#
|
|
# @return [IO] File descriptor for reading
|
|
def reader(fd = 0, cn = open_channel_keys.first)
|
|
channels[cn].io[fd]
|
|
end
|
|
|
|
#
|
|
# Provide IO into which writes to the remote end can be sent
|
|
#
|
|
# @param fd [Integer] Desired descriptor to which to write
|
|
# @param cn [Integer] Desired channel from which to take fd
|
|
#
|
|
# @return [IO] File descriptor for writing
|
|
def writer(fd = 1, cn = open_channel_keys.first)
|
|
channels[cn].io[fd]
|
|
end
|
|
|
|
#
|
|
# Close the connection and underlying socket
|
|
#
|
|
def close
|
|
super
|
|
@transport.io.close if @transport and !@transport.io.closed?
|
|
end
|
|
|
|
attr_accessor :transport, :authentication, :channels, :global_request_handler
|
|
attr_reader :server, :context
|
|
end
|
|
|
|
##
|
|
# A modified Rex::IO::Stream for separate file descriptors
|
|
# consumers are responsible for relevant initialization and
|
|
# fd_rd+fd_wr methods to expose selectable R/W IOs.
|
|
##
|
|
module IOMergeAbstraction
|
|
def inspect
|
|
"#{self.class}(#{fd_rd.inspect}|#{fd_wr.inspect})"
|
|
end
|
|
|
|
def write(buf, opts = {})
|
|
total_sent = 0
|
|
total_length = buf.length
|
|
block_size = 32768
|
|
|
|
begin
|
|
while( total_sent < total_length )
|
|
s = Rex::ThreadSafe.select( nil, [ fd_wr ], nil, 0.2 )
|
|
if( s == nil || s[0] == nil )
|
|
next
|
|
end
|
|
data = buf[total_sent, block_size]
|
|
sent = fd_wr.write_nonblock( data )
|
|
if sent > 0
|
|
total_sent += sent
|
|
end
|
|
end
|
|
rescue ::Errno::EAGAIN, ::Errno::EWOULDBLOCK
|
|
# Sleep for a half a second, or until we can write again
|
|
Rex::ThreadSafe.select( nil, [ fd_wr ], nil, 0.5 )
|
|
# Decrement the block size to handle full sendQs better
|
|
block_size = 1024
|
|
# Try to write the data again
|
|
retry
|
|
rescue ::IOError, ::Errno::EPIPE
|
|
return nil
|
|
end
|
|
|
|
total_sent
|
|
end
|
|
|
|
#
|
|
# This method reads data of the supplied length from the stream.
|
|
#
|
|
def read(length = nil, opts = {})
|
|
|
|
begin
|
|
return fd_rd.read_nonblock( length )
|
|
rescue ::Errno::EAGAIN, ::Errno::EWOULDBLOCK
|
|
# Sleep for a half a second, or until we can read again
|
|
Rex::ThreadSafe.select( [ fd_rd ], nil, nil, 0.5 )
|
|
# Decrement the block size to handle full sendQs better
|
|
retry
|
|
rescue ::IOError, ::Errno::EPIPE
|
|
return nil
|
|
end
|
|
end
|
|
|
|
#
|
|
# Polls the stream to see if there is any read data available. Returns
|
|
# true if data is available for reading, otherwise false is returned.
|
|
#
|
|
def has_read_data?(timeout = nil)
|
|
|
|
# Allow a timeout of "0" that waits almost indefinitely for input, this
|
|
# mimics the behavior of Rex::ThreadSafe.select() and fixes some corner
|
|
# cases of unintentional no-wait timeouts.
|
|
timeout = 3600 if (timeout and timeout == 0)
|
|
|
|
begin
|
|
if ((rv = ::IO.select([ fd_rd ], nil, nil, timeout)) and
|
|
(rv[0]) and
|
|
(rv[0][0] == fd_rd))
|
|
true
|
|
else
|
|
false
|
|
end
|
|
rescue ::Errno::EBADF, ::Errno::ENOTSOCK
|
|
raise ::EOFError
|
|
rescue StreamClosedError, ::IOError, ::EOFError, ::Errno::EPIPE
|
|
# Return false if the socket is dead
|
|
return false
|
|
end
|
|
end
|
|
|
|
def close
|
|
fd_rd.close if (fd_rd and !fd_rd.closed?)
|
|
fd_wr.close if (fd_wr and !fd_wr.closed?)
|
|
end
|
|
|
|
def closed?
|
|
(fd_rd.nil? or fd_rd.closed?) and (fd_wr.nil? or fd_wr.closed?)
|
|
end
|
|
end
|
|
|
|
##
|
|
# Emulate a single bidirectional IO using the clients Connections Channels IOs
|
|
##
|
|
class ChannelFD
|
|
include Rex::IO::Stream
|
|
include IOMergeAbstraction
|
|
def initialize(parent, chan_id = nil)
|
|
@parent = parent
|
|
end
|
|
|
|
def inspect
|
|
"#{super}/#{@parent.inspect}"
|
|
end
|
|
|
|
def close
|
|
super
|
|
@parent.close unless @parent.closed?
|
|
end
|
|
|
|
def closed?
|
|
super and @parent.closed?
|
|
end
|
|
|
|
def cid
|
|
if @cid.nil?
|
|
@cid = @parent.connection.open_channel_keys.first
|
|
end
|
|
@cid
|
|
end
|
|
|
|
def cid=(chan_id)
|
|
if @parent.connection.open_channel_keys.include?(chan_id)
|
|
@cid = chan_id
|
|
else
|
|
raise "Invalid Channel ID passed to #{self.inspect}"
|
|
end
|
|
end
|
|
attr_reader :parent
|
|
|
|
# private
|
|
|
|
#
|
|
# Provide a selectable filedescriptor open for reading
|
|
#
|
|
# @return [IO] Descriptor for reading
|
|
def fd_rd
|
|
begin
|
|
channel.io[0]
|
|
rescue => e
|
|
elog(e)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Provide a selectable filedescriptor open for writing
|
|
#
|
|
# @param fd [Symbol] Output FD type, anything but :stderr uses 1 (STDOUT)
|
|
#
|
|
# @return [IO] Descriptor for writing
|
|
def fd_wr(fd = :stdout)
|
|
begin
|
|
channel.io[(fd == :stderr ? 2 : 1)]
|
|
rescue => e
|
|
elog(e)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Expose a Channel from the Connection
|
|
#
|
|
# @return [HrrRbSsh::Connection::Channel] Channel object
|
|
def channel
|
|
@parent.connection.channels[cid]
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|