# -*- 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