lib/msf/core/db_connector.rb
# -*- 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.config_directory, '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://docs.metasploit.com/docs/using-metasploit/intermediate/metasploit-database-support.html'
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_db_config.configuration_hash
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