Land #15831, add more ssh session support

This commit is contained in:
space-r7 2021-12-16 15:39:55 -06:00
commit 184795513f
No known key found for this signature in database
GPG Key ID: DE80BD86F1B96C84
11 changed files with 269 additions and 122 deletions

View File

@ -1,5 +1,6 @@
require 'net/ssh'
require 'metasploit/framework/login_scanner/base'
require 'metasploit/framework/ssh/platform'
require 'rex/socket/ssh_factory'
module Metasploit
@ -110,80 +111,10 @@ module Metasploit
private
# This method attempts to gather proof that we successfuly logged in.
# This method attempts to gather proof that we successfully logged in.
# @return [String] The proof of a connection, May be empty.
def gather_proof
proof = ''
begin
Timeout.timeout(10) do
proof = ssh_socket.exec!("id\n").to_s
if (proof =~ /id=/)
proof << ssh_socket.exec!("uname -a\n").to_s
if (proof =~ /JUNOS /)
# We're in the SSH shell for a Juniper JunOS, we can pull the version from the cli
# line 2 is hostname, 3 is model, 4 is the Base OS version
proof = ssh_socket.exec!("cli show version\n").split("\n")[2..4].join(", ").to_s
elsif (proof =~ /Linux USG /)
# Ubiquiti Unifi USG
proof << ssh_socket.exec!("cat /etc/version\n").to_s.rstrip
end
temp_proof = ssh_socket.exec!("grep unifi.version /tmp/system.cfg\n").to_s.rstrip
if (temp_proof =~ /unifi\.version/)
proof << temp_proof
# Ubiquiti Unifi device (non-USG), possibly a switch. Tested on US-24, UAP-nanoHD
# The /tmp/*.cfg files don't give us device info, however the info command does
# we dont call it originally since it doesnt say unifi/ubiquiti in it and info
# is a linux command as well
proof << ssh_socket.exec!("grep board.name /etc/board.info\n").to_s.rstrip
end
else
# Cisco IOS
if proof =~ /Unknown command or computer name/
proof = ssh_socket.exec!("ver\n").to_s
# Juniper ScreenOS
elsif proof =~ /unknown keyword/
proof = ssh_socket.exec!("get chassis\n").to_s
# Juniper JunOS CLI
elsif proof =~ /unknown command: id/
proof = ssh_socket.exec!("show version\n").split("\n")[2..4].join(", ").to_s
# Brocade CLI
elsif proof =~ /Invalid input -> id/ || proof =~ /Protocol error, doesn't start with scp\!/
proof = ssh_socket.exec!("show version\n").to_s
if proof =~ /Version:(?<os_version>.+).+HW: (?<hardware>)/mi
proof = "Model: #{hardware}, OS: #{os_version}"
end
# Arista
elsif proof =~ /% Invalid input at line 1/
proof = ssh_socket.exec!("show version\n").split("\n")[0..1]
proof = proof.map {|item| item.strip}
proof = proof.join(", ").to_s
# Windows
elsif proof =~ /command not found|is not recognized as an internal or external command/
proof = ssh_socket.exec!("systeminfo\n").to_s
/OS Name:\s+(?<os_name>.+)$/ =~ proof
/OS Version:\s+(?<os_num>.+)$/ =~ proof
if os_num.present? && os_name.present?
proof = "#{os_name.strip} #{os_num.strip}"
else
proof = ssh_socket.exec!("ver\n").to_s.strip
end
# mikrotik
elsif proof =~ /bad command name id \(line 1 column 1\)/
proof = ssh_socket.exec!("/ system resource print\n").to_s
/platform:\s+(?<platform>.+)$/ =~ proof
/board-name:\s+(?<board>.+)$/ =~ proof
/version:\s+(?<version>.+)$/ =~ proof
if version && platform && board
proof = "#{platform.strip} #{board.strip} #{version.strip}"
end
else
proof << ssh_socket.exec!("help\n?\n\n\n").to_s
end
end
end
rescue ::Exception
end
proof
Metasploit::Framework::Ssh::Platform.get_platform_info(ssh_socket)
end
def set_sane_defaults
@ -195,40 +126,9 @@ module Metasploit
public
def get_platform(proof)
case proof
when /unifi\.version|UniFiSecurityGateway/ #Ubiquiti Unifi. uname -a is left in, so we got to pull before Linux
'unifi'
when /Linux/
'linux'
when /Darwin/
'osx'
when /SunOS/
'solaris'
when /BSD/
'bsd'
when /HP-UX/
'hpux'
when /AIX/
'aix'
when /cygwin|Win32|Windows|Microsoft/
'windows'
when /Unknown command or computer name|Line has invalid autocommand/
'cisco-ios'
when /unknown keyword/ # ScreenOS
'juniper'
when /JUNOS Base OS/ # JunOS
'juniper'
when /MikroTik/
'mikrotik'
when /Arista/
'arista'
else
'unknown'
end
Metasploit::Framework::Ssh::Platform.get_platform_from_info(proof)
end
end
end
end
end

View File

@ -0,0 +1,121 @@
module Metasploit
module Framework
module Ssh
module Platform
def self.get_platform(ssh_socket)
info = get_platform_info(ssh_socket, timeout: 10)
get_platform_from_info(info)
end
def self.get_platform_info(ssh_socket, timeout: 10)
info = ''
begin
Timeout.timeout(timeout) do
info = ssh_socket.exec!("id\n").to_s
if (info =~ /id=/)
info << ssh_socket.exec!("uname -a\n").to_s
if (info =~ /JUNOS /)
# We're in the SSH shell for a Juniper JunOS, we can pull the version from the cli
# line 2 is hostname, 3 is model, 4 is the Base OS version
info = ssh_socket.exec!("cli show version\n").split("\n")[2..4].join(", ").to_s
elsif (info =~ /Linux USG /)
# Ubiquiti Unifi USG
info << ssh_socket.exec!("cat /etc/version\n").to_s.rstrip
end
temp_proof = ssh_socket.exec!("grep unifi.version /tmp/system.cfg\n").to_s.rstrip
if (temp_proof =~ /unifi\.version/)
info << temp_proof
# Ubiquiti Unifi device (non-USG), possibly a switch. Tested on US-24, UAP-nanoHD
# The /tmp/*.cfg files don't give us device info, however the info command does
# we dont call it originally since it doesnt say unifi/ubiquiti in it and info
# is a linux command as well
info << ssh_socket.exec!("grep board.name /etc/board.info\n").to_s.rstrip
end
else
# Cisco IOS
if info =~ /Unknown command or computer name/
info = ssh_socket.exec!("ver\n").to_s
# Juniper ScreenOS
elsif info =~ /unknown keyword/
info = ssh_socket.exec!("get chassis\n").to_s
# Juniper JunOS CLI
elsif info =~ /unknown command: id/
info = ssh_socket.exec!("show version\n").split("\n")[2..4].join(", ").to_s
# Brocade CLI
elsif info =~ /Invalid input -> id/ || info =~ /Protocol error, doesn't start with scp\!/
info = ssh_socket.exec!("show version\n").to_s
if info =~ /Version:(?<os_version>.+).+HW: (?<hardware>)/mi
info = "Model: #{hardware}, OS: #{os_version}"
end
# Arista
elsif info =~ /% Invalid input at line 1/
info = ssh_socket.exec!("show version\n").split("\n")[0..1]
info = info.map {|item| item.strip}
info = info.join(", ").to_s
# Windows
elsif info =~ /command not found|is not recognized as an internal or external command/
info = ssh_socket.exec!("systeminfo\n").to_s
/OS Name:\s+(?<os_name>.+)$/ =~ info
/OS Version:\s+(?<os_num>.+)$/ =~ info
if os_num.present? && os_name.present?
info = "#{os_name.strip} #{os_num.strip}"
else
info = ssh_socket.exec!("ver\n").to_s.strip
end
# mikrotik
elsif info =~ /bad command name id \(line 1 column 1\)/
info = ssh_socket.exec!("/ system resource print\n").to_s
/platform:\s+(?<platform>.+)$/ =~ info
/board-name:\s+(?<board>.+)$/ =~ info
/version:\s+(?<version>.+)$/ =~ info
if version && platform && board
info = "#{platform.strip} #{board.strip} #{version.strip}"
end
else
info << ssh_socket.exec!("help\n?\n\n\n").to_s
end
end
end
rescue Timeout::Error
end
info
end
def self.get_platform_from_info(info)
case info
when /unifi\.version|UniFiSecurityGateway/ #Ubiquiti Unifi. uname -a is left in, so we got to pull before Linux
'unifi'
when /Linux/
'linux'
when /Darwin/
'osx'
when /SunOS/
'solaris'
when /BSD/
'bsd'
when /HP-UX/
'hpux'
when /AIX/
'aix'
when /cygwin|Win32|Windows|Microsoft/
'windows'
when /Unknown command or computer name|Line has invalid autocommand/
'cisco-ios'
when /unknown keyword/ # ScreenOS
'juniper'
when /JUNOS Base OS/ # JunOS
'juniper'
when /MikroTik/
'mikrotik'
when /Arista/
'arista'
else
'unknown'
end
end
end
end
end
end

View File

@ -111,13 +111,11 @@ class CommandShell
banner.gsub!(/[^[:print:][:space:]]+/n, "_")
banner.strip!
banner = %Q{
session_info = @banner = %Q{
Shell Banner:
#{banner}
-----
}
session_info = banner
end
end
@ -790,6 +788,7 @@ Shell Banner:
attr_accessor :arch
attr_accessor :platform
attr_accessor :max_threads
attr_reader :banner
protected
@ -813,7 +812,7 @@ protected
# Displays +info+ on all session startups
# +info+ is set to the shell banner and initial prompt in the +bootstrap+ method
user_output.print("#{self.info}\n") if (self.info && !self.info.empty?) && self.interacting
user_output.print("#{@banner}\n") if !@banner.blank? && self.interacting
run_single('')

View File

@ -32,13 +32,20 @@ module CommandShellOptions
# Configure input/output to match the payload
session.user_input = self.user_input if self.user_input
session.user_output = self.user_output if self.user_output
platform = nil
if self.platform and self.platform.kind_of? Msf::Module::PlatformList
session.platform = self.platform.platforms.first.realname.downcase
platform = self.platform.platforms.first.realname.downcase
end
if self.platform and self.platform.kind_of? Msf::Module::Platform
session.platform = self.platform.realname.downcase
platform = self.platform.realname.downcase
end
# a blank platform is *all* platforms and used by the generic modules, in that case only set this instance if it was
# not previously set to a more specific value through some means
session.platform = platform unless platform.blank? && !session.platform.blank?
if self.arch
if self.arch.kind_of?(Array)
session.arch = self.arch.join('')

View File

@ -1,5 +1,6 @@
# -*- coding: binary -*-
require 'metasploit/framework/ssh/platform'
require 'rex/post/channel'
require 'rex/post/meterpreter/channels/socket_abstraction'
@ -227,11 +228,27 @@ module Msf::Sessions
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)
super(nil, opts)
end
def bootstrap(datastore = {}, handler = nil)
# this won't work after the rstream is initialized, so do it first
@platform = Metasploit::Framework::Ssh::Platform.get_platform(ssh_connection)
# if the platform is known, it was recovered by communicating with the device, so skip verification, also not all
# shells accessed through SSH may respond to the echo command issued for verification as expected
datastore['AutoVerifySession'] &= @platform.blank?
@rstream = Net::SSH::CommandStream.new(ssh_connection).lsock
super
@info = "SSH #{username} @ #{@peer_info}"
end
def desc
"SSH"
end
#
@ -402,6 +419,5 @@ module Msf::Sessions
end
attr_reader :sock, :ssh_connection
end
end

View File

@ -36,20 +36,26 @@ module Auxiliary::CommandShell
obj.sock.extend(CRLFLineEndings)
end
sock ||= obj.sock
sock ||= obj.respond_to?(:sock) ? obj.sock : nil
sess ||= Msf::Sessions::CommandShell.new(sock)
sess.set_from_exploit(obj)
sess.info = info
# Clean up the stored data
sess.exploit_datastore.merge!(ds_merge)
# Prevent the socket from being closed
obj.sockets.delete(sock)
obj.sock = nil if obj.respond_to? :sock
obj.sockets.delete(sock) if sock
obj.sock = nil if obj.respond_to?(:sock)
framework.sessions.register(sess)
if sess.respond_to?(:bootstrap)
sess.bootstrap(datastore)
return unless sess.alive
end
sess.process_autoruns(datastore)
sess.info = info unless info.blank?
# Notify the framework that we have a new session opening up...
# Don't let errant event handlers kill our session

View File

@ -0,0 +1,42 @@
# -*- coding: binary -*-
module Msf
module Handler
module Generic
include Msf::Handler
#
# Returns the handler type of none since payloads that use this handler
# have no connection.
#
def self.handler_type
'none'
end
#
# Returns none to indicate no connection.
#
def self.general_handler_type
'none'
end
# This is necessary for find-sock style payloads.
#
def handler(*args)
create_session(*args)
Claimed
end
#
# Always wait at least 5 seconds for this payload (due to channel delays)
#
def wfs_delay
datastore['WfsDelay'] > 4 ? datastore['WfsDelay'] : 5
end
end
end
end

View File

@ -70,8 +70,7 @@ class MetasploitModule < Msf::Auxiliary
'USERNAME' => result.credential.public,
'PASSWORD' => result.credential.private
}
info = "#{proto_from_fullname} #{result.credential} (#{ Rex::Socket.is_ipv6?(@ip) ? '[' + @ip + ']' : @ip }:#{rport})"
s = start_session(self, info, merge_me, false, sess.rstream, sess)
s = start_session(self, nil, merge_me, false, sess.rstream, sess)
self.sockets.delete(scanner.ssh_socket.transport.socket)
# Set the session platform

View File

@ -91,8 +91,7 @@ class MetasploitModule < Msf::Auxiliary
'KEY_PATH' => nil
}
info = "#{proto_from_fullname} #{result.credential.public}:#{fingerprint} (#{ Rex::Socket.is_ipv6?(ip) ? '[' + ip + ']' : ip }:#{rport})"
s = start_session(self, info, merge_me, false, sess.rstream, sess)
s = start_session(self, nil, merge_me, false, sess.rstream, sess)
self.sockets.delete(scanner.ssh_socket.transport.socket)
# Set the session platform

View File

@ -130,6 +130,20 @@ class MetasploitModule < Msf::Exploit::Remote
'Arch' => ARCH_CMD,
'Platform' => 'unix'
}
],
[
'Interactive SSH',
{
'DefaultOptions' => {
'PAYLOAD' => 'generic/ssh/interact',
'WfsDelay' => 5
},
'Payload' => {
'Compat' => {
'PayloadType' => 'ssh_interact',
}
}
}
]
],
'DefaultTarget' => 0,
@ -214,6 +228,12 @@ class MetasploitModule < Msf::Exploit::Remote
def exploit
do_login(datastore['RHOST'], datastore['USERNAME'], datastore['PASSWORD'], datastore['RPORT'])
if target.name == 'Interactive SSH'
handler(ssh_socket)
return
end
print_status("#{datastore['RHOST']}:#{datastore['RPORT']} - Sending stager...")
case target['Platform']

View File

@ -0,0 +1,38 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
module MetasploitModule
CachedSize = 0
include Msf::Payload::Single
include Msf::Sessions::CommandShellOptions
def initialize(info = {})
super(
merge_info(
info,
'Name' => 'Interact with Established SSH Connection',
'Description' => 'Interacts with a shell on an established SSH connection',
'Author' => 'Spencer McIntyre',
'License' => MSF_LICENSE,
'Platform' => '',
'Arch' => ARCH_ALL,
'Handler' => Msf::Handler::Generic,
'Session' => Msf::Sessions::SshCommandShellBind,
'PayloadType' => 'ssh_interact',
'Payload' => {
'Offsets' => {},
'Payload' => ''
}
)
)
end
def on_session(session)
super
session.arch.clear # undo the ARCH_ALL amalgamation
end
end