Adds MySQL session type

This commit is contained in:
cgranleese-r7 2024-01-17 15:35:47 +00:00
parent 48221e594d
commit 0e9cad6d45
20 changed files with 995 additions and 9 deletions

View File

@ -15,6 +15,10 @@ module Metasploit
include Metasploit::Framework::LoginScanner::RexSocket
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
LIKELY_PORTS = [3306]
LIKELY_SERVICE_NAMES = ['mysql']
@ -35,7 +39,7 @@ module Metasploit
disconnect if self.sock
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
result_options.merge!({
@ -64,8 +68,17 @@ module Metasploit
})
end
unless result_options[:status]
if mysql_conn
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
::Metasploit::Framework::LoginScanner::Result.new(result_options)

View File

@ -228,6 +228,13 @@ class Config < Hash
self.new.postgresql_session_history
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
self.new.pry_history
end
@ -341,6 +348,10 @@ class Config < Hash
config_directory + FileSep + "postgresql_session_history"
end
def mysql_session_history
config_directory + FileSep + "mysql_session_history"
end
def pry_history
config_directory + FileSep + "pry_history"
end

View File

@ -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

View File

@ -33,6 +33,12 @@ module Exploit::Remote::MYSQL
end
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
connect
@ -56,12 +62,21 @@ module Exploit::Remote::MYSQL
return false
end
vprint_good "#{rhost}:#{rport} MySQL - Logged in to '#{db}' with '#{user}':'#{pass}'"
return true
end
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
disconnect if self.sock
vprint_status "#{rhost}:#{rport} MySQL - Disconnected"
end
def mysql_login_datastore
@ -85,6 +100,8 @@ module Exploit::Remote::MYSQL
print_error("Timeout: #{e.message}")
return nil
end
vprint_status "#{rhost}:#{rport} MySQL - querying with '#{sql}'"
res
end
@ -236,4 +253,3 @@ module Exploit::Remote::MYSQL
end
end

View File

@ -24,6 +24,7 @@ module Msf
HIERARCHICAL_SEARCH_TABLE = 'hierarchical_search_table'
SMB_SESSION_TYPE = 'smb_session_type'
POSTGRESQL_SESSION_TYPE = 'postgresql_session_type'
MYSQL_SESSION_TYPE = 'mysql_session_type'
DEFAULTS = [
{
name: WRAPPED_TABLES,
@ -74,6 +75,12 @@ module Msf
name: POSTGRESQL_SESSION_TYPE,
description: 'When enabled will allow for the creation/use of PostgreSQL sessions',
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
}.freeze,
{

View File

@ -9,6 +9,7 @@ module Msf::OptionalSession
def initialize(info = {})
super
if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
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')
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)
register_options(
[
@ -35,7 +46,7 @@ module Msf::OptionalSession
end
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
end

View File

@ -30,7 +30,7 @@ module Msf::Post::Common
session.sock.peerhost
when 'shell', 'powershell'
session.session_host
when 'postgresql'
when 'postgresql', 'mysql'
session.address
end
rescue
@ -45,7 +45,7 @@ module Msf::Post::Common
session.sock.peerport
when 'shell', 'powershell'
session.session_port
when 'postgresql'
when 'postgresql', 'mysql'
session.port
end
rescue

View File

@ -41,6 +41,8 @@ class MsfAutoload
'Http'
elsif basename == 'rftransceiver' && abspath.end_with?("#{__dir__}/rex/post/hwbridge/ui/console/command_dispatcher/rftransceiver.rb")
'RFtransceiver'
elsif basename == 'mysql' && abspath.end_with?("#{__dir__}/msf/base/sessions/mysql.rb")
'MySQL'
else
super
end

View File

@ -4,6 +4,7 @@
require 'rex/post/meterpreter'
require 'rex/post/smb'
require 'rex/post/postgresql'
require 'rex/post/mysql'
module Rex::Post

3
lib/rex/post/mysql.rb Normal file
View File

@ -0,0 +1,3 @@
# -*- coding: binary -*-
require 'rex/post/mysql/ui'

3
lib/rex/post/mysql/ui.rb Normal file
View File

@ -0,0 +1,3 @@
# -*- coding: binary -*-
require 'rex/post/mysql/ui/console'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -10,8 +10,8 @@ class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::MYSQL
include Msf::Auxiliary::Report
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::CommandShell
def initialize(info = {})
super(update_info(info,
@ -36,7 +36,20 @@ class MetasploitModule < Msf::Auxiliary
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
def target
@ -64,6 +77,7 @@ class MetasploitModule < Msf::Auxiliary
send_delay: datastore['TCP::send_delay'],
framework: framework,
framework_module: self,
use_client_as_proof: create_session?,
ssl: datastore['SSL'],
ssl_version: datastore['SSLVersion'],
ssl_verify_mode: datastore['SSLVerifyMode'],
@ -84,6 +98,17 @@ class MetasploitModule < Msf::Auxiliary
create_credential_login(credential_data)
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
invalidate_login(credential_data)
vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
@ -149,6 +174,23 @@ class MetasploitModule < Msf::Auxiliary
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

View File

@ -4,6 +4,7 @@ require 'metasploit/framework/login_scanner/mysql'
RSpec.describe Metasploit::Framework::LoginScanner::MySQL do
let(:public) { 'root' }
let(:private) { 'toor' }
let(:client) { instance_double(::Mysql) }
let(:pub_blank) {
Metasploit::Framework::Credential.new(
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 }
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
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
end
end

View File

@ -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

View File

@ -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