1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075 |
- #!/usr/bin/env ruby
- require 'fileutils'
- require 'io/console'
- require 'json'
- require 'net/http'
- require 'net/https'
- require 'open3'
- require 'optparse'
- require 'rex/socket'
- require 'rex/text'
- require 'securerandom'
- require 'uri'
- require 'yaml'
- require 'pg'
- include Rex::Text::Color
- msfbase = __FILE__
- while File.symlink?(msfbase)
- msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
- end
- $:.unshift(File.expand_path(File.join(File.dirname(msfbase), 'lib')))
- $:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
- require 'msfdb_helpers/pg_ctlcluster'
- require 'msfdb_helpers/pg_ctl'
- require 'msfdb_helpers/standalone'
- require 'msfenv'
- @script_name = File.basename(__FILE__)
- @framework = File.expand_path(File.dirname(__FILE__))
- @localconf = Msf::Config.config_directory
- @db = "#{@localconf}/db"
- @db_conf = "#{@localconf}/database.yml"
- @pg_cluster_conf_root = "#{@localconf}/.local/etc/postgresql"
- @db_driver = nil
- @ws_tag = 'msf-ws'
- @ws_conf = File.join(@framework, "#{@ws_tag}.ru")
- @ws_ssl_key_default = "#{@localconf}/#{@ws_tag}-key.pem"
- @ws_ssl_cert_default = "#{@localconf}/#{@ws_tag}-cert.pem"
- @ws_log = "#{@localconf}/logs/#{@ws_tag}.log"
- @ws_pid = "#{@localconf}/#{@ws_tag}.pid"
- @current_user = ENV['LOGNAME'] || ENV['USERNAME'] || ENV['USER']
- @msf_ws_user = (@current_user || "msfadmin").to_s.strip
- @ws_generated_ssl = false
- @ws_api_token = nil
- @components = %w(database webservice)
- @environments = %w(production development)
- @options = {
- # When the component value is nil, the user has not yet specified a specific component
- # It will later be defaulted to a more sane value
- component: nil,
- debug: false,
- msf_db_name: 'msf',
- msf_db_user: 'msf',
- msftest_db_name: 'msftest',
- msftest_db_user: 'msftest',
- db_host: '127.0.0.1',
- db_port: 5433,
- db_pool: 200,
- address: 'localhost',
- port: 5443,
- daemon: true,
- ssl: true,
- ssl_cert: @ws_ssl_cert_default,
- ssl_key: @ws_ssl_key_default,
- ssl_disable_verify: true,
- ws_env: ENV['RACK_ENV'] || 'production',
- retry_max: 10,
- retry_delay: 5.0,
- ws_user: nil,
- add_data_service: false,
- data_service_name: nil,
- use_defaults: false,
- delete_existing_data: true
- }
- def supports_color?
- return true if Rex::Compat.is_windows
- term = Rex::Compat.getenv('TERM')
- term and term.match(/(?:vt10[03]|xterm(?:-color)?|linux|screen|rxvt)/i) != nil
- end
- class String
- def bold
- substitute_colors("%bld#{self}%clr")
- end
- def underline
- substitute_colors("%und#{self}%clr")
- end
- def red
- substitute_colors("%red#{self}%clr")
- end
- def green
- substitute_colors("%grn#{self}%clr")
- end
- def blue
- substitute_colors("%blu#{self}%clr")
- end
- def cyan
- substitute_colors("%cya#{self}%clr")
- end
- end
- def pw_gen
- SecureRandom.base64(32)
- end
- def tail(file)
- begin
- File.readlines(file).last.to_s.strip
- rescue
- nil
- end
- end
- def status_db
- update_db_port
- case @db_driver.status
- when DatabaseStatus::RUNNING
- puts "Database started"
- when DatabaseStatus::INACTIVE
- puts "Database found, but is not running"
- when DatabaseStatus::NEEDS_INIT
- puts "Database found, but needs initialized"
- when DatabaseStatus::NOT_FOUND
- puts "No database found"
- end
- end
- def start_db
- case @db_driver.status
- when DatabaseStatus::NOT_FOUND
- print_error 'No database found.'
- return
- when DatabaseStatus::NEEDS_INIT
- print_error 'Has the database been initialized with "msfdb init" or "msfdb init --component database"?'
- return
- end
- update_db_port
- db_started = @db_driver.start
- if !db_started
- last_log = tail("#{@db}/log")
- puts last_log
- if last_log =~ /not compatible/
- puts 'Please attempt to upgrade the database manually using pg_upgrade.'
- end
- print_error 'Your database may be corrupt. Try reinitializing.'
- end
- end
- def stop_db
- update_db_port
- @db_driver.stop
- end
- def restart_db
- @db_driver.restart
- end
- def init_db
- case @db_driver.status
- when DatabaseStatus::RUNNING
- puts 'Existing database running'
- return
- when DatabaseStatus::INACTIVE
- puts 'Existing database found, attempting to start it'
- @db_driver.start
- return
- end
- if @db_driver.exists? && !@options[:delete_existing_data]
- if !load_db_config
- puts 'Failed to load existing database config. Please reinit and overwrite the file.'
- return
- end
- end
- # Generate new database passwords if not already assigned
- @msf_pass ||= pw_gen
- @msftest_pass ||= pw_gen
- @db_driver.init(@msf_pass, @msftest_pass)
- write_db_config
- puts 'Creating initial database schema'
- Dir.chdir(@framework) do
- @db_driver.run_cmd('bundle exec rake db:migrate')
- end
- puts 'Database initialization successful'.green.bold.to_s
- end
- def load_db_config
- if File.file?(@db_conf)
- config = YAML.load(File.read(@db_conf))
- production = config['production']
- if production.nil?
- puts "No production section found in database config #{@db_conf}."
- return false
- end
- test = config['test']
- if test.nil?
- puts "No test section found in database config #{@db_conf}."
- return false
- end
- # get values for development and production
- @options[:msf_db_name] = production['database']
- @options[:msf_db_user] = production['username']
- @msf_pass = production['password']
- @options[:db_port] = production['port']
- @options[:db_pool] = production['pool']
- # get values for test
- @options[:msftest_db_name] = test['database']
- @options[:msftest_db_user] = test['username']
- @msftest_pass = test['password']
- return true
- end
- return false
- end
- def write_db_config
- # Write a default database config file
- Dir.mkdir(@localconf) unless File.directory?(@localconf)
- File.open(@db_conf, 'w') do |f|
- f.puts <<~EOF
- development: &pgsql
- adapter: postgresql
- database: #{@options[:msf_db_name]}
- username: #{@options[:msf_db_user]}
- password: #{@msf_pass}
- host: #{@options[:db_host]}
- port: #{@options[:db_port]}
- pool: #{@options[:db_pool]}
- production: &production
- <<: *pgsql
- test:
- <<: *pgsql
- database: #{@options[:msftest_db_name]}
- username: #{@options[:msftest_db_user]}
- password: #{@msftest_pass}
- EOF
- end
- File.chmod(0640, @db_conf)
- end
- def update_db_port
- if File.file?(@db_conf)
- config = begin
- YAML.load_file(@db_conf, aliases: true) || {}
- rescue ArgumentError
- YAML.load_file(@db_conf) || {}
- end
- if config["production"] && config["production"]["port"]
- port = config["production"]["port"]
- if port != @options[:db_port]
- puts "Using database port #{port} found in #{@db_conf}"
- @options[:db_port] = port
- end
- end
- end
- end
- def ask_yn(question, default: nil)
- loop do
- print "#{'[?]'.blue.bold} #{question} [#{default}]: "
- input = STDIN.gets.strip
- input = input.empty? ? default : input
- case input
- when /^[Yy]/
- return true
- when /^[Nn]/
- return false
- else
- puts 'Please answer yes or no.'
- end
- end
- end
- def ask_value(question, default)
- return default if @options[:use_defaults]
- print "#{'[?]'.blue.bold} #{question} [#{default}]: "
- input = STDIN.gets.strip
- if input.nil? || input.empty?
- return default
- else
- return input
- end
- end
- def ask_password(question)
- print "#{'[?]'.blue.bold} #{question}: "
- input = STDIN.noecho(&:gets).chomp
- print "\n"
- if input.nil? || input.empty?
- return pw_gen
- else
- return input
- end
- end
- def print_error(error)
- puts "#{'[!]'.red.bold} #{error}"
- end
- def delete_db
- stop_web_service
- @db_driver.delete
- end
- def reinit_db
- delete_db
- init_db
- end
- def print_webservice_removal_prompt
- $stderr.puts "#{'[WARNING]'.red} The remote web service is being removed. Does this impact you? React here: https://github.com/rapid7/metasploit-framework/issues/18439"
- end
- class WebServicePIDStatus
- RUNNING = 0
- INACTIVE = 1
- NO_PID_FILE = 2
- end
- class DatabaseStatus
- RUNNING = 0
- INACTIVE = 1
- NOT_FOUND = 2
- NEEDS_INIT = 3
- end
- def web_service_pid
- File.file?(@ws_pid) ? tail(@ws_pid) : nil
- end
- def web_service_pid_status
- if File.file?(@ws_pid)
- ws_pid = tail(@ws_pid)
- if ws_pid.nil? || !process_active?(ws_pid.to_i)
- WebServicePIDStatus::INACTIVE
- else
- WebServicePIDStatus::RUNNING
- end
- else
- WebServicePIDStatus::NO_PID_FILE
- end
- end
- def status_web_service
- ws_pid = web_service_pid
- status = web_service_pid_status
- if status == WebServicePIDStatus::RUNNING
- puts "MSF web service is running as PID #{ws_pid}"
- elsif status == WebServicePIDStatus::INACTIVE
- puts "MSF web service is not running: PID file found at #{@ws_pid}, but no active process running as PID #{ws_pid}"
- elsif status == WebServicePIDStatus::NO_PID_FILE
- puts "MSF web service is not running: no PID file found at #{@ws_pid}"
- end
- end
- def init_web_service
- if web_service_pid_status == WebServicePIDStatus::RUNNING
- puts "MSF web service is already running as PID #{web_service_pid}"
- return false
- end
- unless @options[:use_defaults]
- if @options[:ws_user].nil?
- @msf_ws_user = ask_value('Initial MSF web service account username?', @msf_ws_user)
- else
- @msf_ws_user = @options[:ws_user]
- end
- end
- if @options[:use_defaults]
- @msf_ws_pass = pw_gen
- elsif @options[:ws_pass].nil?
- @msf_ws_pass = ask_password('Initial MSF web service account password? (Leave blank for random password)')
- else
- @msf_ws_pass = @options[:ws_pass]
- end
- if should_generate_web_service_ssl && @options[:delete_existing_data]
- generate_web_service_ssl(key: @options[:ssl_key], cert: @options[:ssl_cert])
- end
- if start_web_service(expect_auth: false)
- if add_web_service_workspace && add_web_service_user
- output_web_service_information
- else
- puts 'Failed to complete MSF web service configuration, please reinitialize.'
- stop_web_service
- end
- end
- end
- def start_web_service_daemon(expect_auth:)
- if @db_driver.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"
- return false
- end
- # check if MSF web service is already started
- ws_pid = web_service_pid
- status = web_service_pid_status
- if status == WebServicePIDStatus::RUNNING
- puts "MSF web service is already running as PID #{ws_pid}"
- return false
- elsif status == WebServicePIDStatus::INACTIVE
- puts "MSF web service PID file found, but no active process running as PID #{ws_pid}"
- puts "Deleting MSF web service PID file #{@ws_pid}"
- File.delete(@ws_pid)
- end
- print 'Attempting to start MSF web service...'
- unless File.file?(@options[:ssl_key])
- puts "#{'failed'.red.bold}"
- print_error "The SSL Key needed for the webservice to connect to the database could not be found at #{@options[:ssl_key]}."
- print_error 'Has the webservice been initialized with "msfdb init" or "msfdb init --component webservice"?'
- return false
- end
- if @options[:daemon]
- start_web_service_daemon(expect_auth: expect_auth)
- else
- puts thin_cmd
- system "#{thin_cmd} start"
- end
- end
- def stop_web_service
- ws_pid = web_service_pid
- status = web_service_pid_status
- if status == WebServicePIDStatus::RUNNING
- puts "Stopping MSF web service PID #{ws_pid}"
- @db_driver.run_cmd("#{thin_cmd} stop")
- else
- puts 'MSF web service is no longer running'
- if status == WebServicePIDStatus::INACTIVE
- puts "Deleting MSF web service PID file #{@ws_pid}"
- File.delete(@ws_pid)
- end
- end
- end
- def restart_web_service
- stop_web_service
- start_web_service
- end
- def delete_web_service
- stop_web_service
- File.delete(@ws_pid) if web_service_pid_status == WebServicePIDStatus::INACTIVE
- if @options[:delete_existing_data]
- File.delete(@options[:ssl_key]) if File.file?(@options[:ssl_key])
- File.delete(@options[:ssl_cert]) if File.file?(@options[:ssl_cert])
- end
- end
- def reinit_web_service
- delete_web_service
- init_web_service
- end
- def generate_web_service_ssl(key:, cert:)
- @ws_generated_ssl = true
- if (File.file?(key) || File.file?(cert)) && !@options[:delete_existing_data]
- return
- end
- puts 'Generating SSL key and certificate for MSF web service'
- @ssl_key, @ssl_cert, @ssl_extra_chain_cert = Rex::Socket::Ssl.ssl_generate_certificate
- # write PEM format key and certificate
- mode = 'wb'
- mode_int = 0600
- File.open(key, mode) { |f| f.write(@ssl_key.to_pem) }
- File.chmod(mode_int, key)
- File.open(cert, mode) { |f| f.write(@ssl_cert.to_pem) }
- File.chmod(mode_int, cert)
- end
- def web_service_online_check(expect_auth:)
- msf_version_uri = get_web_service_uri(path: '/api/v1/msf/version')
- response_data = http_request(uri: msf_version_uri, method: :get,
- skip_verify: skip_ssl_verify?, cert: get_ssl_cert)
- if !response_data[:exception].nil? && response_data[:exception].is_a?(Errno::ECONNREFUSED)
- response_data[:state] = :offline
- elsif !response_data[:exception].nil? && response_data[:exception].is_a?(OpenSSL::OpenSSLError)
- response_data[:state] = :error
- response_data[:message] = 'Detected an SSL issue. Please set the same options used to initialize the web service or reinitialize.'
- elsif !response_data[:response].nil? && response_data[:response].dig(:error, :code) == 401
- if expect_auth
- response_data[:state] = :online
- else
- response_data[:state] = :error
- response_data[:message] = 'MSF web service expects authentication. If you wish to reinitialize the web service account you will need to reinitialize the database.'
- end
- elsif !response_data[:response].nil? && !response_data[:response].dig(:data, :metasploit_version).nil?
- response_data[:state] = :online
- else
- response_data[:state] = :error
- end
- puts "web_service_online: expect_auth=#{expect_auth}, response_msg=#{response_data}" if @options[:debug]
- response_data
- end
- def add_web_service_workspace(name: 'default')
- # Send request to create new workspace
- workspace_data = { name: name }
- workspaces_uri = get_web_service_uri(path: '/api/v1/workspaces')
- response_data = http_request(uri: workspaces_uri, data: workspace_data, method: :post,
- skip_verify: skip_ssl_verify?, cert: get_ssl_cert)
- response = response_data[:response]
- puts "add_web_service_workspace: add workspace response=#{response}" if @options[:debug]
- if response.nil? || response.dig(:data, :name) != name
- print_error "Error creating MSF web service workspace '#{name}'"
- return false
- end
- return true
- end
- def add_web_service_user
- puts "Creating MSF web service user #{@msf_ws_user}"
- # Generate new web service user password
- cred_data = { username: @msf_ws_user, password: @msf_ws_pass }
- # Send request to create new admin user
- user_data = cred_data.merge({ admin: true })
- user_uri = get_web_service_uri(path: '/api/v1/users')
- response_data = http_request(uri: user_uri, data: user_data, method: :post,
- skip_verify: skip_ssl_verify?, cert: get_ssl_cert)
- response = response_data[:response]
- puts "add_web_service_user: create user response=#{response}" if @options[:debug]
- if response.nil? || response.dig(:data, :username) != @msf_ws_user
- print_error "Error creating MSF web service user #{@msf_ws_user}"
- return false
- end
- puts "\n#{' ############################################################'.cyan}"
- print "#{' ## '.cyan}"
- print"#{'MSF Web Service Credentials'.cyan.bold.underline}"
- puts"#{' ##'.cyan}"
- puts "#{' ## ##'.cyan}"
- puts "#{' ## Please store these credentials securely. ##'.cyan}"
- puts "#{' ## You will need them to connect to the webservice. ##'.cyan}"
- puts "#{' ############################################################'.cyan}"
- puts "\n#{'MSF web service username'.cyan.bold}: #{@msf_ws_user}"
- puts "#{'MSF web service password'.cyan.bold}: #{@msf_ws_pass}"
- # Send request to create new API token for the user
- generate_token_uri = get_web_service_uri(path: '/api/v1/auth/generate-token')
- response_data = http_request(uri: generate_token_uri, data: cred_data, method: :post,
- skip_verify: skip_ssl_verify?, cert: get_ssl_cert)
- response = response_data[:response]
- puts "add_web_service_user: generate token response=#{response}" if @options[:debug]
- if response.nil? || (@ws_api_token = response.dig(:data, :token)).nil?
- print_error "Error creating MSF web service user API token"
- return false
- end
- puts "#{'MSF web service user API token'.cyan.bold}: #{@ws_api_token}"
- return true
- end
- def output_web_service_information
- puts "\n\n"
- puts 'MSF web service configuration complete'
- if @options[:add_data_service]
- data_service_name = @options[:data_service_name] || "local-#{@options[:ssl] ? 'https' : 'http'}-data-service"
- puts "The web service has been configured as your default data service in msfconsole with the name \"#{data_service_name}\""
- else
- puts "No data service has been configured in msfconsole."
- end
- puts ''
- puts 'If needed, manually reconnect to the data service in msfconsole using the command:'
- puts "#{get_db_connect_command}"
- puts ''
- puts 'The username and password are credentials for the API account:'
- puts "#{get_web_service_uri(path: '/api/v1/auth/account')}"
- puts ''
- if @options[:add_data_service]
- persist_data_service
- end
- end
- def run_msfconsole_command(cmd)
- # Attempts to run a the metasploit command first with the default env settings, and once again with the path set
- # to the current directory. This ensures that it works in an environment such as bundler
- # @msf_command holds the initial common part of commands (msfconsole -qx) and takes the optional specific commands as arguments (#{cmd})
- msf_command = "msfconsole -qx '#{cmd}'"
- if @db_driver.run_cmd(msf_command) != 0
- # attempt to execute msfconsole in the current working directory
- if @db_driver.run_cmd(msf_command, env: {'PATH' => ".:#{ENV["PATH"]}"}) != 0
- puts 'Failed to run msfconsole'
- end
- end
- end
- def persist_data_service
- puts 'Persisting http web data service credentials in msfconsole'
- # execute msfconsole commands to add and persist the data service connection
- cmd = "#{get_db_connect_command}; db_save; exit"
- run_msfconsole_command(cmd)
- end
- 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 #{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?
- connect_cmd << " #{get_web_service_uri}"
- connect_cmd
- end
- def get_web_service_uri(path: nil)
- uri_class = @options[:ssl] ? URI::HTTPS : URI::HTTP
- uri_class.build({host: get_web_service_host, port: @options[:port], path: path})
- end
- def get_web_service_host
- # user specified any address INADDR_ANY (0.0.0.0), return a routable address
- @options[:address] == '0.0.0.0' ? 'localhost' : @options[:address]
- end
- def skip_ssl_verify?
- @ws_generated_ssl || @options[:ssl_disable_verify]
- end
- 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.shellescape} --address #{@options[:address].shellescape} --port #{@options[:port]}"
- ssl_opts = @options[:ssl] ? "--ssl --ssl-key-file #{@options[:ssl_key].shellescape} --ssl-cert-file #{@options[:ssl_cert].shellescape}" : ''
- ssl_opts << ' --ssl-disable-verify' if skip_ssl_verify?
- adapter_opts = "--environment #{@options[:ws_env]}"
- daemon_opts = "--daemonize --log #{@ws_log.shellescape} --pid #{@ws_pid.shellescape} --tag #{@ws_tag}" if @options[:daemon]
- all_opts = [server_opts, ssl_opts, adapter_opts, daemon_opts].reject(&:blank?).join(' ')
- "thin #{all_opts}"
- end
- def process_active?(pid)
- begin
- Process.kill(0, pid)
- true
- rescue Errno::ESRCH
- false
- end
- end
- def http_request(uri:, query: nil, data: nil, method: :get, headers: nil, skip_verify: false, cert: nil)
- all_headers = { 'User-Agent': @script_name }
- all_headers.merge!(headers) unless headers.nil?
- query_str = (!query.nil? && !query.empty?) ? URI.encode_www_form(query.compact) : nil
- uri.query = query_str
- http = Net::HTTP.new(uri.host, uri.port)
- if uri.is_a?(URI::HTTPS)
- http.use_ssl = true
- if skip_verify
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
- else
- # https://stackoverflow.com/questions/22093042/implementing-https-certificate-pubkey-pinning-with-ruby
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
- user_passed_cert = OpenSSL::X509::Certificate.new(File.read(cert))
- http.verify_callback = lambda do |preverify_ok, cert_store|
- server_cert = cert_store.chain[0]
- return true unless server_cert.to_der == cert_store.current_cert.to_der
- same_public_key?(server_cert, user_passed_cert)
- end
- end
- end
- begin
- response_data = { response: nil }
- case method
- when :get
- request = Net::HTTP::Get.new(uri.request_uri, initheader=all_headers)
- when :post
- request = Net::HTTP::Post.new(uri.request_uri, initheader=all_headers)
- else
- raise Exception, "Request method #{method} is not handled"
- end
- request.content_type = 'application/json'
- unless data.nil?
- json_body = data.to_json
- request.body = json_body
- end
- response = http.request(request)
- unless response.body.nil? || response.body.empty?
- response_data[:response] = JSON.parse(response.body, symbolize_names: true)
- end
- rescue => e
- response_data[:exception] = e
- puts "Problem with HTTP #{method} request #{uri.request_uri}, message: #{e.message}" if @options[:debug]
- end
- response_data
- end
- # Tells us whether the private keys on the passed certificates match
- # and use the same algo
- def same_public_key?(ref_cert, actual_cert)
- pkr, pka = ref_cert.public_key, actual_cert.public_key
- # First check if the public keys use the same crypto...
- return false unless pkr.class == pka.class
- # ...and then - that they have the same contents
- return false unless pkr.to_pem == pka.to_pem
- true
- end
- def parse_args(args)
- subtext = <<~USAGE
- Commands:
- init initialize the component
- reinit delete and reinitialize the component
- delete delete and stop the component
- status check component status
- start start the component
- stop stop the component
- restart restart the component
- USAGE
- parser = OptionParser.new do |opts|
- opts.banner = "Usage: #{@script_name} [options] <command>"
- opts.separator('Manage a Metasploit Framework database and web service')
- opts.separator('')
- opts.separator('General Options:')
- opts.on('--component COMPONENT', @components + ['all'], 'Component used with provided command (default: database)',
- " (#{@components.join(', ')})") { |component|
- @options[:component] = component.to_sym
- }
- opts.on('-d', '--debug', 'Enable debug output') { |d| @options[:debug] = d }
- opts.on('-h', '--help', 'Show this help message') {
- puts opts
- exit
- }
- opts.on('--use-defaults', 'Accept all defaults and do not prompt for options during an init') { |d|
- @options[:use_defaults] = d
- }
- opts.separator('')
- opts.separator('Database Options:')
- opts.on('--msf-db-name NAME', "Database name (default: #{@options[:msf_db_name]})") { |n|
- @options[:msf_db_name] = n
- }
- opts.on('--msf-db-user-name USER', "Database username (default: #{@options[:msf_db_user]})") { |u|
- @options[:msf_db_user] = u
- }
- opts.on('--msf-test-db-name NAME', "Test database name (default: #{@options[:msftest_db_name]})") { |n|
- @options[:msftest_db_name] = n
- }
- opts.on('--msf-test-db-user-name USER', "Test database username (default: #{@options[:msftest_db_user]})") { |u|
- @options[:msftest_db_user] = u
- }
- opts.on('--db-port PORT', Integer, "Database port (default: #{@options[:db_port]})") { |p|
- @options[:db_port] = p
- }
- opts.on('--db-pool MAX', Integer, "Database connection pool size (default: #{@options[:db_pool]})") { |m|
- @options[:db_pool] = m
- }
- opts.on('--connection-string URI', 'Use a pre-existing database cluster for initialization',
- 'Example: --connection-string=postgresql://postgres:mysecretpassword@localhost:5432/postgres') { |c|
- @connection_string = c
- }
- opts.separator('')
- opts.separator('Web Service Options:')
- opts.on('-a', '--address ADDRESS',
- "Bind to host address (default: #{@options[:address]})") { |a|
- @options[:address] = a
- }
- opts.on('-p', '--port PORT', Integer,
- "Web service port (default: #{@options[:port]})") { |p|
- @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|
- @options[:ssl_key] = p
- }
- opts.on('--ssl-cert-file PATH', "Path to certificate (default: #{@options[:ssl_cert]})") { |p|
- @options[:ssl_cert] = p
- }
- opts.on('--[no-]ssl-disable-verify',
- "Disables (optional) client cert requests (default: #{@options[:ssl_disable_verify]})") { |v|
- @options[:ssl_disable_verify] = v
- }
- opts.on('--environment ENV', @environments,
- "Web service framework environment (default: #{@options[:ws_env]})",
- " (#{@environments.join(', ')})") { |e|
- @options[:ws_env] = e
- }
- opts.on('--retry-max MAX', Integer,
- "Maximum number of web service connect attempts (default: #{@options[:retry_max]})") { |m|
- @options[:retry_max] = m
- }
- opts.on('--retry-delay DELAY', Float,
- "Delay in seconds between web service connect attempts (default: #{@options[:retry_delay]})") { |d|
- @options[:retry_delay] = d
- }
- opts.on('--user USER', 'Initial web service admin username') { |u|
- @options[:ws_user] = u
- }
- opts.on('--pass PASS', 'Initial web service admin password') { |p|
- @options[:ws_pass] = p
- }
- opts.on('--[no-]msf-data-service NAME', 'Local msfconsole data service connection name') { |n|
- if !n
- @options[:add_data_service] = false
- else
- @options[:add_data_service] = true
- @options[:data_service_name] = n
- end
- }
- opts.separator('')
- opts.separator(subtext)
- end
- parser.parse!(args)
- if args.length != 1
- puts parser
- abort
- end
- @options
- end
- def invoke_command(commands, component, command)
- method = commands[component][command]
- if !method.nil?
- send(method)
- else
- print_error "Error: unrecognized command '#{command}' for #{component}"
- end
- end
- def installed?(cmd)
- !Msf::Util::Helper.which(cmd).nil?
- end
- def has_requirements(postgresql_cmds)
- ret_val = true
- other_cmds = %w(bundle thin)
- missing_msg = "Missing requirement: %<name>s does not appear to be installed or '%<prog>s' is not in the environment path"
- postgresql_cmds.each do |cmd|
- next unless Msf::Util::Helper.which(cmd).nil?
- puts missing_msg % { name: 'PostgreSQL', prog: cmd }
- ret_val = false
- end
- other_cmds.each do |cmd|
- if Msf::Util::Helper.which(cmd).nil?
- puts missing_msg % { name: "'#{cmd}'", prog: cmd }
- ret_val = false
- end
- end
- ret_val
- end
- def should_generate_web_service_ssl
- @options[:ssl] && ((!File.file?(@options[:ssl_key]) || !File.file?(@options[:ssl_cert])) ||
- (@options[:ssl_key] == @ws_ssl_key_default && @options[:ssl_cert] == @ws_ssl_cert_default))
- end
- def prompt_for_component(command)
- if command == :status || command == :delete
- return :all
- end
- if command == :stop && web_service_pid_status != WebServicePIDStatus::RUNNING
- return :database
- end
- if @options[:add_data_service] == true
- :all
- else
- :database
- end
- end
- def prompt_for_deletion(command)
- destructive_operations = [:reinit, :delete]
- if destructive_operations.include? command
- @options[:delete_existing_data] = should_delete
- end
- end
- def should_delete
- return true if @options[:use_defaults]
- ask_yn("Would you like to delete your existing data and configurations?")
- end
- if $PROGRAM_NAME == __FILE__
- # Bomb out if we're root
- if !Gem.win_platform? && Process.uid.zero?
- puts "Please run #{@script_name} as a non-root user"
- abort
- end
- # map component commands to methods
- commands = {
- database: {
- init: :init_db,
- reinit: :reinit_db,
- delete: :delete_db,
- status: :status_db,
- start: :start_db,
- stop: :stop_db,
- restart: :restart_db
- },
- webservice: {
- init: :init_web_service,
- reinit: :reinit_web_service,
- delete: :delete_web_service,
- status: :status_web_service,
- start: :start_web_service,
- stop: :stop_web_service,
- restart: :restart_web_service
- }
- }
- parse_args(ARGV)
- update_db_port
- if @connection_string
- @db_driver = MsfdbHelpers::Standalone.new(options: @options, db_conf: @db_conf, connection_string: @connection_string)
- elsif installed?('pg_ctl') && has_requirements(MsfdbHelpers::PgCtl.requirements)
- @db_driver = MsfdbHelpers::PgCtl.new(db_path: @db, options: @options, localconf: @localconf, db_conf: @db_conf)
- elsif installed?('pg_ctlcluster') && has_requirements(MsfdbHelpers::PgCtlcluster.requirements)
- @db_driver = MsfdbHelpers::PgCtlcluster.new(db_path: @db, options: @options, localconf: @localconf, db_conf: @db_conf)
- else
- print_error('You need to have postgres installed or specify a database with --connection-string')
- abort
- end
- command = ARGV[0].to_sym
- if @options[:component].nil?
- @options[:component] = prompt_for_component(command)
- end
- prompt_for_deletion(command)
- if @options[:component] == :all
- @components.each { |component|
- if component == :webservice
- 3.times { print_webservice_removal_prompt }
- end
- puts '===================================================================='
- puts "Running the '#{command}' command for the #{component}:"
- invoke_command(commands, component.to_sym, command)
- puts '===================================================================='
- puts
- }
- else
- puts "Running the '#{command}' command for the #{@options[:component]}:"
- if @options[:component] == :webservice
- 3.times { print_webservice_removal_prompt }
- end
- invoke_command(commands, @options[:component], command)
- end
- end
|