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::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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
require 'rex/post/meterpreter'
|
||||
require 'rex/post/smb'
|
||||
require 'rex/post/postgresql'
|
||||
require 'rex/post/mysql'
|
||||
|
||||
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::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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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