From fdfecdc8ff1f2ab120e551fcbab937dd70339a67 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 14 Sep 2021 16:40:58 +1000 Subject: [PATCH] Initial work on reverse pivoting through SSH --- .../base/sessions/ssh_command_shell_bind.rb | 152 ++++++++++++++++-- 1 file changed, 137 insertions(+), 15 deletions(-) diff --git a/lib/msf/base/sessions/ssh_command_shell_bind.rb b/lib/msf/base/sessions/ssh_command_shell_bind.rb index 41ec5ccdc9..d7ab60424e 100644 --- a/lib/msf/base/sessions/ssh_command_shell_bind.rb +++ b/lib/msf/base/sessions/ssh_command_shell_bind.rb @@ -138,6 +138,59 @@ module Msf::Sessions 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 + 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 close + @client.stop_server_channel(@host, @port) + 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. @@ -148,11 +201,15 @@ module Msf::Sessions 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 @@ -172,29 +229,79 @@ module Msf::Sessions # Notify handlers before we create the socket notify_before_socket_create(self, params) - mutex = Mutex.new - condition = ConditionVariable.new ssh_channel = msf_channel = nil - opened = false if params.proto == 'tcp' - if params.server - raise ::Rex::BindFailed.new(params.localhost, params.localport, reason: 'TCP server sockets are not supported by SSH sessions.') - end - - 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 + if params.server + sock = create_server_channel(params) + else + sock = create_client_channel(params) end - 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 if ssh_channel.nil? + 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) + keep_trying = true + msf_channel = nil + @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 + keep_trying = false + end + @ssh_connection.loop(params.timeout) { + keep_trying + } + + # Return the server channel itself + sock = msf_channel + end + + def stop_server_channel(host, port) + keep_trying = true + @ssh_connection.send_global_request("cancel-tcpip-forward", :string, host, :long, port) do |success, response| + if success + key = [host, port] + @server_channels.delete(key) + keep_trying = false + else + keep_trying = false + raise Rex::ConnectionError, "Could not stop reverse listener on #{host}:#{port}" + end + end + timeout = 5 # seconds + @ssh_connection.loop(timeout) { + keep_trying + } + end + + def create_client_channel(params) + 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 @@ -225,7 +332,6 @@ module Msf::Sessions 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 @@ -235,8 +341,24 @@ module Msf::Sessions 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) + end + def cleanup channels.each_value(&:close) + server_channels.each_value(&:cleanup) super end