Land #15831, add more ssh session support
This commit is contained in:
commit
184795513f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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('')
|
||||
|
||||
|
|
|
@ -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('')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue