Adds MySQL session type
This commit is contained in:
parent
48221e594d
commit
0e9cad6d45
|
@ -15,6 +15,10 @@ module Metasploit
|
||||||
include Metasploit::Framework::LoginScanner::RexSocket
|
include Metasploit::Framework::LoginScanner::RexSocket
|
||||||
include Metasploit::Framework::Tcp::Client
|
include Metasploit::Framework::Tcp::Client
|
||||||
|
|
||||||
|
# @returns [Boolean] If a login is successful and this attribute is true - a MySQL::Client instance is used as proof,
|
||||||
|
# and the socket is not immediately closed
|
||||||
|
attr_accessor :use_client_as_proof
|
||||||
|
|
||||||
DEFAULT_PORT = 3306
|
DEFAULT_PORT = 3306
|
||||||
LIKELY_PORTS = [3306]
|
LIKELY_PORTS = [3306]
|
||||||
LIKELY_SERVICE_NAMES = ['mysql']
|
LIKELY_SERVICE_NAMES = ['mysql']
|
||||||
|
@ -35,7 +39,7 @@ module Metasploit
|
||||||
disconnect if self.sock
|
disconnect if self.sock
|
||||||
connect
|
connect
|
||||||
|
|
||||||
::Mysql.connect(host, credential.public, credential.private, '', port, sock)
|
mysql_conn = ::Mysql.connect(host, credential.public, credential.private, '', port, sock)
|
||||||
|
|
||||||
rescue ::SystemCallError, Rex::ConnectionError => e
|
rescue ::SystemCallError, Rex::ConnectionError => e
|
||||||
result_options.merge!({
|
result_options.merge!({
|
||||||
|
@ -64,8 +68,17 @@ module Metasploit
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
unless result_options[:status]
|
if mysql_conn
|
||||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||||
|
|
||||||
|
# This module no long owns the socket, return it as proof so the calling context can perform additional operations
|
||||||
|
# Additionally assign values to nil to avoid closing the socket etc automatically
|
||||||
|
if use_client_as_proof
|
||||||
|
result_options[:proof] = mysql_conn
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
mysql_conn.close
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||||
|
|
|
@ -228,6 +228,13 @@ class Config < Hash
|
||||||
self.new.postgresql_session_history
|
self.new.postgresql_session_history
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the full path to the MySQL session history file.
|
||||||
|
#
|
||||||
|
# @return [String] path to the history file.
|
||||||
|
def self.mysql_session_history
|
||||||
|
self.new.mysql_session_history
|
||||||
|
end
|
||||||
|
|
||||||
def self.pry_history
|
def self.pry_history
|
||||||
self.new.pry_history
|
self.new.pry_history
|
||||||
end
|
end
|
||||||
|
@ -341,6 +348,10 @@ class Config < Hash
|
||||||
config_directory + FileSep + "postgresql_session_history"
|
config_directory + FileSep + "postgresql_session_history"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mysql_session_history
|
||||||
|
config_directory + FileSep + "mysql_session_history"
|
||||||
|
end
|
||||||
|
|
||||||
def pry_history
|
def pry_history
|
||||||
config_directory + FileSep + "pry_history"
|
config_directory + FileSep + "pry_history"
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'rex/post/mysql'
|
||||||
|
|
||||||
|
class Msf::Sessions::MySQL
|
||||||
|
|
||||||
|
# This interface supports basic interaction.
|
||||||
|
include Msf::Session::Basic
|
||||||
|
include Msf::Sessions::Scriptable
|
||||||
|
|
||||||
|
# @return [Rex::Post::MySQL::Ui::Console] The interactive console
|
||||||
|
attr_accessor :console
|
||||||
|
# @return [MySQL::Client]
|
||||||
|
attr_accessor :client
|
||||||
|
attr_accessor :platform, :arch
|
||||||
|
|
||||||
|
# @param[Rex::IO::Stream] rstream
|
||||||
|
# @param [Hash] opts
|
||||||
|
def initialize(rstream, opts = {})
|
||||||
|
@client = opts.fetch(:client)
|
||||||
|
self.console = ::Rex::Post::MySQL::Ui::Console.new(self)
|
||||||
|
super(rstream, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Hash] datastore
|
||||||
|
# @param [nil] handler
|
||||||
|
# @return [String]
|
||||||
|
def bootstrap(datastore = {}, handler = nil)
|
||||||
|
session = self
|
||||||
|
session.init_ui(user_input, user_output)
|
||||||
|
|
||||||
|
@info = "MySQL #{datastore['USERNAME']} @ #{client.socket.peerinfo}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_autoruns(datastore)
|
||||||
|
['InitialAutoRunScript', 'AutoRunScript'].each do |key|
|
||||||
|
next if datastore[key].nil? || datastore[key].empty?
|
||||||
|
|
||||||
|
args = Shellwords.shellwords(datastore[key])
|
||||||
|
print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'")
|
||||||
|
execute_script(args.shift, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def type
|
||||||
|
self.class.type
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String] The type of the session
|
||||||
|
def self.type
|
||||||
|
'MySQL'
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] Can the session clean up after itself
|
||||||
|
def self.can_cleanup_files
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String] The session description
|
||||||
|
def desc
|
||||||
|
'MySQL'
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object] The peer address
|
||||||
|
def address
|
||||||
|
return @address if @address
|
||||||
|
|
||||||
|
@address, @port = @client.socket.peerinfo.split(':')
|
||||||
|
@address
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object] The peer host
|
||||||
|
def port
|
||||||
|
return @port if @port
|
||||||
|
|
||||||
|
@address, @port = @client.socket.peerinfo.split(':')
|
||||||
|
@port
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initializes the console's I/O handles.
|
||||||
|
#
|
||||||
|
# @param [Object] input
|
||||||
|
# @param [Object] output
|
||||||
|
# @return [String]
|
||||||
|
def init_ui(input, output)
|
||||||
|
super(input, output)
|
||||||
|
|
||||||
|
console.init_ui(input, output)
|
||||||
|
console.set_log_source(log_source)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resets the console's I/O handles.
|
||||||
|
#
|
||||||
|
# @return [Object]
|
||||||
|
def reset_ui
|
||||||
|
console.unset_log_source
|
||||||
|
console.reset_ui
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Exit the console
|
||||||
|
#
|
||||||
|
# @return [TrueClass]
|
||||||
|
def exit
|
||||||
|
console.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Override the basic session interaction to use shell_read and
|
||||||
|
# shell_write instead of operating on rstream directly.
|
||||||
|
#
|
||||||
|
# @return [Object]
|
||||||
|
def _interact
|
||||||
|
framework.events.on_session_interact(self)
|
||||||
|
framework.history_manager.with_context(name: type.to_sym) { _interact_stream }
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object]
|
||||||
|
def _interact_stream
|
||||||
|
framework.events.on_session_interact(self)
|
||||||
|
|
||||||
|
console.framework = framework
|
||||||
|
# Call the console interaction of the mysql client and
|
||||||
|
# pass it a block that returns whether or not we should still be
|
||||||
|
# interacting. This will allow the shell to abort if interaction is
|
||||||
|
# canceled.
|
||||||
|
console.interact { interacting != true }
|
||||||
|
console.framework = nil
|
||||||
|
|
||||||
|
# If the stop flag has been set, then that means the user exited. Raise
|
||||||
|
# the EOFError so we can drop this handle like a bad habit.
|
||||||
|
raise ::EOFError if (console.stopped? == true)
|
||||||
|
end
|
||||||
|
end
|
|
@ -33,6 +33,12 @@ module Exploit::Remote::MYSQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def mysql_login(user='root', pass='', db=nil)
|
def mysql_login(user='root', pass='', db=nil)
|
||||||
|
unless defined?(session).nil? || session.nil?
|
||||||
|
print_status("Using existing session #{session.sid}")
|
||||||
|
@mysql_handle = session.client
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
disconnect if self.sock
|
disconnect if self.sock
|
||||||
connect
|
connect
|
||||||
|
|
||||||
|
@ -56,12 +62,21 @@ module Exploit::Remote::MYSQL
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vprint_good "#{rhost}:#{rport} MySQL - Logged in to '#{db}' with '#{user}':'#{pass}'"
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
def mysql_logoff
|
def mysql_logoff
|
||||||
|
# Don't log out if we are using a session.
|
||||||
|
if defined?(session) && session
|
||||||
|
vprint_status "#{rhost}:#{rport} MySQL - Skipping disconnecting from the session"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
@mysql_handle = nil if @mysql_handle
|
@mysql_handle = nil if @mysql_handle
|
||||||
disconnect if self.sock
|
disconnect if self.sock
|
||||||
|
vprint_status "#{rhost}:#{rport} MySQL - Disconnected"
|
||||||
end
|
end
|
||||||
|
|
||||||
def mysql_login_datastore
|
def mysql_login_datastore
|
||||||
|
@ -85,6 +100,8 @@ module Exploit::Remote::MYSQL
|
||||||
print_error("Timeout: #{e.message}")
|
print_error("Timeout: #{e.message}")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vprint_status "#{rhost}:#{rport} MySQL - querying with '#{sql}'"
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -236,4 +253,3 @@ module Exploit::Remote::MYSQL
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ module Msf
|
||||||
HIERARCHICAL_SEARCH_TABLE = 'hierarchical_search_table'
|
HIERARCHICAL_SEARCH_TABLE = 'hierarchical_search_table'
|
||||||
SMB_SESSION_TYPE = 'smb_session_type'
|
SMB_SESSION_TYPE = 'smb_session_type'
|
||||||
POSTGRESQL_SESSION_TYPE = 'postgresql_session_type'
|
POSTGRESQL_SESSION_TYPE = 'postgresql_session_type'
|
||||||
|
MYSQL_SESSION_TYPE = 'mysql_session_type'
|
||||||
DEFAULTS = [
|
DEFAULTS = [
|
||||||
{
|
{
|
||||||
name: WRAPPED_TABLES,
|
name: WRAPPED_TABLES,
|
||||||
|
@ -74,6 +75,12 @@ module Msf
|
||||||
name: POSTGRESQL_SESSION_TYPE,
|
name: POSTGRESQL_SESSION_TYPE,
|
||||||
description: 'When enabled will allow for the creation/use of PostgreSQL sessions',
|
description: 'When enabled will allow for the creation/use of PostgreSQL sessions',
|
||||||
requires_restart: true,
|
requires_restart: true,
|
||||||
|
default_value: false,
|
||||||
|
}.freeze,
|
||||||
|
{
|
||||||
|
name: MYSQL_SESSION_TYPE,
|
||||||
|
description: 'When enabled will allow for the creation/use of MySQL sessions',
|
||||||
|
requires_restart: true,
|
||||||
default_value: false
|
default_value: false
|
||||||
}.freeze,
|
}.freeze,
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,6 +9,7 @@ module Msf::OptionalSession
|
||||||
|
|
||||||
def initialize(info = {})
|
def initialize(info = {})
|
||||||
super
|
super
|
||||||
|
|
||||||
if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
|
if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
|
||||||
register_options(
|
register_options(
|
||||||
[
|
[
|
||||||
|
@ -20,6 +21,16 @@ module Msf::OptionalSession
|
||||||
add_info('New in Metasploit 6.4 - This module can target a %grnSESSION%clr or an %grnRHOST%clr')
|
add_info('New in Metasploit 6.4 - This module can target a %grnSESSION%clr or an %grnRHOST%clr')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
|
||||||
|
register_options(
|
||||||
|
[
|
||||||
|
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
|
||||||
|
Msf::Opt::RHOST(nil, false),
|
||||||
|
Msf::Opt::RPORT(nil, false)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
if framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)
|
if framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)
|
||||||
register_options(
|
register_options(
|
||||||
[
|
[
|
||||||
|
@ -35,7 +46,7 @@ module Msf::OptionalSession
|
||||||
end
|
end
|
||||||
|
|
||||||
def session
|
def session
|
||||||
return nil unless (framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE))
|
return nil unless (framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE))
|
||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,7 @@ module Msf::Post::Common
|
||||||
session.sock.peerhost
|
session.sock.peerhost
|
||||||
when 'shell', 'powershell'
|
when 'shell', 'powershell'
|
||||||
session.session_host
|
session.session_host
|
||||||
when 'postgresql'
|
when 'postgresql', 'mysql'
|
||||||
session.address
|
session.address
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
|
@ -45,7 +45,7 @@ module Msf::Post::Common
|
||||||
session.sock.peerport
|
session.sock.peerport
|
||||||
when 'shell', 'powershell'
|
when 'shell', 'powershell'
|
||||||
session.session_port
|
session.session_port
|
||||||
when 'postgresql'
|
when 'postgresql', 'mysql'
|
||||||
session.port
|
session.port
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
|
|
|
@ -41,6 +41,8 @@ class MsfAutoload
|
||||||
'Http'
|
'Http'
|
||||||
elsif basename == 'rftransceiver' && abspath.end_with?("#{__dir__}/rex/post/hwbridge/ui/console/command_dispatcher/rftransceiver.rb")
|
elsif basename == 'rftransceiver' && abspath.end_with?("#{__dir__}/rex/post/hwbridge/ui/console/command_dispatcher/rftransceiver.rb")
|
||||||
'RFtransceiver'
|
'RFtransceiver'
|
||||||
|
elsif basename == 'mysql' && abspath.end_with?("#{__dir__}/msf/base/sessions/mysql.rb")
|
||||||
|
'MySQL'
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
require 'rex/post/meterpreter'
|
require 'rex/post/meterpreter'
|
||||||
require 'rex/post/smb'
|
require 'rex/post/smb'
|
||||||
require 'rex/post/postgresql'
|
require 'rex/post/postgresql'
|
||||||
|
require 'rex/post/mysql'
|
||||||
|
|
||||||
module Rex::Post
|
module Rex::Post
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'rex/post/mysql/ui'
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'rex/post/mysql/ui/console'
|
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Post
|
||||||
|
module MySQL
|
||||||
|
module Ui
|
||||||
|
|
||||||
|
# This class provides a shell driven interface to the MySQL client API.
|
||||||
|
class Console
|
||||||
|
include Rex::Ui::Text::DispatcherShell
|
||||||
|
|
||||||
|
# Dispatchers
|
||||||
|
require 'rex/post/mysql/ui/console/command_dispatcher'
|
||||||
|
require 'rex/post/mysql/ui/console/command_dispatcher/core'
|
||||||
|
require 'rex/post/mysql/ui/console/command_dispatcher/client'
|
||||||
|
require 'rex/post/mysql/ui/console/command_dispatcher/modules'
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize the MySQL console.
|
||||||
|
#
|
||||||
|
# @param [Msf::Sessions::MySQL] session
|
||||||
|
def initialize(session)
|
||||||
|
# The mysql client context
|
||||||
|
self.session = session
|
||||||
|
self.client = session.client
|
||||||
|
self.cwd = client.database
|
||||||
|
prompt = "%undMySQL @ #{client.socket.peerinfo} (#{cwd})%clr"
|
||||||
|
history_manager = Msf::Config.mysql_session_history
|
||||||
|
super(prompt, '>', history_manager, nil, :mysql)
|
||||||
|
|
||||||
|
# Queued commands array
|
||||||
|
self.commands = []
|
||||||
|
|
||||||
|
# Point the input/output handles elsewhere
|
||||||
|
reset_ui
|
||||||
|
|
||||||
|
enstack_dispatcher(::Rex::Post::MySQL::Ui::Console::CommandDispatcher::Core)
|
||||||
|
enstack_dispatcher(::Rex::Post::MySQL::Ui::Console::CommandDispatcher::Client)
|
||||||
|
enstack_dispatcher(::Rex::Post::MySQL::Ui::Console::CommandDispatcher::Modules)
|
||||||
|
|
||||||
|
# Set up logging to whatever logsink 'core' is using
|
||||||
|
if ! $dispatcher['mysql']
|
||||||
|
$dispatcher['mysql'] = $dispatcher['core']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Called when someone wants to interact with the mysql client. It's
|
||||||
|
# assumed that init_ui has been called prior.
|
||||||
|
#
|
||||||
|
# @param [Proc] block
|
||||||
|
# @return [Integer]
|
||||||
|
def interact(&block)
|
||||||
|
# Run queued commands
|
||||||
|
commands.delete_if do |ent|
|
||||||
|
run_single(ent)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run the interactive loop
|
||||||
|
run do |line|
|
||||||
|
# Run the command
|
||||||
|
run_single(line)
|
||||||
|
|
||||||
|
# If a block was supplied, call it, otherwise return false
|
||||||
|
if block
|
||||||
|
block.call
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Queues a command to be run when the interactive loop is entered.
|
||||||
|
#
|
||||||
|
# @param [Object] cmd
|
||||||
|
# @return [Object]
|
||||||
|
def queue_cmd(cmd)
|
||||||
|
self.commands << cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
# Runs the specified command wrapper in something to catch meterpreter
|
||||||
|
# exceptions.
|
||||||
|
#
|
||||||
|
# @param [Object] dispatcher
|
||||||
|
# @param [Object] method
|
||||||
|
# @param [Object] arguments
|
||||||
|
# @return [FalseClass]
|
||||||
|
def run_command(dispatcher, method, arguments)
|
||||||
|
begin
|
||||||
|
super
|
||||||
|
rescue ::Timeout::Error
|
||||||
|
log_error('Operation timed out.')
|
||||||
|
rescue ::Rex::InvalidDestination => e
|
||||||
|
log_error(e.message)
|
||||||
|
rescue ::Errno::EPIPE, ::OpenSSL::SSL::SSLError, ::IOError
|
||||||
|
self.session.kill
|
||||||
|
rescue ::StandardError => e
|
||||||
|
log_error("Error running command #{method}: #{e.class} #{e}")
|
||||||
|
elog(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Logs that an error occurred and persists the callstack.
|
||||||
|
#
|
||||||
|
# @param [Object] msg
|
||||||
|
# @return [Object]
|
||||||
|
def log_error(msg)
|
||||||
|
print_error(msg)
|
||||||
|
|
||||||
|
elog(msg, 'mysql')
|
||||||
|
|
||||||
|
dlog("Call stack:\n#{$@.join("\n")}", 'mysql')
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Msf::Sessions::MySQL]
|
||||||
|
attr_reader :session
|
||||||
|
|
||||||
|
# @return [MySQL::Client]
|
||||||
|
attr_reader :client
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
attr_accessor :cwd
|
||||||
|
|
||||||
|
# @param [Object] val
|
||||||
|
# @return [String]
|
||||||
|
def format_prompt(val)
|
||||||
|
@cwd ||= client.database
|
||||||
|
prompt = "%undMySQL @ #{client.socket.peerinfo} (#{@cwd})%clr > "
|
||||||
|
substitute_colors(prompt, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_writer :session, :client # :nodoc:
|
||||||
|
attr_accessor :commands # :nodoc:
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,102 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'rex/ui/text/dispatcher_shell'
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Post
|
||||||
|
module MySQL
|
||||||
|
module Ui
|
||||||
|
|
||||||
|
# Base class for all command dispatchers within the MySQL console user interface.
|
||||||
|
module Console::CommandDispatcher
|
||||||
|
include Msf::Ui::Console::CommandDispatcher::Session
|
||||||
|
|
||||||
|
# Initializes an instance of the core command set using the supplied session and client
|
||||||
|
# for interactivity.
|
||||||
|
#
|
||||||
|
# @param [Rex::Post::MySQL::Ui::Console] console
|
||||||
|
def initialize(console)
|
||||||
|
super
|
||||||
|
@msf_loaded = nil
|
||||||
|
@filtered_commands = []
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the MySQL client context.
|
||||||
|
#
|
||||||
|
# @return [MySQL::Client]
|
||||||
|
def client
|
||||||
|
console = shell
|
||||||
|
console.client
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the MySQL session context.
|
||||||
|
#
|
||||||
|
# @return [Msf::Sessions::MySQL]
|
||||||
|
def session
|
||||||
|
console = shell
|
||||||
|
console.session
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the commands that meet the requirements
|
||||||
|
#
|
||||||
|
# @param [Object] all
|
||||||
|
# @param [Object] reqs
|
||||||
|
# @return [Object]
|
||||||
|
def filter_commands(all, reqs)
|
||||||
|
all.delete_if do |cmd, _desc|
|
||||||
|
if reqs[cmd]&.any? { |req| !client.commands.include?(req) }
|
||||||
|
@filtered_commands << cmd
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Object] cmd
|
||||||
|
# @param [Object] line
|
||||||
|
# @return [Symbol, nil]
|
||||||
|
def unknown_command(cmd, line)
|
||||||
|
if @filtered_commands.include?(cmd)
|
||||||
|
print_error("The \"#{cmd}\" command is not supported by this session type (#{session.session_type})")
|
||||||
|
return :handled
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the subdir of the `documentation/` directory that should be used
|
||||||
|
# to find usage documentation
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def docs_dir
|
||||||
|
::File.join(super, 'mysql_session')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the client has a framework object.
|
||||||
|
# Used for firing framework session events
|
||||||
|
#
|
||||||
|
# @return [TrueClass, FalseClass]
|
||||||
|
def msf_loaded?
|
||||||
|
return @msf_loaded unless @msf_loaded.nil?
|
||||||
|
|
||||||
|
# if we get here we must not have initialized yet
|
||||||
|
|
||||||
|
@msf_loaded = !session.framework.nil?
|
||||||
|
@msf_loaded
|
||||||
|
end
|
||||||
|
|
||||||
|
# Log that an error occurred.
|
||||||
|
#
|
||||||
|
# @param [Object] msg
|
||||||
|
# @return [Object]
|
||||||
|
def log_error(msg)
|
||||||
|
print_error(msg)
|
||||||
|
|
||||||
|
elog(msg, 'mysql')
|
||||||
|
|
||||||
|
dlog("Call stack:\n#{$ERROR_POSITION.join("\n")}", 'mysql')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,161 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
require 'reline'
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Post
|
||||||
|
module MySQL
|
||||||
|
module Ui
|
||||||
|
|
||||||
|
# Core MySQL client commands
|
||||||
|
class Console::CommandDispatcher::Client
|
||||||
|
|
||||||
|
include Rex::Post::MySQL::Ui::Console::CommandDispatcher
|
||||||
|
|
||||||
|
# Initializes an instance of the core command set using the supplied console
|
||||||
|
# for interactivity.
|
||||||
|
#
|
||||||
|
# @param [Rex::Post::MySQL::Ui::Console] console
|
||||||
|
def initialize(console)
|
||||||
|
super
|
||||||
|
|
||||||
|
@db_search_results = []
|
||||||
|
end
|
||||||
|
|
||||||
|
# List of supported commands.
|
||||||
|
#
|
||||||
|
# @return [Hash{String->String}]
|
||||||
|
def commands
|
||||||
|
cmds = {
|
||||||
|
'query' => 'Run a raw SQL query',
|
||||||
|
'shell' => 'Enter a raw shell where SQL queries can be executed',
|
||||||
|
}
|
||||||
|
|
||||||
|
reqs = {}
|
||||||
|
|
||||||
|
filter_commands(cmds, reqs)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def name
|
||||||
|
'MySQL Client'
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Object] args
|
||||||
|
# @return [FalseClass, TrueClass]
|
||||||
|
def help_args?(args)
|
||||||
|
return false unless args.instance_of?(::Array)
|
||||||
|
|
||||||
|
args.include?('-h') || args.include?('--help')
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object]
|
||||||
|
def cmd_shell_help
|
||||||
|
print_line 'Usage: shell'
|
||||||
|
print_line
|
||||||
|
print_line 'Go into a raw SQL shell where SQL queries can be executed.'
|
||||||
|
print_line 'To exit, type `exit`, `quit`, `end` or `stop`.'
|
||||||
|
print_line
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Array] args
|
||||||
|
# @return [Object]
|
||||||
|
def cmd_shell(*args)
|
||||||
|
if help_args?(args)
|
||||||
|
cmd_shell_help
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
stop_words = %w[stop s exit e end quit q].freeze
|
||||||
|
|
||||||
|
# Allow the user to query the DB in a loop.
|
||||||
|
finished = false
|
||||||
|
until finished
|
||||||
|
begin
|
||||||
|
# This needs to be here, otherwise the `ensure` block would reset it to the previous
|
||||||
|
# value after a single query, meaning future queries would have the default prompt_block.
|
||||||
|
prompt_proc_before = ::Reline.prompt_proc
|
||||||
|
::Reline.prompt_proc = proc { |line_buffer| line_buffer.each_with_index.map { |_line, i| i > 0 ? 'SQL *> ' : 'SQL >> ' } }
|
||||||
|
|
||||||
|
# This will loop until it receives `true`.
|
||||||
|
raw_query = ::Reline.readmultiline('SQL >> ', use_history = true) do |multiline_input|
|
||||||
|
# In the case only a stop word was input, exit out of the REPL shell
|
||||||
|
finished = multiline_input.split.count == 1 && stop_words.include?(multiline_input.split.last)
|
||||||
|
# Accept the input until the current line does not end with '\', similar to a shell
|
||||||
|
finished || multiline_input.split.empty? || !multiline_input.split.last&.end_with?('\\')
|
||||||
|
end
|
||||||
|
rescue ::Interrupt => _e
|
||||||
|
finished = true
|
||||||
|
ensure
|
||||||
|
::Reline.prompt_proc = prompt_proc_before
|
||||||
|
end
|
||||||
|
|
||||||
|
if finished
|
||||||
|
print_status 'Exiting Shell mode.'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
formatted_query = process_query(query: raw_query)
|
||||||
|
|
||||||
|
unless formatted_query.empty?
|
||||||
|
print_status "Running SQL Command: '#{formatted_query}'"
|
||||||
|
cmd_query(formatted_query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object]
|
||||||
|
def cmd_query_help
|
||||||
|
print_line 'Usage: query'
|
||||||
|
print_line
|
||||||
|
print_line 'Run a raw SQL query on the target.'
|
||||||
|
print_line 'Examples:'
|
||||||
|
print_line "\tquery SHOW DATABASES;"
|
||||||
|
print_line "\tquery USE information_schema;"
|
||||||
|
print_line "\tquery SELECT * FROM SQL_FUNCTIONS;"
|
||||||
|
print_line "\tquery SELECT version();"
|
||||||
|
print_line
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Array] result The result of an SQL query to format.
|
||||||
|
def format_result(result)
|
||||||
|
columns = ['#']
|
||||||
|
|
||||||
|
unless result.is_a?(Array)
|
||||||
|
result.fields.each { |field| columns.append(field.name) }
|
||||||
|
|
||||||
|
::Rex::Text::Table.new(
|
||||||
|
'Header' => 'Query Result',
|
||||||
|
'Indent' => 4,
|
||||||
|
'Columns' => columns,
|
||||||
|
'Rows' => result.map.each.with_index { |row, i| [i, row].flatten }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Array] args SQL query
|
||||||
|
# @return [Object]
|
||||||
|
def cmd_query(*args)
|
||||||
|
cmd_query_help && return if help_args?(args)
|
||||||
|
|
||||||
|
query = args.join(' ').to_s
|
||||||
|
print_status("Sending statement: '#{query}'...")
|
||||||
|
result = client.query(query) || []
|
||||||
|
|
||||||
|
table = format_result(result)
|
||||||
|
print_line(table.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [String] query
|
||||||
|
# @return [String]
|
||||||
|
def process_query(query: '')
|
||||||
|
return '' if query.empty?
|
||||||
|
|
||||||
|
query.lines.each.map { |line| line.chomp("\\\n").strip }.reject(&:empty?).compact.join(' ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Post
|
||||||
|
module MySQL
|
||||||
|
module Ui
|
||||||
|
|
||||||
|
# Core MySQL client commands
|
||||||
|
class Console::CommandDispatcher::Core
|
||||||
|
|
||||||
|
include Rex::Post::MySQL::Ui::Console::CommandDispatcher
|
||||||
|
|
||||||
|
# List of supported commands.
|
||||||
|
#
|
||||||
|
# @return [Hash{String->String}]
|
||||||
|
def commands
|
||||||
|
cmds = {
|
||||||
|
'?' => 'Help menu',
|
||||||
|
'background' => 'Backgrounds the current session',
|
||||||
|
'bg' => 'Alias for background',
|
||||||
|
'exit' => 'Terminate the MySQL session',
|
||||||
|
'help' => 'Help menu',
|
||||||
|
'irb' => 'Open an interactive Ruby shell on the current session',
|
||||||
|
'pry' => 'Open the Pry debugger on the current session',
|
||||||
|
'sessions' => 'Quickly switch to another session',
|
||||||
|
}
|
||||||
|
|
||||||
|
reqs = {}
|
||||||
|
|
||||||
|
filter_commands(cmds, reqs)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def name
|
||||||
|
'Core'
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Object] cmd
|
||||||
|
# @param [Object] line
|
||||||
|
# @return [Symbol, nil]
|
||||||
|
def unknown_command(cmd, line)
|
||||||
|
status = super
|
||||||
|
|
||||||
|
status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,100 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Post
|
||||||
|
module MySQL
|
||||||
|
module Ui
|
||||||
|
|
||||||
|
# MySQL client commands for running modules
|
||||||
|
class Console::CommandDispatcher::Modules
|
||||||
|
|
||||||
|
include Rex::Post::MySQL::Ui::Console::CommandDispatcher
|
||||||
|
|
||||||
|
# List of supported commands.
|
||||||
|
#
|
||||||
|
# @return [Hash{String->String}]
|
||||||
|
def commands
|
||||||
|
cmds = {
|
||||||
|
'run' => 'Run a module'
|
||||||
|
}
|
||||||
|
|
||||||
|
reqs = {}
|
||||||
|
|
||||||
|
filter_commands(cmds, reqs)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Modules
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def name
|
||||||
|
'Modules'
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Object]
|
||||||
|
def cmd_run_help
|
||||||
|
print_line 'Usage: run'
|
||||||
|
print_line
|
||||||
|
print_line 'Run a module or script against the current session.'
|
||||||
|
print_line
|
||||||
|
print_line 'Example:'
|
||||||
|
print_line "\trun auxiliary/admin/mysql/mysql_enum"
|
||||||
|
print_line "\trun my_erb_script.rc"
|
||||||
|
print_line
|
||||||
|
end
|
||||||
|
|
||||||
|
# Executes a module/script in the context of the MySQL session.
|
||||||
|
#
|
||||||
|
# @param [Array] args
|
||||||
|
# @return [TrueClass]
|
||||||
|
def cmd_run(*args)
|
||||||
|
if args.empty? || args.first == '-h' || args.first == '--help'
|
||||||
|
cmd_run_help
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the script name
|
||||||
|
begin
|
||||||
|
script_name = args.shift
|
||||||
|
# First try it as a module if we have access to the Metasploit
|
||||||
|
# Framework instance. If we don't, or if no such module exists,
|
||||||
|
# fall back to using the scripting interface.
|
||||||
|
if msf_loaded? && (mod = session.framework.modules.create(script_name))
|
||||||
|
original_mod = mod
|
||||||
|
reloaded_mod = session.framework.modules.reload_module(original_mod)
|
||||||
|
|
||||||
|
unless reloaded_mod
|
||||||
|
error = session.framework.modules.module_load_error_by_path[original_mod.file_path]
|
||||||
|
print_error("Failed to reload module: #{error}")
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
opts = ''
|
||||||
|
|
||||||
|
opts << (args + [ "SESSION=#{session.sid}" ]).join(',')
|
||||||
|
result = reloaded_mod.run_simple(
|
||||||
|
'LocalInput' => shell.input,
|
||||||
|
'LocalOutput' => shell.output,
|
||||||
|
'OptionStr' => opts
|
||||||
|
)
|
||||||
|
|
||||||
|
print_status("Session #{result.sid} created in the background.") if result.is_a?(Msf::Session)
|
||||||
|
else
|
||||||
|
# the rest of the arguments get passed in through the binding
|
||||||
|
session.execute_script(script_name, args)
|
||||||
|
end
|
||||||
|
rescue Msf::OptionValidateError => e
|
||||||
|
print_error(e.message)
|
||||||
|
elog('Option validation error:', error: e)
|
||||||
|
rescue StandardError => e
|
||||||
|
print_error("Error in script: #{script_name}")
|
||||||
|
elog("Error in script: #{script_name}", error: e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,8 +10,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
include Msf::Exploit::Remote::MYSQL
|
include Msf::Exploit::Remote::MYSQL
|
||||||
include Msf::Auxiliary::Report
|
include Msf::Auxiliary::Report
|
||||||
include Msf::Auxiliary::AuthBrute
|
include Msf::Auxiliary::AuthBrute
|
||||||
|
|
||||||
include Msf::Auxiliary::Scanner
|
include Msf::Auxiliary::Scanner
|
||||||
|
include Msf::Auxiliary::CommandShell
|
||||||
|
|
||||||
def initialize(info = {})
|
def initialize(info = {})
|
||||||
super(update_info(info,
|
super(update_info(info,
|
||||||
|
@ -36,7 +36,20 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
Opt::Proxies
|
Opt::Proxies
|
||||||
])
|
])
|
||||||
|
|
||||||
deregister_options('PASSWORD_SPRAY')
|
options_to_deregister = %w[PASSWORD_SPRAY]
|
||||||
|
unless framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
|
||||||
|
options_to_deregister << 'CreateSession'
|
||||||
|
end
|
||||||
|
deregister_options(*options_to_deregister)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [FalseClass]
|
||||||
|
def create_session?
|
||||||
|
if framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
|
||||||
|
datastore['CreateSession']
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def target
|
def target
|
||||||
|
@ -64,6 +77,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
send_delay: datastore['TCP::send_delay'],
|
send_delay: datastore['TCP::send_delay'],
|
||||||
framework: framework,
|
framework: framework,
|
||||||
framework_module: self,
|
framework_module: self,
|
||||||
|
use_client_as_proof: create_session?,
|
||||||
ssl: datastore['SSL'],
|
ssl: datastore['SSL'],
|
||||||
ssl_version: datastore['SSLVersion'],
|
ssl_version: datastore['SSLVersion'],
|
||||||
ssl_verify_mode: datastore['SSLVerifyMode'],
|
ssl_verify_mode: datastore['SSLVerifyMode'],
|
||||||
|
@ -84,6 +98,17 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
create_credential_login(credential_data)
|
create_credential_login(credential_data)
|
||||||
|
|
||||||
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
||||||
|
|
||||||
|
if create_session?
|
||||||
|
begin
|
||||||
|
mysql_client = result.proof
|
||||||
|
session_setup(result, mysql_client)
|
||||||
|
rescue ::StandardError => e
|
||||||
|
elog('Failed: ', error: e)
|
||||||
|
print_error(e)
|
||||||
|
result.proof.conn.close if result.proof&.conn
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
invalidate_login(credential_data)
|
invalidate_login(credential_data)
|
||||||
vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
|
vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
|
||||||
|
@ -149,6 +174,23 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param [Metasploit::Framework::LoginScanner::Result] result
|
||||||
|
# @param [::Mysql] client
|
||||||
|
# @return [Msf::Sessions::MySQL]
|
||||||
|
def session_setup(result, client)
|
||||||
|
return unless (result && client)
|
||||||
|
|
||||||
|
rstream = client.socket
|
||||||
|
|
||||||
|
my_session = Msf::Sessions::MySQL.new(rstream, { client: client })
|
||||||
|
merging = {
|
||||||
|
'USERPASS_FILE' => nil,
|
||||||
|
'USER_FILE' => nil,
|
||||||
|
'PASS_FILE' => nil,
|
||||||
|
'USERNAME' => result.credential.public,
|
||||||
|
'PASSWORD' => result.credential.private
|
||||||
|
}
|
||||||
|
|
||||||
|
start_session(self, nil, merging, false, my_session.rstream, my_session)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'metasploit/framework/login_scanner/mysql'
|
||||||
RSpec.describe Metasploit::Framework::LoginScanner::MySQL do
|
RSpec.describe Metasploit::Framework::LoginScanner::MySQL do
|
||||||
let(:public) { 'root' }
|
let(:public) { 'root' }
|
||||||
let(:private) { 'toor' }
|
let(:private) { 'toor' }
|
||||||
|
let(:client) { instance_double(::Mysql) }
|
||||||
let(:pub_blank) {
|
let(:pub_blank) {
|
||||||
Metasploit::Framework::Credential.new(
|
Metasploit::Framework::Credential.new(
|
||||||
paired: true,
|
paired: true,
|
||||||
|
@ -28,6 +29,10 @@ RSpec.describe Metasploit::Framework::LoginScanner::MySQL do
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow(client).to receive(:close).and_return(client)
|
||||||
|
end
|
||||||
|
|
||||||
subject(:login_scanner) { described_class.new }
|
subject(:login_scanner) { described_class.new }
|
||||||
|
|
||||||
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false
|
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false
|
||||||
|
@ -37,7 +42,7 @@ RSpec.describe Metasploit::Framework::LoginScanner::MySQL do
|
||||||
|
|
||||||
context 'when the attempt is successful' do
|
context 'when the attempt is successful' do
|
||||||
it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do
|
it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do
|
||||||
expect(::Mysql).to receive(:connect).and_return "fake mysql handle"
|
expect(::Mysql).to receive(:connect).and_return(client)
|
||||||
expect(login_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL
|
expect(login_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'mysql'
|
||||||
|
|
||||||
|
RSpec.describe Msf::Sessions::MySQL do
|
||||||
|
let(:rstream) { instance_double(::Rex::Socket) }
|
||||||
|
let(:client) { instance_double(::Mysql) }
|
||||||
|
let(:opts) { { client: client } }
|
||||||
|
let(:console_class) { Rex::Post::MySQL::Ui::Console }
|
||||||
|
let(:user_input) { instance_double(Rex::Ui::Text::Input::Readline) }
|
||||||
|
let(:user_output) { instance_double(Rex::Ui::Text::Output::Stdio) }
|
||||||
|
let(:name) { 'mysql' }
|
||||||
|
let(:log_source) { "session_#{name}" }
|
||||||
|
let(:type) { 'MySQL' }
|
||||||
|
let(:description) { 'MySQL' }
|
||||||
|
let(:can_cleanup_files) { false }
|
||||||
|
let(:address) { '192.0.2.1' }
|
||||||
|
let(:port) { '3306' }
|
||||||
|
let(:peerinfo) { "#{address}:#{port}" }
|
||||||
|
let(:database) { 'database_name' }
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow(user_input).to receive(:output=)
|
||||||
|
allow(user_input).to receive(:intrinsic_shell?).and_return(true)
|
||||||
|
allow(rstream).to receive(:peerinfo).and_return(peerinfo)
|
||||||
|
allow(client).to receive(:socket).and_return(rstream)
|
||||||
|
allow(client).to receive(:database).and_return(database)
|
||||||
|
allow(::Mysql).to receive(:connect).and_return(client)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:session) do
|
||||||
|
mysql_session = described_class.new(rstream, opts)
|
||||||
|
mysql_session.user_input = user_input
|
||||||
|
mysql_session.user_output = user_output
|
||||||
|
mysql_session.name = name
|
||||||
|
mysql_session
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.type' do
|
||||||
|
it 'should have the correct type' do
|
||||||
|
expect(described_class.type).to eq(type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.can_cleanup_files' do
|
||||||
|
it 'should be able to cleanup files' do
|
||||||
|
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#desc' do
|
||||||
|
it 'should have the correct description' do
|
||||||
|
expect(subject.desc).to eq(description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#type' do
|
||||||
|
it 'should have the correct type' do
|
||||||
|
expect(subject.type).to eq(type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#initialize' do
|
||||||
|
context 'without a client' do
|
||||||
|
let(:opts) { {} }
|
||||||
|
|
||||||
|
it 'raises a KeyError' do
|
||||||
|
expect { subject }.to raise_exception(KeyError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context 'with a client' do
|
||||||
|
it 'does not raise an exception' do
|
||||||
|
expect { subject }.not_to raise_exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new console' do
|
||||||
|
expect(subject.console).to be_a(console_class)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#bootstrap' do
|
||||||
|
subject { session.bootstrap }
|
||||||
|
|
||||||
|
it 'keeps the sessions user input' do
|
||||||
|
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps the sessions user output' do
|
||||||
|
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the console input' do
|
||||||
|
expect { subject }.to change(session.console, :input).to(user_input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the console output' do
|
||||||
|
expect { subject }.to change(session.console, :output).to(user_output)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the log source' do
|
||||||
|
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#reset_ui' do
|
||||||
|
before(:each) do
|
||||||
|
session.bootstrap
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { session.reset_ui }
|
||||||
|
|
||||||
|
it 'keeps the sessions user input' do
|
||||||
|
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps the sessions user output' do
|
||||||
|
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets the console input' do
|
||||||
|
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets the console output' do
|
||||||
|
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#exit' do
|
||||||
|
subject { session.exit }
|
||||||
|
|
||||||
|
it 'exits the session' do
|
||||||
|
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#address' do
|
||||||
|
subject { session.address }
|
||||||
|
|
||||||
|
it { is_expected.to eq(address) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#port' do
|
||||||
|
subject { session.port }
|
||||||
|
|
||||||
|
it { is_expected.to eq(port) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'rex/post/mysql/ui/console'
|
||||||
|
require 'mysql'
|
||||||
|
|
||||||
|
RSpec.describe Rex::Post::MySQL::Ui::Console::CommandDispatcher::Core do
|
||||||
|
let(:rstream) { instance_double(::Rex::Socket) }
|
||||||
|
let(:client) { instance_double(::Mysql) }
|
||||||
|
let(:database) { 'database_name' }
|
||||||
|
let(:address) { '192.0.2.1' }
|
||||||
|
let(:port) { '3306' }
|
||||||
|
let(:peerinfo) { "#{address}:#{port}" }
|
||||||
|
let(:session) { Msf::Sessions::MySQL.new(rstream, { client: client }) }
|
||||||
|
let(:console) do
|
||||||
|
console = Rex::Post::MySQL::Ui::Console.new(session)
|
||||||
|
console.disable_output = true
|
||||||
|
console
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow(rstream).to receive(:peerinfo).and_return(peerinfo)
|
||||||
|
allow(client).to receive(:database).and_return(database)
|
||||||
|
allow(client).to receive(:socket).and_return(rstream)
|
||||||
|
allow(session).to receive(:console).and_return(console)
|
||||||
|
allow(session).to receive(:name).and_return('test client name')
|
||||||
|
allow(session).to receive(:sid).and_return('test client sid')
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:command_dispatcher) { described_class.new(session.console) }
|
||||||
|
|
||||||
|
it_behaves_like 'session command dispatcher'
|
||||||
|
end
|
Loading…
Reference in New Issue