218 lines
5.1 KiB
Ruby
218 lines
5.1 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
module Rex
|
|
module Proto
|
|
module Ssh
|
|
###
|
|
#
|
|
# Runtime extension of the SSH clients that connect to the server.
|
|
#
|
|
###
|
|
|
|
module ServerClient
|
|
#
|
|
# Initialize a new connection instance.
|
|
#
|
|
def init_cli(server, do_not_start = false)
|
|
# Ssh relies on PTY not available on Windows, limiting the `require` here
|
|
# ensures eager_load patterns from zeitwerk will not attempt to load `hrr_rb_ssh`
|
|
# during startup.
|
|
require 'rex/proto/ssh/connection'
|
|
@server = server
|
|
@connection = Rex::Proto::Ssh::Connection.new(
|
|
self, server.server_options.merge(ssh_server: server), server.context
|
|
)
|
|
@connection_thread = Rex::ThreadFactory.spawn("SshConnectionMonitor-#{self}", false) {
|
|
self.connection.start
|
|
} unless do_not_start
|
|
rescue LoadError => e
|
|
wlog(e)
|
|
end
|
|
|
|
def close
|
|
@connection_thread.kill if @connection_thread and @connection_thread.alive?
|
|
super
|
|
end
|
|
|
|
attr_reader :connection, :server
|
|
end
|
|
|
|
###
|
|
#
|
|
# Acts as an SSH server, accepting clients and extending them with Connections
|
|
#
|
|
###
|
|
class Server
|
|
|
|
include Proto
|
|
#
|
|
# Initializes an SSH server as listening on the provided port and
|
|
# hostname.
|
|
#
|
|
def initialize(port = 22, listen_host = '0.0.0.0', context = {}, comm = nil,
|
|
ssh_opts = default_options, cc_cb = nil, cd_cb = nil)
|
|
|
|
self.listen_host = listen_host
|
|
self.listen_port = port
|
|
self.context = context
|
|
self.comm = comm
|
|
self.listener = nil
|
|
self.server_options = ssh_opts
|
|
self.on_client_connect_proc = cc_cb
|
|
self.on_client_data_proc = cd_cb
|
|
end
|
|
|
|
# More readable inspect that only shows the url and resources
|
|
# @return [String]
|
|
def inspect
|
|
"#<#{self.class} ssh://#{listen_host}:#{listen_port}>"
|
|
end
|
|
|
|
#
|
|
# Returns the hardcore alias for the SSH service
|
|
#
|
|
def self.hardcore_alias(*args)
|
|
"#{(args[0])}-#{(args[1])}-#{args[4] || ''}"
|
|
end
|
|
|
|
#
|
|
# SSH server.
|
|
#
|
|
def alias
|
|
super || "SSH Server"
|
|
end
|
|
|
|
|
|
#
|
|
# Listens on the defined port and host and starts monitoring for clients.
|
|
#
|
|
def start(srvsock = nil)
|
|
|
|
self.listener = srvsock.is_a?(Rex::Socket::TcpServer) ? srvsock : Rex::Socket::TcpServer.create(
|
|
'LocalHost' => self.listen_host,
|
|
'LocalPort' => self.listen_port,
|
|
'Context' => self.context,
|
|
'Comm' => self.comm
|
|
)
|
|
|
|
# Register callbacks
|
|
self.listener.on_client_connect_proc = Proc.new { |cli|
|
|
on_client_connect(cli)
|
|
}
|
|
# self.listener.on_client_data_proc = Proc.new { |cli|
|
|
# on_client_data(cli)
|
|
# }
|
|
self.clients = []
|
|
self.monitor_thread = Rex::ThreadFactory.spawn("SshServerClientMonitor", false) {
|
|
monitor_clients
|
|
}
|
|
self.listener.start
|
|
end
|
|
|
|
#
|
|
# Terminates the monitor thread and turns off the listener.
|
|
#
|
|
def stop
|
|
self.listener.stop
|
|
self.listener.close
|
|
self.clients = []
|
|
end
|
|
|
|
|
|
#
|
|
# Waits for the SSH service to terminate
|
|
#
|
|
def wait
|
|
self.listener.wait if self.listener
|
|
end
|
|
|
|
#
|
|
# Closes the supplied client, if valid.
|
|
#
|
|
def close_client(cli)
|
|
clients.delete(cli)
|
|
listener.close_client(cli.parent)
|
|
end
|
|
|
|
|
|
attr_accessor :listen_port, :listen_host, :context, :comm, :clients, :monitor_thread
|
|
attr_accessor :listener, :server_options, :on_client_connect_proc, :on_client_data_proc
|
|
|
|
protected
|
|
|
|
#
|
|
# Extends new clients with the ServerClient module and initializes them.
|
|
#
|
|
def on_client_connect(cli)
|
|
cli.extend(ServerClient)
|
|
|
|
cli.init_cli(self)
|
|
if self.on_client_connect_proc
|
|
self.on_client_connect_proc.call(cli)
|
|
else
|
|
enqueue_client(cli)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Watches FD channel abstractions, removes closed instances,
|
|
# checks for read data on clients if client data callback is defined,
|
|
# invokes the callback if possible, sleeps otherwise.
|
|
#
|
|
def monitor_clients
|
|
loop do
|
|
self.clients.delete_if {|c| c.closed? }
|
|
if self.on_client_data_proc
|
|
if clients.any? { |cli|
|
|
cli.has_read_data? and self.on_client_data_proc.call(cli)}
|
|
next
|
|
else
|
|
sleep 0.05
|
|
end
|
|
else
|
|
sleep 0.5
|
|
end
|
|
end
|
|
rescue => e
|
|
wlog(e)
|
|
end
|
|
|
|
#
|
|
# Waits for SSH client to "grow a pair" of FDs and adds
|
|
# a ChannelFD object derived from the client's Connection
|
|
# Channel's FDs to the Ssh::Server's clients array
|
|
#
|
|
# @param cli [Rex::Proto::Ssh::ServerClient] SSH client
|
|
#
|
|
def enqueue_client(cli)
|
|
Rex::ThreadFactory.spawn("ChannelFDWaiter", false) do
|
|
begin
|
|
Timeout::timeout(15) do
|
|
while cli.connection.open_channel_keys.empty? do
|
|
sleep 0.02
|
|
end
|
|
self.clients.push(Ssh::ChannelFD.new(cli))
|
|
end
|
|
rescue Timeout::Error
|
|
elog("Unable to find channel FDs for client #{cli}")
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Ssh relies on PTY not available on Windows, limiting the `require` here
|
|
# ensures eager_load patterns from zeitwerk will not attempt to load `hrr_rb_ssh`
|
|
# during startup.
|
|
def default_options
|
|
require 'rex/proto/ssh/connection'
|
|
Ssh::Connection.default_options
|
|
rescue LoadError => e
|
|
wlog(e)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|