1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-11-12 11:52:01 +01:00

Land #14985, Updates the JSON RPC Web service to correctly use framework's database configuration, and adds support for foregrounding the JSON RPC web service

This commit is contained in:
cgranleese-r7 2021-04-15 11:22:07 +01:00 committed by GitHub
commit dbd0ac8203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 624 additions and 337 deletions

View File

@ -0,0 +1,313 @@
# -*- coding: binary -*-
module Msf
##
#
# This class is used when wanting to connect/disconnect framework
# from a particular database or http service
#
##
module DbConnector
DbConfigGroup = 'framework/database'
#
# Connect to a database by using the default framework config, or the config file provided
#
def self.db_connect_from_config(framework, path = nil)
begin
conf = Msf::Config.load(path)
rescue StandardError => e
wlog("Failed to load configuration: #{e}")
return {}
end
if conf.group?(DbConfigGroup)
conf[DbConfigGroup].each_pair do |k, v|
next unless k.downcase == 'default_db'
ilog 'Default data service found. Attempting to connect...'
db_name = v
config = load_db_config(db_name, path)
if config
if framework.db.active && config[:url] !~ /http/
ilog 'Existing local data connection found. Disconnecting first.'
db_disconnect(framework)
end
return db_connect(framework, config).merge(data_service_name: db_name)
else
elog "Config entry for '#{db_name}' could not be found. Config file might be corrupt."
end
end
end
{}
end
# Connect to the required database
#
# @Example Connect to a remote http service
# db_connect(
# framework,
# {
# url: 'https://localhost:5443',
# cert: '/Users/user/.msf4/msf-ws-cert.pem',
# skip_verify: true,
# api_token: 'b1ca123e2f160a8a1fbf79baed180b8dc480de5b994f53eee42e57771e3f65e13bec737e4a4acbb2'
# }
# )
def self.db_connect(framework, opts = {})
unless framework.db.driver
return { error: 'No database driver installed.'}
end
if !opts[:url] && !opts[:yaml_file]
return { error: 'A URL or saved data service name is required.' }
end
if opts[:url] =~ /http/
new_conn_type = 'http'
else
new_conn_type = framework.db.driver
end
# Currently only able to be connected to one DB at a time
if framework.db.connection_established?
# But the http connection still requires a local database to support AR, so we have to allow that
# Don't allow more than one HTTP service, though
if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 2
return {
error: 'Connection already established. Only one connection is allowed at a time. Run db_disconnect first if you wish to connect to a different data service.'
}
end
end
if opts[:yaml_file]
db_connect_yaml(framework, opts)
elsif new_conn_type == 'http'
db_connect_http(framework, as_connection_options(opts))
elsif new_conn_type == 'postgresql'
db_connect_postgresql(framework, as_connection_options(opts))
else
{
error: "This database driver #{new_conn_type} is not currently supported"
}
end
end
#
# Disconnect from the currently connected database. This will gracefully fallback
# from a remote data service to a local postgres instance if configured correctly.
#
def self.db_disconnect(framework)
result = { old_data_service_name: framework.db.name }
unless framework.db.driver
result[:error] = 'No database driver installed.'
return result
end
if framework.db.active
if framework.db.driver == 'http'
begin
framework.db.delete_current_data_service
local_db_url = build_postgres_url
local_name = data_service_search(url: local_db_url)
result[:data_service_name] = local_name
rescue StandardError => e
result[:error] = e.message
end
else
framework.db.disconnect
result[:data_service_name] = nil
end
end
result
end
#
# Connect to a database via the supplied yaml file
#
def self.db_connect_yaml(framework, opts)
file = opts[:yaml_file] || ::File.join(Msf::Config.get_config_root, 'database.yml')
file = ::File.expand_path(file)
unless ::File.exist?(file)
return { error: 'File not found' }
end
begin
db = YAML.load(::File.read(file))['production']
rescue => _e
return { error: 'File did not contain valid production database credentials' }
end
framework.db.connect(db)
local_db_url = build_postgres_url
local_name = data_service_search(url: local_db_url)
return {
result: 'Connected to the database specified in the YAML file',
data_service_name: local_name
}
end
#
# Connect to an existing http database
#
def self.db_connect_http(framework, opts)
# local database is required to use Mdm objects
unless framework.db.active
error = 'No local database connected, meaning some Metasploit features will not be available. A full list of '\
'the affected features & database setup instructions can be found here: '\
'https://github.com/rapid7/metasploit-framework/wiki/msfdb:-Database-Features-&-How-to-Set-up-a-Database-for-Metasploit'
return {
error: error
}
end
uri = db_parse_db_uri_http(opts[:url])
remote_data_service = Metasploit::Framework::DataService::RemoteHTTPDataService.new(uri.to_s, opts)
begin
framework.db.register_data_service(remote_data_service)
framework.db.workspace = framework.db.default_workspace
{
result: "Connected to HTTP data service: #{remote_data_service.name}",
data_service_name: data_service_search(url: opts[:url])
}
rescue => e
{
error: "Failed to connect to the HTTP data service: #{e.message}"
}
end
end
#
# Connect to an existing Postgres database
#
def self.db_connect_postgresql(framework, cli_opts)
info = db_parse_db_uri_postgresql(cli_opts[:url])
opts = { 'adapter' => 'postgresql' }
opts['username'] = info[:user] if (info[:user])
opts['password'] = info[:pass] if (info[:pass])
opts['database'] = info[:name]
opts['host'] = info[:host] if (info[:host])
opts['port'] = info[:port] if (info[:port])
opts['pass'] ||= ''
# Do a little legwork to find the real database socket
if !opts['host']
while(true)
done = false
dirs = %W{ /var/run/postgresql /tmp }
dirs.each do |dir|
if ::File.directory?(dir)
d = ::Dir.new(dir)
d.entries.grep(/^\.s\.PGSQL.(\d+)$/).each do |ent|
opts['port'] = ent.split('.')[-1].to_i
opts['host'] = dir
done = true
break
end
end
break if done
end
break
end
end
# Default to loopback
unless opts['host']
opts['host'] = '127.0.0.1'
end
if framework.db.connect(opts) && framework.db.connection_established?
{
result: "Connected to Postgres data service: #{info[:host]}/#{info[:name]}",
data_service_name: data_service_search(url: opts[:url]) || framework.db.name
}
else
{
error: "Failed to connect to the Postgres data service: #{framework.db.error}"
}
end
end
def self.db_parse_db_uri_postgresql(path)
res = {}
if path
auth, dest = path.split('@')
(dest = auth and auth = nil) if not dest
# remove optional scheme in database url
auth = auth.sub(/^\w+:\/\//, '') if auth
res[:user],res[:pass] = auth.split(':') if auth
targ,name = dest.split('/')
(name = targ and targ = nil) if not name
res[:host],res[:port] = targ.split(':') if targ
end
res[:name] = name || 'metasploit3'
res
end
def self.db_parse_db_uri_http(path)
URI.parse(path)
end
def self.build_postgres_url
conn_params = ApplicationRecord.connection_config
url = ''
url += "#{conn_params[:username]}" if conn_params[:username]
url += ":#{conn_params[:password]}" if conn_params[:password]
url += "@#{conn_params[:host]}" if conn_params[:host]
url += ":#{conn_params[:port]}" if conn_params[:port]
url += "/#{conn_params[:database]}" if conn_params[:database]
url
end
#
# Search for a human readable data service name based on the search criteria
# The search criteria can match against a service name or url
#
def self.data_service_search(name: nil, url: nil)
conf = Msf::Config.load
result = nil
conf.each_pair do |key, value|
conf_name = key.split('/').last
has_name_match = !name.nil? && (conf_name == name)
has_url_match = !url.nil? && (value.is_a?(Hash) && value['url'] == url)
if has_name_match || has_url_match
result = conf_name
end
end
result
end
def self.load_db_config(db_name, path = nil)
conf = Msf::Config.load(path)
conf_options = conf["#{DbConfigGroup}/#{db_name}"]
return unless conf_options
conf_options.transform_keys(&:to_sym)
end
def self.as_connection_options(conf_options)
opts = {}
https_opts = {}
if conf_options
opts[:url] = conf_options[:url] if conf_options[:url]
opts[:api_token] = conf_options[:api_token] if conf_options[:api_token]
https_opts[:cert] = conf_options[:cert] if conf_options[:cert]
https_opts[:skip_verify] = conf_options[:skip_verify] if conf_options[:skip_verify]
else
return
end
opts[:https_opts] = https_opts unless https_opts.empty?
opts
end
end
end

View File

@ -122,7 +122,6 @@ class Msf::DBManager
#
def initialize(framework, opts = {})
self.framework = framework
self.migrated = false
self.modules_cached = false
@ -176,7 +175,6 @@ class Msf::DBManager
end
def init_db(opts)
init_success = false
# Append any migration paths necessary to bring the database online
@ -186,28 +184,24 @@ class Msf::DBManager
end
end
if connection_established?
after_establish_connection
configuration_pathname = Metasploit::Framework::Database.configurations_pathname(path: opts['DatabaseYAML'])
if configuration_pathname.nil?
self.error = "No database YAML file"
else
configuration_pathname = Metasploit::Framework::Database.configurations_pathname(path: opts['DatabaseYAML'])
if configuration_pathname.nil?
self.error = "No database YAML file"
if configuration_pathname.readable?
# parse specified database YAML file
dbinfo = YAML.load_file(configuration_pathname) || {}
dbenv = opts['DatabaseEnv'] || Rails.env
db_opts = dbinfo[dbenv]
else
if configuration_pathname.readable?
# parse specified database YAML file
dbinfo = YAML.load_file(configuration_pathname) || {}
dbenv = opts['DatabaseEnv'] || Rails.env
db = dbinfo[dbenv]
else
elog("Warning, #{configuration_pathname} is not readable. Try running as root or chmod.")
end
elog("Warning, #{configuration_pathname} is not readable. Try running as root or chmod.")
end
if not db
elog("No database definition for environment #{dbenv}")
else
init_success = connect(db)
end
if db_opts
init_success = connect(db_opts)
else
elog("No database definition for environment #{dbenv}")
end
end

View File

@ -26,6 +26,10 @@ module Msf::WebServices
def framework
settings.framework
end
def get_db
framework.db
end
end
def self.registered(app)
@ -35,50 +39,41 @@ module Msf::WebServices
app.set :data_service_api_token, ENV.fetch('MSF_WS_DATA_SERVICE_API_TOKEN', nil)
app.set :data_service_cert, ENV.fetch('MSF_WS_DATA_SERVICE_CERT', nil)
app.set :data_service_skip_verify, to_bool(ENV.fetch('MSF_WS_DATA_SERVICE_SKIP_VERIFY', false))
@@framework = nil
# Create simplified instance of the framework
app.set :framework, Proc.new {
app.set :framework, (proc {
@@framework ||= begin
init_framework_opts = {
'Logger' => ENV.fetch('MSF_WS_DATA_SERVICE_LOGGER', nil)
'Logger' => ENV.fetch('MSF_WS_DATA_SERVICE_LOGGER', nil),
# SkipDatabaseInit false is the default behavior, however for explicitness - note that framework first
# connects to a local database as a pre-requisite to connecting to a remote service to correctly
# configure active record
'SkipDatabaseInit' => false
}
framework = Msf::Simple::Framework.create(init_framework_opts)
if !app.settings.data_service_url.nil? && !app.settings.data_service_url.empty?
framework_db_connect_http_data_service(framework: framework,
data_service_url: app.settings.data_service_url,
api_token: app.settings.data_service_api_token,
cert: app.settings.data_service_cert,
skip_verify: app.settings.data_service_skip_verify)
end
Msf::WebServices::FrameworkExtension.db_connect(framework, app)
framework
end
}
})
end
def self.framework_db_connect_http_data_service(
framework:, data_service_url:, api_token: nil, cert: nil, skip_verify: false)
# local database is required to use Mdm objects
unless framework.db.active
raise "No local database connected"
def self.db_connect(framework, app)
if !app.settings.data_service_url.nil? && !app.settings.data_service_url.empty?
options = {
url: app.settings.data_service_url,
api_token: app.settings.data_service_api_token,
cert: app.settings.data_service_cert,
skip_verify: app.settings.data_service_skip_verify
}
db_result = Msf::DbConnector.db_connect(framework, options)
else
db_result = Msf::DbConnector.db_connect_from_config(framework)
end
opts = {}
https_opts = {}
opts[:url] = data_service_url unless data_service_url.nil?
opts[:api_token] = api_token unless api_token.nil?
https_opts[:cert] = cert unless cert.nil?
https_opts[:skip_verify] = skip_verify if skip_verify
opts[:https_opts] = https_opts unless https_opts.empty?
begin
uri = URI.parse(data_service_url)
remote_data_service = Metasploit::Framework::DataService::RemoteHTTPDataService.new(uri.to_s, opts)
framework.db.register_data_service(remote_data_service)
framework.db.workspace = framework.db.default_workspace
rescue => e
raise "Failed to connect to the HTTP data service: #{e.message}"
if db_result[:error]
raise db_result[:error]
end
end

View File

@ -1,6 +1,7 @@
require 'rack'
require 'metasploit/framework/parsed_options/remote_db'
# TODO: This functionality isn't fully used currently, it should be integrated and called from the top level msfdb.rb file
class Msf::WebServices::HttpDBManagerService
def start(opts)

View File

@ -75,6 +75,14 @@ module CommandDispatcher
dlog("Call stack:\n#{$@.join("\n")}", 'core', LEV_1)
end
#
# Load the configuration required for this CommandDispatcher, configuring
# any internal state as required.
#
def load_config(_path = nil)
# noop
end
#
# Return the subdir of the `documentation/` directory that should be used
# to find usage documentation

View File

@ -71,6 +71,21 @@ class Db
]
end
#
# Attempts to connect to the previously configured database, and additionally keeps track of
# the currently loaded data service.
#
def load_config(path = nil)
result = Msf::DbConnector.db_connect_from_config(framework, path)
if result[:error]
print_error(result[:error])
end
if result[:data_service_name]
@current_data_service = result[:data_service_name]
end
end
#
# Returns true if the db is connected, prints an error and returns
# false if not.
@ -401,6 +416,7 @@ class Db
mode << :add
when '-d','--delete'
mode << :delete
return
when '-c','-C'
list = args.shift
if(!list)
@ -1724,42 +1740,39 @@ class Db
return if not db_check_driver
opts = {}
https_opts = {}
while (arg = args.shift)
case arg
when '-h', '--help'
cmd_db_connect_help
return
when '-y', '--yaml'
yaml_file = args.shift
opts[:yaml_file] = args.shift
when '-c', '--cert'
https_opts[:cert] = args.shift
opts[:cert] = args.shift
when '-t', '--token'
opts[:api_token] = args.shift
when '-l', '--list-services'
list_saved_data_services
return
when '-n', '--name'
name = args.shift
if name =~ /\/|\[|\]/
opts[:name] = args.shift
if opts[:name] =~ /\/|\[|\]/
print_error "Provided name contains an invalid character. Aborting connection."
return
end
when '--skip-verify'
https_opts[:skip_verify] = true
opts[:skip_verify] = true
else
found_name = data_service_search(arg)
found_name = ::Msf::DbConnector.data_service_search(name: arg)
if found_name
opts = load_db_config(found_name)
opts = ::Msf::DbConnector.load_db_config(found_name)
else
opts[:url] = arg
end
end
end
opts[:https_opts] = https_opts unless https_opts.empty?
if !opts[:url] && !yaml_file
if !opts[:url] && !opts[:yaml_file]
print_error 'A URL or saved data service name is required.'
print_line
cmd_db_connect_help
@ -1786,36 +1799,27 @@ class Db
end
end
if yaml_file
if (yaml_file and not ::File.exist? ::File.expand_path(yaml_file))
print_error("File not found")
return
end
file = yaml_file || ::File.join(Msf::Config.get_config_root, "database.yml")
file = ::File.expand_path(file)
if (::File.exist? file)
db = YAML.load(::File.read(file))['production']
framework.db.connect(db)
print_line('Connected to the database specified in the YAML file.')
return
end
result = Msf::DbConnector.db_connect(framework, opts)
if result[:error]
print_error result[:error]
return
end
meth = "db_connect_#{new_conn_type}"
if(self.respond_to?(meth, true))
self.send(meth, opts)
else
print_error("This database driver #{new_conn_type} is not currently supported")
if result[:result]
print_status result[:result]
end
if framework.db.active
name = opts[:name]
if !name || name.empty?
if found_name
name = found_name
elsif result[:data_service_name]
name = result[:data_service_name]
else
name = Rex::Text.rand_text_alphanumeric(8)
end
end
save_db_to_config(framework.db, name)
@current_data_service = name
end
@ -1836,25 +1840,20 @@ class Db
return
end
db_name = framework.db.name
previous_name = framework.db.name
result = Msf::DbConnector.db_disconnect(framework)
if framework.db.active
if framework.db.driver == 'http'
begin
framework.db.delete_current_data_service
local_db_url = build_postgres_url
local_name = data_service_search(local_db_url)
@current_data_service = local_name
rescue => e
print_error "Unable to disconnect from the data service: #{e.message}"
end
else
framework.db.disconnect
@current_data_service = nil
end
print_line "Successfully disconnected from the data service: #{db_name}."
if result[:error]
print_error "Unable to disconnect from the data service: #{@current_data_service}"
print_error result[:error]
elsif result[:old_data_service_name].nil?
print_error 'Not currently connected to a data service.'
else
print_error "Not currently connected to a data service."
print_line "Successfully disconnected from the data service: #{previous_name}."
@current_data_service = result[:data_service_name]
if @current_data_service
print_line "Now connected to: #{@current_data_service}."
end
end
end
@ -1912,7 +1911,7 @@ class Db
print_error "There was an error saving the data service configuration: #{e.message}"
end
else
url = build_postgres_url
url = Msf::DbConnector.build_postgres_url
config_opts['url'] = url
Msf::Config.save(config_path => config_opts)
end
@ -1971,106 +1970,11 @@ class Db
true
end
#
# Database management: Postgres
#
#
# Connect to an existing Postgres database
#
def db_connect_postgresql(cli_opts)
info = db_parse_db_uri_postgresql(cli_opts[:url])
opts = { 'adapter' => 'postgresql' }
opts['username'] = info[:user] if (info[:user])
opts['password'] = info[:pass] if (info[:pass])
opts['database'] = info[:name]
opts['host'] = info[:host] if (info[:host])
opts['port'] = info[:port] if (info[:port])
opts['pass'] ||= ''
# Do a little legwork to find the real database socket
if(! opts['host'])
while(true)
done = false
dirs = %W{ /var/run/postgresql /tmp }
dirs.each do |dir|
if(::File.directory?(dir))
d = ::Dir.new(dir)
d.entries.grep(/^\.s\.PGSQL.(\d+)$/).each do |ent|
opts['port'] = ent.split('.')[-1].to_i
opts['host'] = dir
done = true
break
end
end
break if done
end
break
end
end
# Default to loopback
if(! opts['host'])
opts['host'] = '127.0.0.1'
end
if framework.db.connect(opts) && framework.db.connection_established?
print_line "Connected to Postgres data service: #{info[:host]}/#{info[:name]}"
else
raise RuntimeError.new("Failed to connect to the Postgres data service: #{framework.db.error}")
end
end
def db_connect_http(opts)
# local database is required to use Mdm objects
unless framework.db.active
err_msg = 'No local database connected, meaning some Metasploit features will not be available. A full list of '\
'the affected features & database setup instructions can be found here: '\
'https://github.com/rapid7/metasploit-framework/wiki/msfdb:-Database-Features-&-How-to-Set-up-a-Database-for-Metasploit'
print_error(err_msg)
return
end
uri = db_parse_db_uri_http(opts[:url])
remote_data_service = Metasploit::Framework::DataService::RemoteHTTPDataService.new(uri.to_s, opts)
begin
framework.db.register_data_service(remote_data_service)
print_line "Connected to HTTP data service: #{remote_data_service.name}"
framework.db.workspace = framework.db.default_workspace
rescue => e
raise RuntimeError.new("Failed to connect to the HTTP data service: #{e.message}")
end
end
def db_parse_db_uri_postgresql(path)
res = {}
if (path)
auth, dest = path.split('@')
(dest = auth and auth = nil) if not dest
# remove optional scheme in database url
auth = auth.sub(/^\w+:\/\//, "") if auth
res[:user],res[:pass] = auth.split(':') if auth
targ,name = dest.split('/')
(name = targ and targ = nil) if not name
res[:host],res[:port] = targ.split(':') if targ
end
res[:name] = name || 'metasploit3'
res
end
def db_parse_db_uri_http(path)
URI.parse(path)
end
#
# Miscellaneous option helpers
#
#
# Takes +host_ranges+, an Array of RangeWalkers, and chunks it up into
# blocks of 1024.
@ -2160,36 +2064,6 @@ class Db
print_status(output)
end
def data_service_search(search_criteria)
conf = Msf::Config.load
rv = nil
conf.each_pair do |k,v|
name = k.split('/').last
rv = name if name == search_criteria
end
rv
end
def load_db_config(db_name)
conf = Msf::Config.load
conf_options = conf["#{DB_CONFIG_PATH}/#{db_name}"]
opts = {}
https_opts = {}
if conf_options
opts[:url] = conf_options['url'] if conf_options['url']
opts[:api_token] = conf_options['api_token'] if conf_options['api_token']
https_opts[:cert] = conf_options['cert'] if conf_options['cert']
https_opts[:skip_verify] = conf_options['skip_verify'] if conf_options['skip_verify']
else
print_error "Unable to locate saved data service with name '#{db_name}'"
return
end
opts[:https_opts] = https_opts unless https_opts.empty?
opts
end
def list_saved_data_services
conf = Msf::Config.load
default = nil
@ -2217,17 +2091,6 @@ class Db
print_line tbl.to_s
end
def build_postgres_url
conn_params = ApplicationRecord.connection_config
url = ""
url += "#{conn_params[:username]}" if conn_params[:username]
url += ":#{conn_params[:password]}" if conn_params[:password]
url += "@#{conn_params[:host]}" if conn_params[:host]
url += ":#{conn_params[:port]}" if conn_params[:port]
url += "/#{conn_params[:database]}" if conn_params[:database]
url
end
def print_msgs(status_msg, error_msg)
status_msg.each do |s|
print_status(s)

View File

@ -16,7 +16,6 @@ class Driver < Msf::Ui::Driver
ConfigCore = "framework/core"
ConfigGroup = "framework/ui/console"
DbConfigGroup = "framework/database"
DefaultPrompt = "%undmsf#{Metasploit::Framework::Version::MAJOR}%clr"
DefaultPromptChar = "%clr>"
@ -118,12 +117,11 @@ class Driver < Msf::Ui::Driver
end
# Load the other "core" command dispatchers
CommandDispatchers.each do |dispatcher|
enstack_dispatcher(dispatcher)
CommandDispatchers.each do |dispatcher_class|
dispatcher = enstack_dispatcher(dispatcher_class)
dispatcher.load_config(opts['Config'])
end
load_db_config(opts['Config'])
begin
FeatureManager.instance.load_config
rescue StandardException => e
@ -223,38 +221,6 @@ class Driver < Msf::Ui::Driver
end
end
def load_db_config(path=nil)
begin
conf = Msf::Config.load(path)
rescue
wlog("Failed to load configuration: #{$!}")
return
end
if conf.group?(DbConfigGroup)
conf[DbConfigGroup].each_pair do |k, v|
if k.downcase == 'default_db'
ilog "Default data service found. Attempting to connect..."
default_db_config_path = "#{DbConfigGroup}/#{v}"
default_db = conf[default_db_config_path]
if default_db
connect_string = "db_connect #{v}"
if framework.db.active && default_db['url'] !~ /http/
ilog "Existing local data connection found. Disconnecting first."
run_single("db_disconnect")
end
run_single(connect_string)
else
elog "Config entry for '#{default_db_config_path}' could not be found. Config file might be corrupt."
return
end
end
end
end
end
#
# Loads configuration for the console.
#

122
msfdb
View File

@ -58,6 +58,7 @@ require 'msfenv'
db_pool: 200,
address: 'localhost',
port: 5443,
daemon: true,
ssl: true,
ssl_cert: @ws_ssl_cert_default,
ssl_key: @ws_ssl_key_default,
@ -121,12 +122,10 @@ def run_cmd(cmd, input: nil, env: {})
exitstatus = wait_thr.value.exitstatus
end
if exitstatus != 0
if @options[:debug]
puts "'#{cmd}' returned #{exitstatus}"
puts out
puts err
end
if @options[:debug]
puts "'#{cmd}' returned #{exitstatus}"
puts out
puts err
end
exitstatus
@ -497,6 +496,44 @@ def init_web_service
end
end
def start_web_service_daemon(expect_auth:)
if run_cmd("#{thin_cmd} start") == 0
# wait until web service is online
retry_count = 0
response_data = web_service_online_check(expect_auth: expect_auth)
is_online = response_data[:state] != :offline
while !is_online && retry_count < @options[:retry_max]
retry_count += 1
if @options[:debug]
puts "MSF web service doesn't appear to be online. Sleeping #{@options[:retry_delay]}s until check #{retry_count}/#{@options[:retry_max]}"
end
sleep(@options[:retry_delay])
response_data = web_service_online_check(expect_auth: expect_auth)
is_online = response_data[:state] != :offline
end
if response_data[:state] == :online
puts "#{'success'.green.bold}"
puts 'MSF web service started and online'
return true
elsif response_data[:state] == :error
puts "#{'failed'.red.bold}"
print_error 'MSF web service failed and returned the following message:'
puts "#{response_data[:message].nil? || response_data[:message].empty? ? "No message returned." : response_data[:message]}"
elsif response_data[:state] == :offline
puts "#{'failed'.red.bold}"
print_error 'A connection with the web service was refused.'
end
puts "Please see #{@ws_log} for additional webservice details."
return false
else
puts "#{'failed'.red.bold}"
puts 'Failed to start MSF web service'
return false
end
end
def start_web_service(expect_auth: true)
unless File.file?(@ws_conf)
puts "No MSF web service configuration found at #{@ws_conf}, not starting"
@ -515,7 +552,6 @@ def start_web_service(expect_auth: true)
File.delete(@ws_pid)
end
# daemonize MSF web service
print 'Attempting to start MSF web service...'
unless File.file?(@ws_ssl_key_default)
@ -525,40 +561,11 @@ def start_web_service(expect_auth: true)
return false
end
if run_cmd("#{thin_cmd} start") == 0
# wait until web service is online
retry_count = 0
response_data = web_service_online_check(expect_auth: expect_auth)
is_online = response_data[:state] != :offline
while !is_online && retry_count < @options[:retry_max]
retry_count += 1
if @options[:debug]
puts "MSF web service doesn't appear to be online. Sleeping #{@options[:retry_delay]}s until check #{retry_count}/#{@options[:retry_max]}"
end
sleep(@options[:retry_delay])
response_data = web_service_online_check(expect_auth: expect_auth)
is_online = response_data[:state] != :offline
end
if response_data[:state] == :online
puts "#{'success'.green.bold}"
puts 'MSF web service started and online'
return true
elsif response_data[:state] == :error
puts "#{'failed'.red.bold}"
print_error 'MSF web service failed and returned the following message:'
puts "#{response_data[:message].nil? || response_data[:message].empty? ? "No message returned." : response_data[:message]}"
elsif response_data[:state] == :offline
puts "#{'failed'.red.bold}"
print_error 'A connection with the web service was refused.'
end
puts "Please see #{@ws_log} for additional webservice details."
return false
if @options[:daemon]
start_web_service_daemon(expect_auth: expect_auth)
else
puts "#{'failed'.red.bold}"
puts 'Failed to start MSF web service'
return false
puts thin_cmd
system "#{thin_cmd} start"
end
end
@ -719,33 +726,31 @@ def output_web_service_information
puts "#{get_web_service_uri(path: '/api/v1/auth/account')}"
puts ''
persist_data_service
if @options[:add_data_service]
persist_data_service
end
end
def persist_data_service
data_service_name = "local-#{@options[:ssl] ? 'https' : 'http'}-data-service"
if !@options[:add_data_service]
return
elsif !@options[:data_service_name].nil?
data_service_name = @options[:data_service_name]
end
# execute msfconsole commands to add and persist the data service connection
connect_cmd = get_db_connect_command(name: data_service_name)
cmd = "msfconsole -qx \"#{connect_cmd}; db_save; exit\""
cmd = "./msfconsole -qx \"#{get_db_connect_command}; db_save; exit\""
if run_cmd(cmd) != 0
# attempt to execute msfconsole in the current working directory
if run_cmd(cmd, env: {'PATH' => ".:#{ENV["PATH"]}"}) != 0
puts 'Failed to run msfconsole and persist the data service connection'
end
end
end
def get_db_connect_command(name: nil)
# build db_connect command based on install options
def get_db_connect_command
data_service_name = "local-#{@options[:ssl] ? 'https' : 'http'}-data-service"
if !@options[:data_service_name].nil?
data_service_name = @options[:data_service_name]
end
# build db_remove and db_connect command based on install options
connect_cmd = "db_connect"
connect_cmd << " --name #{name}" unless name.nil?
connect_cmd << " --name #{data_service_name}"
connect_cmd << " --token #{@ws_api_token}"
connect_cmd << " --cert #{@options[:ssl_cert]}" if @options[:ssl]
connect_cmd << " --skip-verify" if skip_ssl_verify?
@ -771,13 +776,14 @@ def get_ssl_cert
@options[:ssl] ? @options[:ssl_cert] : nil
end
# TODO: In the future this can be replaced by Msf::WebServices::HttpDBManagerService
def thin_cmd
server_opts = "--rackup #{@ws_conf} --address #{@options[:address]} --port #{@options[:port]}"
ssl_opts = @options[:ssl] ? "--ssl --ssl-key-file #{@options[:ssl_key]} --ssl-cert-file #{@options[:ssl_cert]}" : ''
ssl_opts << ' --ssl-disable-verify' if skip_ssl_verify?
adapter_opts = "--environment #{@options[:ws_env]}"
daemon_opts = "--daemonize --log #{@ws_log} --pid #{@ws_pid} --tag #{@ws_tag}"
all_opts = [server_opts, ssl_opts, adapter_opts, daemon_opts].reject(&:empty?).join(' ')
daemon_opts = "--daemonize --log #{@ws_log} --pid #{@ws_pid} --tag #{@ws_tag}" if @options[:daemon]
all_opts = [server_opts, ssl_opts, adapter_opts, daemon_opts].reject(&:blank?).join(' ')
"thin #{all_opts}"
end
@ -926,6 +932,10 @@ def parse_args(args)
@options[:port] = p
}
opts.on('--[no-]daemon', 'Enable daemon') { |d|
@options[:daemon] = d
}
opts.on('--[no-]ssl', "Enable SSL (default: #{@options[:ssl]})") { |s| @options[:ssl] = s }
opts.on('--ssl-key-file PATH', "Path to private key (default: #{@options[:ssl_key]})") { |p|

View File

@ -0,0 +1,8 @@
[framework/database]
default_db=local-https-data-service
[framework/database/local-https-data-service]
url=https://localhost:5443
cert=/Users/user/.msf4/msf-ws-cert.pem
skip_verify=true
api_token=b1cd123e2f160a8a1fbf79baed180b8dc480de5b994f53eee42e57771e3f65e13bec737e4a4acbb2

View File

@ -0,0 +1,130 @@
RSpec.describe Msf::DbConnector do
let(:file_fixtures_path) { File.join(Msf::Config.install_root, 'spec', 'file_fixtures') }
let(:empty_config_file) { File.join(file_fixtures_path, 'config_files', 'empty.ini') }
let(:default_remote_db_config_file) { File.join(file_fixtures_path, 'config_files', 'default_remote_db.ini') }
let(:config_file) { empty_config_file }
let(:db) do
instance_double(
Msf::DBManager,
connection_established?: false,
driver: 'driver',
active: true
)
end
let(:framework) do
instance_double(
::Msf::Framework,
version: 'VERSION',
db: db
)
end
before :each do
allow_any_instance_of(::Msf::Config).to receive(:config_file).and_return(config_file)
end
it { is_expected.to respond_to :db_connect_postgresql }
describe '#load_db_config' do
context 'when the config file does not exist' do
let(:config_file) { File.join(file_fixtures_path, 'config_files', 'non_existent_file.ini') }
it 'returns nil' do
expect(subject.load_db_config('local-https-data-service')).to eql(nil)
end
end
context 'when there is no db config present' do
let(:config_file) { empty_config_file }
it 'returns nil' do
expect(subject.load_db_config('local-https-data-service')).to eql(nil)
end
end
context 'when there is a default database registered' do
let(:config_file) { default_remote_db_config_file }
it 'returns the cb config' do
expected_config = {
url: 'https://localhost:5443',
cert: '/Users/user/.msf4/msf-ws-cert.pem',
skip_verify: 'true',
api_token: 'b1cd123e2f160a8a1fbf79baed180b8dc480de5b994f53eee42e57771e3f65e13bec737e4a4acbb2'
}
expect(subject.load_db_config('local-https-data-service')).to eql(expected_config)
end
end
end
describe '#db_connect_from_config' do
let(:db_connect_response) { { result: 'mock result message', data_service_name: 'local-https-data-service' } }
before :each do
allow(subject).to receive(:db_connect).and_return(db_connect_response)
end
context 'when the config file does not exist' do
let(:config_file) { File.join(file_fixtures_path, 'config_files', 'non_existent_file.ini') }
it 'returns an empty object' do
expect(subject.db_connect_from_config(framework)).to eql({})
end
end
context 'when there is no db config present' do
let(:config_file) { empty_config_file }
it 'returns an empty object' do
expect(subject.db_connect_from_config(framework)).to eql({})
end
end
context 'when there is a default database registered' do
let(:config_file) { default_remote_db_config_file }
it 'returns the db_connect_response' do
expected_config = {
url: 'https://localhost:5443',
cert: '/Users/user/.msf4/msf-ws-cert.pem',
skip_verify: 'true',
api_token: 'b1cd123e2f160a8a1fbf79baed180b8dc480de5b994f53eee42e57771e3f65e13bec737e4a4acbb2'
}
expect(subject.db_connect_from_config(framework)).to eql(db_connect_response)
expect(subject).to have_received(:db_connect).with(framework, expected_config)
end
end
end
describe '#data_service_search' do
context 'when the name is not present' do
let(:config_file) { empty_config_file }
it 'returns nil' do
expect(subject.data_service_search(name: 'local-https-data-service')).to eql nil
end
end
context 'when the name is present' do
let(:config_file) { default_remote_db_config_file }
it 'returns the name' do
expect(subject.data_service_search(name: 'local-https-data-service')).to eql 'local-https-data-service'
end
end
context 'when the url is not present' do
let(:config_file) { empty_config_file }
it 'returns nil' do
expect(subject.data_service_search(url: 'https://localhost:5443')).to eql nil
end
end
context 'when the url is present' do
let(:config_file) { default_remote_db_config_file }
it 'returns the name' do
expect(subject.data_service_search(url: 'https://localhost:5443')).to eql 'local-https-data-service'
end
end
end
end

View File

@ -337,7 +337,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'correctly retrieves and parses an empty config file and datastore' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'empty.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'empty.ini'))
framework = instance_double(
::Msf::Framework,
@ -372,7 +372,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'correctly retrieves and parses a populated global datastore' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'empty.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'empty.ini'))
framework = instance_double(
::Msf::Framework,
@ -414,7 +414,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'correctly retrieves and parses a populated global datastore and current module' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'empty.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'empty.ini'))
framework = instance_double(
::Msf::Framework,
@ -465,7 +465,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'correctly retrieves and parses active module variables' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'empty.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'empty.ini'))
framework = instance_double(
::Msf::Framework,
@ -513,7 +513,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'preferences the framework datastore values over config stored values' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'module.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'module.ini'))
framework = instance_double(
::Msf::Framework,
@ -564,7 +564,7 @@ RSpec.describe Msf::Ui::Debug do
end
it 'correctly retrieves and parses Database information' do
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'debug', 'config_files', 'db.ini'))
allow(::Msf::Config).to receive(:config_file).and_return(File.join(file_fixtures_path, 'config_files', 'db.ini'))
framework = instance_double(
::Msf::Framework,

View File

@ -42,9 +42,7 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
it { is_expected.to respond_to :cmd_workspace_tabs }
it { is_expected.to respond_to :commands }
it { is_expected.to respond_to :db_check_driver }
it { is_expected.to respond_to :db_connect_postgresql }
it { is_expected.to respond_to :db_find_tools }
it { is_expected.to respond_to :db_parse_db_uri_postgresql }
it { is_expected.to respond_to :deprecated_commands }
it { is_expected.to respond_to :each_host_range_chunk }
it { is_expected.to respond_to :name }

View File

@ -2,6 +2,7 @@
# -*- coding: binary -*-
#
# Starts the HTTP DB Service interface
# TODO: This functionality exists within the top level msfdb.rb, and should be merged
require 'optparse'