386 lines
12 KiB
Ruby
386 lines
12 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
# TODO: refactor this so it's no longer under Meterpreter so it can be used elsewhere
|
|
require 'rex/post/channel'
|
|
require 'rex/post/meterpreter/channels/socket_abstraction'
|
|
|
|
module Msf::Sessions
|
|
#
|
|
# This class provides a session for SSH client connections, where Metasploit
|
|
# has authenticated to a remote SSH server. It is compatible with the
|
|
# Net::SSH library.
|
|
#
|
|
class SshCommandShellBind < Msf::Sessions::CommandShell
|
|
|
|
include Msf::Session::Comm
|
|
include Rex::Post::Channel::Container
|
|
|
|
# see: https://datatracker.ietf.org/doc/html/rfc4254#section-5.1
|
|
module ChannelFailureReason
|
|
SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1
|
|
SSH_OPEN_CONNECT_FAILED = 2
|
|
SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3
|
|
SSH_OPEN_RESOURCE_SHORTAGE = 4
|
|
end
|
|
|
|
#
|
|
# This is a Metasploit Framework channel object that wraps a Net::SSH native
|
|
# channel object.
|
|
#
|
|
class TcpClientChannel
|
|
include Rex::Post::Channel::StreamAbstraction
|
|
|
|
#
|
|
# This is a common interface that socket paris are extended with to be
|
|
# compatible with pivoting.
|
|
#
|
|
module SocketInterface
|
|
include Rex::Post::Channel::SocketAbstraction::SocketInterface
|
|
|
|
def type?
|
|
'tcp'
|
|
end
|
|
end
|
|
|
|
#
|
|
# Create a new TcpClientChannel instance.
|
|
#
|
|
# @param client [SshCommandShellBind] The command shell session that this
|
|
# channel instance belongs to.
|
|
# @param cid [Integer] The channel ID.
|
|
# @param ssh_channel [Net::SSH::Connection::Channel] The connected SSH
|
|
# channel.
|
|
# @param params [Rex::Socket::Parameters] The parameters that were used to
|
|
# open the channel.
|
|
def initialize(client, cid, ssh_channel, params)
|
|
initialize_abstraction
|
|
|
|
@client = client
|
|
@cid = cid
|
|
@ssh_channel = ssh_channel
|
|
@params = params
|
|
@mutex = Mutex.new
|
|
|
|
ssh_channel.on_close do |_ch|
|
|
dlog('ssh_channel#on_close closing the sock')
|
|
close
|
|
end
|
|
|
|
ssh_channel.on_data do |_ch, data|
|
|
# dlog("ssh_channel#on_data received #{data.length} bytes")
|
|
rsock.syswrite(data)
|
|
end
|
|
|
|
ssh_channel.on_eof do |_ch|
|
|
dlog('ssh_channel#on_eof shutting down the socket')
|
|
rsock.shutdown(Socket::SHUT_WR)
|
|
end
|
|
|
|
lsock.extend(SocketInterface)
|
|
lsock.channel = self
|
|
|
|
rsock.extend(SocketInterface)
|
|
rsock.channel = self
|
|
|
|
lsock.extend(Rex::Socket::SslTcp) if params.ssl
|
|
|
|
# synchronize access so the socket isn't closed while initializing, this is particularly important for SSL
|
|
lsock.synchronize_access { lsock.initsock(params) }
|
|
rsock.synchronize_access { rsock.initsock(params) }
|
|
|
|
client.add_channel(self)
|
|
end
|
|
|
|
def closed?
|
|
@cid.nil?
|
|
end
|
|
|
|
def close
|
|
cid = @cid
|
|
@mutex.synchronize do
|
|
return if closed?
|
|
|
|
@cid = nil
|
|
end
|
|
|
|
@client.remove_channel(cid)
|
|
cleanup_abstraction
|
|
@ssh_channel.close
|
|
end
|
|
|
|
def close_write
|
|
if closed?
|
|
raise IOError, 'Channel has been closed.', caller
|
|
end
|
|
|
|
@ssh_channel.eof!
|
|
end
|
|
|
|
#
|
|
# Write *buf* to the channel, optionally truncating it to *length* bytes.
|
|
#
|
|
# @param [String] buf The data to write to the channel.
|
|
# @param [Integer] length An optional length to truncate *data* to before
|
|
# sending it.
|
|
def write(buf, length = nil)
|
|
if closed?
|
|
raise IOError, 'Channel has been closed.', caller
|
|
end
|
|
|
|
if !length.nil? && buf.length >= length
|
|
buf = buf[0..length]
|
|
end
|
|
|
|
@ssh_channel.send_data(buf)
|
|
buf.length
|
|
end
|
|
|
|
attr_reader :cid, :client, :params
|
|
end
|
|
|
|
class TcpServerChannel
|
|
def initialize(params, client, host, port)
|
|
@params = params
|
|
@client = client
|
|
@host = host
|
|
@port = port
|
|
@channels = Queue.new
|
|
@closed = false
|
|
end
|
|
|
|
def accept(opts = {})
|
|
timeout = opts['Timeout']
|
|
if (timeout.nil? || timeout <= 0)
|
|
timeout = 0
|
|
end
|
|
|
|
result = nil
|
|
begin
|
|
::Timeout.timeout(timeout) {
|
|
result = _accept
|
|
}
|
|
rescue Timeout::Error
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def closed?
|
|
@closed
|
|
end
|
|
|
|
def close
|
|
if !closed?
|
|
@closed = @client.stop_server_channel(@host, @port)
|
|
end
|
|
end
|
|
|
|
def create(cid, ssh_channel)
|
|
channel = TcpClientChannel.new(@client, cid, ssh_channel, @params)
|
|
@channels.enq(channel)
|
|
end
|
|
|
|
protected
|
|
|
|
def _accept(nonblock = false)
|
|
result = nil
|
|
begin
|
|
channel = @channels.deq(nonblock)
|
|
if channel
|
|
result = channel.lsock
|
|
end
|
|
rescue ThreadError
|
|
# This happens when there are no clients in the queue
|
|
end
|
|
result
|
|
end
|
|
|
|
|
|
end
|
|
|
|
#
|
|
# Create a sessions instance from an SshConnection. This will handle creating
|
|
# a new command stream.
|
|
#
|
|
# @param ssh_connection [Net::SSH::Connection] The SSH connection to create a
|
|
# session instance for.
|
|
# @param opts [Hash] Optional parameters to pass to the session object.
|
|
def initialize(ssh_connection, opts = {})
|
|
@ssh_connection = ssh_connection
|
|
@sock = ssh_connection.transport.socket
|
|
@server_channels = {}
|
|
|
|
initialize_channels
|
|
@channel_ticker = 0
|
|
|
|
rstream = Net::SSH::CommandStream.new(ssh_connection).lsock
|
|
|
|
# Be alerted to reverse port forward connections (once we start listening on a port)
|
|
ssh_connection.on_open_channel('forwarded-tcpip', &method(:on_got_remote_connection))
|
|
super(rstream, opts)
|
|
end
|
|
|
|
#
|
|
# Create a network socket using this session. At this time, only TCP client
|
|
# connections can be made (like SSH port forwarding) while TCP server sockets
|
|
# can not be opened (SSH reverse port forwarding). The SSH specification does
|
|
# not define a UDP channel, so that is not supported either.
|
|
#
|
|
# @param params [Rex::Socket::Parameters] The parameters that should be used
|
|
# to open the socket.
|
|
#
|
|
# @raise [Rex::ConnectionError] If the connection fails, timesout or is not
|
|
# supported, a ConnectionError will be raised.
|
|
# @return [TcpClientChannel] The connected TCP client channel.
|
|
def create(params)
|
|
# Notify handlers before we create the socket
|
|
notify_before_socket_create(self, params)
|
|
|
|
ssh_channel = msf_channel = nil
|
|
|
|
if params.proto == 'tcp'
|
|
if params.server
|
|
sock = create_server_channel(params)
|
|
else
|
|
sock = create_client_channel(params)
|
|
end
|
|
elsif params.proto == 'udp'
|
|
raise ::Rex::ConnectionError.new(params.peerhost, params.peerport, reason: 'UDP sockets are not supported by SSH sessions.')
|
|
end
|
|
|
|
raise ::Rex::ConnectionError unless sock
|
|
|
|
# Notify now that we've created the socket
|
|
notify_socket_created(self, sock, params)
|
|
|
|
sock
|
|
end
|
|
|
|
def create_server_channel(params)
|
|
msf_channel = nil
|
|
completed_event = Rex::Sync::Event.new
|
|
@ssh_connection.send_global_request('tcpip-forward', :string, params.localhost, :long, params.localport) do |success, response|
|
|
if success
|
|
remote_port = params.localport
|
|
remote_port = response.read_long if remote_port == 0
|
|
dlog("Remote forwarding from #{params.localhost} established on port #{remote_port}")
|
|
key = [params.localhost, remote_port]
|
|
msf_channel = TcpServerChannel.new(params, self, params.localhost, remote_port)
|
|
@server_channels[key] = msf_channel
|
|
else
|
|
print_error("Remote forwarding failed on #{params.localhost}:#{params.localport}")
|
|
end
|
|
completed_event.set
|
|
end
|
|
|
|
begin
|
|
completed_event.wait(params.timeout)
|
|
rescue TimeoutError
|
|
# Did not receive a response
|
|
end
|
|
|
|
# Return the server channel itself
|
|
sock = msf_channel
|
|
end
|
|
|
|
def stop_server_channel(host, port)
|
|
completed_event = Rex::Sync::Event.new
|
|
dlog("Cancelling tcpip-forward to #{host}:#{port}")
|
|
@ssh_connection.send_global_request("cancel-tcpip-forward", :string, host, :long, port) do |success, response|
|
|
if success
|
|
key = [host, port]
|
|
@server_channels.delete(key)
|
|
print_status("Reverse listener deleted")
|
|
else
|
|
print_error("Could not stop reverse listener on #{host}:#{port}")
|
|
end
|
|
completed_event.set
|
|
end
|
|
timeout = 5 # seconds
|
|
begin
|
|
completed_event.wait(timeout)
|
|
true
|
|
rescue TimeoutError
|
|
false
|
|
end
|
|
|
|
end
|
|
|
|
def create_client_channel(params)
|
|
msf_channel = nil
|
|
mutex = Mutex.new
|
|
condition = ConditionVariable.new
|
|
opened = false
|
|
ssh_channel = @ssh_connection.open_channel('direct-tcpip', :string, params.peerhost, :long, params.peerport, :string, params.localhost, :long, params.localport) do |_|
|
|
dlog("new direct-tcpip channel opened to #{Rex::Socket.is_ipv6?(params.peerhost) ? '[' + params.peerhost + ']' : params.peerhost}:#{params.peerport}")
|
|
opened = true
|
|
mutex.synchronize do
|
|
condition.signal
|
|
end
|
|
end
|
|
failure_reason_code = nil
|
|
ssh_channel.on_open_failed do |_ch, code, desc|
|
|
failure_reason_code = code
|
|
wlog("failed to open SSH channel (code: #{code.inspect}, description: #{desc.inspect})")
|
|
mutex.synchronize do
|
|
condition.signal
|
|
end
|
|
end
|
|
|
|
mutex.synchronize do
|
|
timeout = params.timeout.to_i <= 0 ? nil : params.timeout
|
|
condition.wait(mutex, timeout)
|
|
end
|
|
|
|
unless opened
|
|
ssh_channel.close
|
|
|
|
raise ::Rex::ConnectionTimeout.new(params.peerhost, params.peerport) if failure_reason_code.nil?
|
|
|
|
case failure_reason_code
|
|
when ChannelFailureReason::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED
|
|
reason = 'The SSH channel request was administratively prohibited.'
|
|
when ChannelFailureReason::SSH_OPEN_UNKNOWN_CHANNEL_TYPE
|
|
reason = 'The SSH channel type is not supported.'
|
|
when ChannelFailureReason::SSH_OPEN_RESOURCE_SHORTAGE
|
|
reason = 'The SSH channel request was denied because of a resource shortage.'
|
|
end
|
|
|
|
raise ::Rex::ConnectionError.new(params.peerhost, params.peerport, reason: reason)
|
|
end
|
|
msf_channel = TcpClientChannel.new(self, @channel_ticker += 1, ssh_channel, params)
|
|
sock = msf_channel.lsock
|
|
|
|
# Notify now that we've created the socket
|
|
notify_socket_created(self, sock, params)
|
|
|
|
sock
|
|
end
|
|
|
|
# The SSH server has told us that there's a port forwarding request.
|
|
# Find the relevant server channel and inform it.
|
|
def on_got_remote_connection(session, channel, packet)
|
|
connected_address = packet.read_string
|
|
connected_port = packet.read_long
|
|
originator_address = packet.read_string
|
|
originator_port = packet.read_long
|
|
ilog("Received connection: #{connected_address}:#{connected_port} <--> #{originator_address}:#{originator_port}")
|
|
# Find the correct TcpServerChannel
|
|
#
|
|
key = [connected_address, connected_port]
|
|
server_channel = @server_channels[key]
|
|
server_channel.create(@channel_ticker += 1, channel)
|
|
end
|
|
|
|
def cleanup
|
|
channels.each_value(&:close)
|
|
@server_channels.each_value(&:close)
|
|
|
|
super
|
|
end
|
|
|
|
attr_reader :sock, :ssh_connection
|
|
|
|
end
|
|
end
|