diff --git a/lib/msf/core/web_services/framework_extension.rb b/lib/msf/core/web_services/framework_extension.rb new file mode 100644 index 0000000000..af2aeeac0e --- /dev/null +++ b/lib/msf/core/web_services/framework_extension.rb @@ -0,0 +1,85 @@ +require 'sinatra/base' +require 'uri' + +require 'metasploit/framework/data_service/remote/http/core' +require 'msf/base/simple/framework' + +module Msf::WebServices + # Extension provides a Metasploit Framework instance to a Sinatra application. + # The framework instance is stored with the setting name framework and is + # also accessible via the framework helper method. If the data service URL + # environment variable is set, then the framework instance will be configured + # to use the data service rather than the local database. + # + # Environment Variables: + # MSF_WS_DATA_SERVICE_URL - The data service URL. + # MSF_WS_DATA_SERVICE_API_TOKEN - API token used to authenticate to the remote data service. + # MSF_WS_DATA_SERVICE_CERT - Certificate file matching the remote data server's certificate. + # Needed when using self-signed SSL certificates. + # MSF_WS_DATA_SERVICE_SKIP_VERIFY - (Boolean) Skip validating authenticity of server's certificate. + module FrameworkExtension + FALSE_VALUES = [nil, false, 0, '0', 'f', 'false', 'off', 'no'].to_set + + module Helpers + # Get framework instance from settings. + def framework + settings.framework + end + end + + def self.registered(app) + app.helpers FrameworkExtension::Helpers + + app.set :data_service_url, ENV.fetch('MSF_WS_DATA_SERVICE_URL', nil) + 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)) + + # Create simplified instance of the framework + app.set :framework, Msf::Simple::Framework.create + + if !app.settings.data_service_url.nil? && !app.settings.data_service_url.empty? + framework_db_connect_http_data_service(framework: app.settings.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 + 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" + 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}" + end + end + + private + + def self.to_bool(value) + if value.is_a?(String) + value = value.downcase + end + + !FALSE_VALUES.include?(value) + end + end +end \ No newline at end of file diff --git a/lib/msf/core/web_services/json_rpc_app.rb b/lib/msf/core/web_services/json_rpc_app.rb index 096d1117af..7d7b2bf40c 100644 --- a/lib/msf/core/web_services/json_rpc_app.rb +++ b/lib/msf/core/web_services/json_rpc_app.rb @@ -4,64 +4,69 @@ require 'sysrandom/securerandom' require 'warden' require 'msf/core/rpc' require 'msf/core/web_services/authentication' +require 'msf/core/web_services/framework_extension' require 'msf/core/web_services/servlet_helper' require 'msf/core/web_services/servlet/auth_servlet' require 'msf/core/web_services/servlet/json_rpc_servlet' -class JsonRpcApp < Sinatra::Base - helpers ServletHelper - helpers Msf::RPC::JSON::DispatcherHelper +module Msf::WebServices + class JsonRpcApp < Sinatra::Base + helpers ServletHelper + helpers Msf::RPC::JSON::DispatcherHelper - # Servlet registration - register AuthServlet - register JsonRpcServlet + # Extension registration + register FrameworkExtension - set :framework, Msf::Simple::Framework.create({}) - set :dispatchers, {} + # Servlet registration + register AuthServlet + register JsonRpcServlet + + configure do + set :dispatchers, {} + + set :sessions, {key: 'msf-ws.session', expire_after: 300} + set :session_secret, ENV.fetch('MSF_WS_SESSION_SECRET', SecureRandom.hex(16)) + end + + before do + # store DBManager in request environment so that it is available to Warden + request.env['msf.db_manager'] = get_db + # store flag indicating whether authentication is initialized in the request environment + @@auth_initialized ||= get_db.users({}).count > 0 + request.env['msf.auth_initialized'] = @@auth_initialized + end + + use Warden::Manager do |config| + # failed authentication is handled by this application + config.failure_app = self + # don't intercept 401 responses since the app will provide custom failure messages + config.intercept_401 = false + config.default_scope = :api + + config.scope_defaults :user, + # whether to persist the result in the session or not + store: true, + # list of strategies to use + strategies: [:password], + # action (route) of the failure application + action: "#{AuthServlet.api_unauthenticated_path}/user" + + config.scope_defaults :api, + # whether to persist the result in the session or not + store: false, + # list of strategies to use + strategies: [:api_token], + # action (route) of the failure application + action: AuthServlet.api_unauthenticated_path + + config.scope_defaults :admin_api, + # whether to persist the result in the session or not + store: false, + # list of strategies to use + strategies: [:admin_api_token], + # action (route) of the failure application + action: AuthServlet.api_unauthenticated_path + end - configure do - set :sessions, {key: 'msf-ws.session', expire_after: 300} - set :session_secret, ENV.fetch('MSF_WS_SESSION_SECRET') { SecureRandom.hex(16) } end - - before do - # store DBManager in request environment so that it is available to Warden - request.env['msf.db_manager'] = get_db - # store flag indicating whether authentication is initialized in the request environment - @@auth_initialized ||= get_db.users({}).count > 0 - request.env['msf.auth_initialized'] = @@auth_initialized - end - - use Warden::Manager do |config| - # failed authentication is handled by this application - config.failure_app = self - # don't intercept 401 responses since the app will provide custom failure messages - config.intercept_401 = false - config.default_scope = :api - - config.scope_defaults :user, - # whether to persist the result in the session or not - store: true, - # list of strategies to use - strategies: [:password], - # action (route) of the failure application - action: "#{AuthServlet.api_unauthenticated_path}/user" - - config.scope_defaults :api, - # whether to persist the result in the session or not - store: false, - # list of strategies to use - strategies: [:api_token], - # action (route) of the failure application - action: AuthServlet.api_unauthenticated_path - - config.scope_defaults :admin_api, - # whether to persist the result in the session or not - store: false, - # list of strategies to use - strategies: [:admin_api_token], - # action (route) of the failure application - action: AuthServlet.api_unauthenticated_path - end - end \ No newline at end of file diff --git a/lib/msf/core/web_services/servlet/json_rpc_servlet.rb b/lib/msf/core/web_services/servlet/json_rpc_servlet.rb index d9d7482f32..b13e7328d3 100644 --- a/lib/msf/core/web_services/servlet/json_rpc_servlet.rb +++ b/lib/msf/core/web_services/servlet/json_rpc_servlet.rb @@ -1,34 +1,36 @@ require 'msf/core/rpc' -module JsonRpcServlet +module Msf::WebServices + module JsonRpcServlet - def self.api_path - '/api/:version/json-rpc' - end + def self.api_path + '/api/:version/json-rpc' + end - def self.registered(app) - app.post JsonRpcServlet.api_path, &post_rpc - end + def self.registered(app) + app.post JsonRpcServlet.api_path, &post_rpc + end - ####### - private - ####### + ####### + private + ####### - # Process JSON-RPC request - def self.post_rpc - lambda { - warden.authenticate! - begin - body = request.body.read - tmp_params = sanitize_params(params) - data = get_dispatcher(settings.dispatchers, tmp_params[:version].to_sym, settings.framework).process(body) - set_raw_response(data) - rescue => e - print_error("There was an error executing the RPC: #{e.message}.", e) - error = Msf::RPC::JSON::Dispatcher.create_error_response(Msf::RPC::JSON::InternalError.new(e)) - data = Msf::RPC::JSON::Dispatcher.to_json(error) - set_raw_response(data, code: 500) - end - } + # Process JSON-RPC request + def self.post_rpc + lambda { + warden.authenticate! + begin + body = request.body.read + tmp_params = sanitize_params(params) + data = get_dispatcher(settings.dispatchers, tmp_params[:version].to_sym, framework).process(body) + set_raw_response(data) + rescue => e + print_error("There was an error executing the RPC: #{e.message}.", e) + error = Msf::RPC::JSON::Dispatcher.create_error_response(Msf::RPC::JSON::InternalError.new(e)) + data = Msf::RPC::JSON::Dispatcher.to_json(error) + set_raw_response(data, code: 500) + end + } + end end end \ No newline at end of file diff --git a/msf-json-rpc.ru b/msf-json-rpc.ru index 1bd93c2205..9cad349579 100644 --- a/msf-json-rpc.ru +++ b/msf-json-rpc.ru @@ -18,4 +18,4 @@ end # Note: setup Rails environment before calling require require 'msf/core/web_services/json_rpc_app' -run JsonRpcApp +run Msf::WebServices::JsonRpcApp