diff --git a/lib/msf/core/rpc.rb b/lib/msf/core/rpc.rb index 7558682bf2..f20dd1a823 100644 --- a/lib/msf/core/rpc.rb +++ b/lib/msf/core/rpc.rb @@ -1,14 +1,35 @@ # -*- coding: binary -*- -require "msf/core/rpc/service" -require "msf/core/rpc/client" +module Msf::RPC + require 'msf/core/rpc/v10/constants' -require "msf/core/rpc/base" -require "msf/core/rpc/auth" -require "msf/core/rpc/core" -require "msf/core/rpc/session" -require "msf/core/rpc/module" -require "msf/core/rpc/job" -require "msf/core/rpc/console" -require "msf/core/rpc/db" -require "msf/core/rpc/plugin" + require 'msf/core/rpc/v10/service' + require 'msf/core/rpc/v10/client' + require 'msf/core/rpc/v10/rpc_auth' + require 'msf/core/rpc/v10/rpc_base' + require 'msf/core/rpc/v10/rpc_console' + require 'msf/core/rpc/v10/rpc_core' + require 'msf/core/rpc/v10/rpc_db' + require 'msf/core/rpc/v10/rpc_job' + require 'msf/core/rpc/v10/rpc_module' + require 'msf/core/rpc/v10/rpc_plugin' + require 'msf/core/rpc/v10/rpc_session' + + + module JSON + autoload :Dispatcher, 'msf/core/rpc/json/dispatcher' + autoload :DispatcherHelper, 'msf/core/rpc/json/dispatcher_helper' + autoload :RpcCommand, 'msf/core/rpc/json/rpc_command' + autoload :RpcCommandFactory, 'msf/core/rpc/json/rpc_command_factory' + + # exception classes + autoload :Error, 'msf/core/rpc/json/error' + autoload :ParseError, 'msf/core/rpc/json/error' + autoload :InvalidRequest, 'msf/core/rpc/json/error' + autoload :MethodNotFound, 'msf/core/rpc/json/error' + autoload :InvalidParams, 'msf/core/rpc/json/error' + autoload :InternalError, 'msf/core/rpc/json/error' + autoload :ServerError, 'msf/core/rpc/json/error' + autoload :ApplicationServerError, 'msf/core/rpc/json/error' + end +end diff --git a/lib/msf/core/rpc/json/dispatcher.rb b/lib/msf/core/rpc/json/dispatcher.rb new file mode 100644 index 0000000000..eaebadf587 --- /dev/null +++ b/lib/msf/core/rpc/json/dispatcher.rb @@ -0,0 +1,206 @@ +require 'json' +require 'msf/core/rpc' + +module Msf::RPC::JSON + class Dispatcher + JSON_RPC_VERSION = '2.0' + + attr_reader :framework + attr_reader :command + + def initialize(framework) + @framework = framework + @command = nil + end + + def set_command(command) + @command = command + $stderr.puts("Msf::RPC::JSON::Dispatcher.set_command(): command=#{command}, @command=#{@command}") # TODO: remove + end + + def process(source) + begin + $stderr.puts("Msf::RPC::JSON::Dispatcher.process(): source=#{source}") # TODO: remove + request = parse_json_request(source) + $stderr.puts("Msf::RPC::JSON::Dispatcher.process(): request=#{request}") # TODO: remove + if request.is_a?(Array) + $stderr.puts("Msf::RPC::JSON::Dispatcher.process(): batch request") # TODO: remove + # If the batch rpc call itself fails to be recognized as an valid + # JSON or as an Array with at least one value, the response from + # the Server MUST be a single Response object. + raise InvalidRequest.new if request.empty? + # process batch request + response = request.map { |r| process_request(r) } + # A Response object SHOULD exist for each Request object, except that + # there SHOULD NOT be any Response objects for notifications. + # Remove nil responses from response array + response.compact! + else + response = process_request(request) + end + rescue ParseError, InvalidRequest => e + # If there was an error in detecting the id in the Request object + # (e.g. Parse error/Invalid Request), then the id member MUST be + # Null. Don't pass request obj when building the error response. + response = self.class.create_error_response(e) + rescue RpcError => e + # other JSON-RPC errors should include the id from the Request object + response = self.class.create_error_response(e, request) + rescue => e + response = self.class.create_error_response(ApplicationServerError.new(e), request) + end + + # When a rpc call is made, the Server MUST reply with a Response, except + # for in the case of Notifications. The Response is expressed as a single + # JSON Object. + self.class.to_json(response) + end + + def process_request(request) + begin + $stderr.puts("Msf::RPC::JSON::Dispatcher.process_request(): request=#{request}") # TODO: remove + + if !validate_rpc_request(request) + response = self.class.create_error_response(InvalidRequest.new) + return response + end + + # dispatch method execution to command + result = @command.execute(request[:method], request[:params]) + $stderr.puts("Msf::RPC::JSON::Dispatcher.process_request(): dispatch result=#{result}, result.class=#{result.class}") # TODO: remove + + # A Notification is a Request object without an "id" member. A Request + # object that is a Notification signifies the Client's lack of interest + # in the corresponding Response object, and as such no Response object + # needs to be returned to the client. The Server MUST NOT reply to a + # Notification, including those that are within a batch request. + if request.key?(:id) + response = self.class.create_success_response(result, request) + else + response = nil + end + + response + rescue ArgumentError + raise InvalidParams.new + rescue Msf::RPC::Exception => e + ApplicationServerError.new(e.message, data: { code: e.code }) + # rescue => e + # raise ApplicationServerError.new(e) + end + end + + def validate_rpc_request(request) + required_members = %i(jsonrpc method) + member_types = { + # A String specifying the version of the JSON-RPC protocol. + jsonrpc: [String], + # A String containing the name of the method to be invoked. + method: [String], + # If present, parameters for the rpc call MUST be provided as a Structured + # value. Either by-position through an Array or by-name through an Object. + # * by-position: params MUST be an Array, containing the values in the + # Server expected order. + # * by-name: params MUST be an Object, with member names that match the + # Server expected parameter names. The absence of expected names MAY + # result in an error being generated. The names MUST match exactly, + # including case, to the method's expected parameters. + params: [Array, Hash], + # An identifier established by the Client that MUST contain a String, + # Number, or NULL value if included. If it is not included it is assumed + # to be a notification. The value SHOULD normally not be Null [1] and + # Numbers SHOULD NOT contain fractional parts [2] + id: [Integer, String, NilClass] + } + + $stderr.puts("Msf::RPC::JSON::Dispatcher.validate_rpc_request(): request.is_a?(Hash)=#{request.is_a?(Hash)}, request=#{request}") + # validate request is an object + return false unless request.is_a?(Hash) + + # validate request contains required members + required_members.each { |member| return false unless request.key?(member) } + # required_members.each do |member| + # $stderr.puts("Msf::RPC::JSON::Dispatcher.validate_rpc_request(): member=#{member}, request.key?(member)=#{request.key?(member)}") + # return false unless request.key?(member) + # end + + $stderr.puts("Msf::RPC::JSON::Dispatcher.validate_rpc_request(): request[:jsonrpc] != JSON_RPC_VERSION=#{request[:jsonrpc] != JSON_RPC_VERSION}") + return false if request[:jsonrpc] != JSON_RPC_VERSION + + # validate request members are correct types + request.each do |member, value| + return false if member_types.key?(member) && + !member_types[member].one? { |type| value.is_a?(type) } + # if member_types.key?(member) && !member_types[member].one? { |type| value.is_a?(type) } + # return false + # else + # return false + # end + end + + true + end + + # Parse the JSON document source into a Hash or Array with symbols for the names (keys). + # @return [Hash or Array] source + def parse_json_request(source) + begin + JSON.parse(source, symbolize_names: true) + rescue + raise ParseError.new + end + end + + # Serialize data as JSON string. + # @return [String] data serialized JSON string if data not nil; otherwise, nil. + def self.to_json(data) + return nil if data.nil? + + json = data.to_json + return json.to_s + end + + def self.create_success_response(result, request = nil) + response = { + # A String specifying the version of the JSON-RPC protocol. + jsonrpc: JSON_RPC_VERSION, + + # This member is REQUIRED on success. + # This member MUST NOT exist if there was an error invoking the method. + # The value of this member is determined by the method invoked on the Server. + result: result + } + + self.add_response_id_member(response, request) + $stderr.puts("Msf::RPC::JSON::Dispatcher.success_response(): response=#{response}") + + response + end + + def self.create_error_response(error, request = nil) + response = { + # A String specifying the version of the JSON-RPC protocol. + jsonrpc: JSON_RPC_VERSION, + + # This member is REQUIRED on error. + # This member MUST NOT exist if there was no error triggered during invocation. + # The value for this member MUST be an Object as defined in section 5.1. + error: error.to_h + } + + self.add_response_id_member(response, request) + $stderr.puts("Msf::RPC::JSON::Dispatcher.error_response(): response=#{response}") + + response + end + + # Adds response id based on request id. + def self.add_response_id_member(response, request) + if !request.nil? && request.key?(:id) + response[:id] = request[:id] + else + response[:id] = nil + end + end + end +end \ No newline at end of file diff --git a/lib/msf/core/rpc/json/dispatcher_helper.rb b/lib/msf/core/rpc/json/dispatcher_helper.rb new file mode 100644 index 0000000000..64544f449b --- /dev/null +++ b/lib/msf/core/rpc/json/dispatcher_helper.rb @@ -0,0 +1,28 @@ +require 'msf/core/rpc' + +module Msf::RPC::JSON + module DispatcherHelper + def get_dispatcher(dispatchers, version, framework) + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.get_dispatcher(): dispatchers=#{dispatchers}, version=#{version}, framework=#{framework}") + version_sym = version.to_sym + unless dispatchers.key?(version_sym) + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.get_dispatcher(): creating dispatcher for RPC version #{version}...") + dispatchers[version_sym] = create_dispatcher(version_sym, framework) + end + + dispatchers[version_sym] + end + + def create_dispatcher(version, framework) + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.create_dispatcher(): version=#{version}, framework=#{framework}") + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.create_dispatcher(): creating RpcCommand...") + command = RpcCommandFactory.create(version, framework) + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.create_dispatcher(): command=#{command}") + $stderr.puts("Msf::RPC::JSON::DispatcherHelper.create_dispatcher(): creating Dispatcher...") + dispatcher = Dispatcher.new(framework) + dispatcher.set_command(command) + + dispatcher + end + end +end \ No newline at end of file diff --git a/lib/msf/core/rpc/json/error.rb b/lib/msf/core/rpc/json/error.rb new file mode 100644 index 0000000000..f1db564324 --- /dev/null +++ b/lib/msf/core/rpc/json/error.rb @@ -0,0 +1,136 @@ +module Msf::RPC::JSON + + # JSON-RPC 2.0 Error Codes + ## Specification errors: + PARSE_ERROR = -32700 + INVALID_REQUEST = -32600 + METHOD_NOT_FOUND = -32601 + INVALID_PARAMS = -32602 + INTERNAL_ERROR = -32603 + ## Implementation-defined server-errors: + SERVER_ERROR_MAX = -32000 + SERVER_ERROR_MIN = -32099 + APPLICATION_SERVER_ERROR = -32000 + + # JSON-RPC 2.0 Error Messages + ERROR_MESSAGES = { + # Specification errors: + PARSE_ERROR => 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', + INVALID_REQUEST => 'The JSON sent is not a valid Request object.', + METHOD_NOT_FOUND => 'The method %s does not exist.', + INVALID_PARAMS => 'Invalid method parameter(s).', + INTERNAL_ERROR => 'Internal JSON-RPC error', + # Implementation-defined server-errors: + APPLICATION_SERVER_ERROR => 'Application server error: %s', + } + + # Base class for all Msf::RPC::JSON exceptions. + class RpcError < StandardError + # Code Message Meaning + # -32700 Parse error Invalid JSON was received by the server. An error + # occurred on the server while parsing the JSON text. + # -32600 Invalid Request The JSON sent is not a valid Request object. + # -32601 Method not found The method does not exist / is not available. + # -32602 Invalid params Invalid method parameter(s). + # -32603 Internal error Internal JSON-RPC error. + # -32000 to -32099 Server error Reserved for implementation-defined server-errors. + + attr_reader :code + attr_reader :message + attr_reader :data + + # Instantiate an RpcError object. + # + # @param code [Integer] A Number that indicates the error type that occurred. + # @param message [String] A String providing a short description of the error. + # The message SHOULD be limited to a concise single sentence. + # @param data [Object] A Primitive or Structured value that contains additional + # information about the error. This may be omitted. The value of this member is + # defined by the Server (e.g. detailed error information, nested errors etc.). + # The default value is nil. + def initialize(code, message, data: nil) + super(message) + @code = code + @message = message + @data = data + end + + def to_h + hash = { + code: @code, + message: @message + } + + # process data member + unless @data.nil? + if @data.is_a?(String) || @data.kind_of?(Numeric) || @data.is_a?(Array) || @data.is_a?(Hash) + hash[:data] = @data + elsif @data.respond_to?(:to_h) + hash[:data] = @data.to_h + else + hash[:data] = @data.to_s + end + end + + hash + end + end + + class ParseError < RpcError + def initialize(data: nil) + super(PARSE_ERROR, ERROR_MESSAGES[PARSE_ERROR], data: data) + end + end + + class InvalidRequest < RpcError + def initialize(data: nil) + super(INVALID_REQUEST, ERROR_MESSAGES[INVALID_REQUEST], data: data) + end + end + + class MethodNotFound < RpcError + def initialize(method, data: nil) + super(METHOD_NOT_FOUND, ERROR_MESSAGES[METHOD_NOT_FOUND] % {name: method}, data: data) + end + end + + class InvalidParams < RpcError + def initialize(data: nil) + super(INVALID_PARAMS, ERROR_MESSAGES[INVALID_PARAMS], data: data) + end + end + + class InternalError < RpcError + def initialize(e, data: nil) + super(INTERNAL_ERROR, "#{ERROR_MESSAGES[INTERNAL_ERROR]}: #{e}", data: data) + end + end + + # Class is reserved for implementation-defined server-error exceptions. + class ServerError < RpcError + + # Instantiate a ServerError object. + # + # @param code [Integer] A Number that indicates the error type that occurred. + # The code must be between -32000 and -32099. + # @param message [String] A String providing a short description of the error. + # The message SHOULD be limited to a concise single sentence. + # @param data [Object] A Primitive or Structured value that contains additional + # information about the error. This may be omitted. The value of this member is + # defined by the Server (e.g. detailed error information, nested errors etc.). + # The default value is nil. + # @raise [ArgumentError] Module not found (either the wrong type or name). + def initialize(code, message, data: nil) + if code < SERVER_ERROR_MIN || code > SERVER_ERROR_MAX + raise ArgumentError.new("invalid code #{code}, must be between #{SERVER_ERROR_MAX} and #{SERVER_ERROR_MIN}") + end + super(code, message, data: data) + end + end + + class ApplicationServerError < ServerError + def initialize(message, data: nil) + super(APPLICATION_SERVER_ERROR, ERROR_MESSAGES[APPLICATION_SERVER_ERROR] % {msg: message}, data: data) + end + end +end diff --git a/lib/msf/core/rpc/json/rpc_command.rb b/lib/msf/core/rpc/json/rpc_command.rb new file mode 100644 index 0000000000..10df34c4c7 --- /dev/null +++ b/lib/msf/core/rpc/json/rpc_command.rb @@ -0,0 +1,80 @@ +module Msf::RPC::JSON + class RpcCommand + attr_reader :framework + attr_accessor :execute_timeout + + def initialize(framework, execute_timeout: 7200) + @framework = framework + @execute_timeout = execute_timeout + @methods = {} + + $stderr.puts("Msf::RPC::JSON::RpcCommand.initialize(): @framework=#{@framework}, @framework.object_id=#{@framework.object_id}") + $stderr.puts("Msf::RPC::JSON::RpcCommand.initialize(): @execute_timeout=#{@execute_timeout}") + $stderr.puts("Msf::RPC::JSON::RpcCommand.initialize(): @methods=#{@methods}") + end + + # Add a method to the RPC Command + def register_method(method, name: nil) + $stderr.puts("Msf::RPC::JSON::RpcCommand.register_method(): method.class=#{method.class}, method.inspect=#{method.inspect}, name=#{name}") + if name.nil? + if method.is_a?(Method) + $stderr.puts("Msf::RPC::JSON::RpcCommand.register_method(): method.name=#{method.name}") + name = method.name.to_s + else + name = method.to_s + end + end + @methods[name] = method + end + + # Call method on the receiver object previously registered. + def execute(method, params) + $stderr.puts("Msf::RPC::JSON::RpcCommand.execute(): method=#{method}, params=#{params}") + $stderr.puts("Msf::RPC::JSON::RpcCommand.execute(): @methods.key?(method)=#{@methods.key?(method)}") + $stderr.puts("Msf::RPC::JSON::RpcCommand.execute(): @methods[method]=#{@methods[method]}") if @methods.key?(method) + + unless @methods.key?(method) + $stderr.puts("Msf::RPC::JSON::RpcCommand.execute(): raising MethodNotFound...") + raise MethodNotFound.new(method) + end + + $stderr.puts("Msf::RPC::JSON::RpcCommand.execute(): calling method_name=#{method}...") + ::Timeout.timeout(@execute_timeout) do + params = prepare_params(params) + if params.nil? + return @methods[method].call() + elsif params.is_a?(Array) + return @methods[method].call(*params) + else + return @methods[method].call(params) + end + end + end + + private + + # Prepare params for use by RPC methods by converting all hashes to use strings for their names (keys). + def prepare_params(params) + clean_params = params + if params.is_a?(Array) + clean_params = params.map do |p| + if p.is_a?(Hash) + stringify_names(p) + else + p + end + end + elsif params.is_a?(Hash) + clean_params = stringify_names(params) + end + + $stderr.puts("Msf::RPC::JSON::RpcCommand.prepare_params(): params=#{params}, clean_params=#{clean_params}") + clean_params + end + + # Returns a new hash with strings for the names (keys). + def stringify_names(hash) + JSON.parse(JSON.dump(hash), symbolize_names: false) + end + end +end \ No newline at end of file diff --git a/lib/msf/core/rpc/json/rpc_command_factory.rb b/lib/msf/core/rpc/json/rpc_command_factory.rb new file mode 100644 index 0000000000..6ac2c0b4e2 --- /dev/null +++ b/lib/msf/core/rpc/json/rpc_command_factory.rb @@ -0,0 +1,37 @@ +require 'msf/core/rpc' +require 'msf/core/rpc/json/v1_0/rpc_command' +require 'msf/core/rpc/json/v2_0/rpc_test' + +module Msf::RPC::JSON + class RpcCommandFactory + def self.create(version, framework) + $stderr.puts("Msf::RPC::JSON::RpcCommandFactory.create: version=#{version}, framework=#{framework}") # TODO: remove + case version + when :v1, :v1_0, :v10 + return Msf::RPC::JSON::V1_0::RpcCommand.new(framework) + when :v2, :v2_0 + return RpcCommandFactory.create_rpc_command_v2_0(framework) + else + raise ArgumentError.new("invalid RPC version #{version}") + end + end + + def self.create_rpc_command_v2_0(framework) + # TODO: does belong in some sort of loader class for an RPC version? + # instantiate receiver + rpc_test = Msf::RPC::JSON::V2_0::RpcTest.new() + $stderr.puts("Msf::RPC::JSON::RpcCommandFactory.create_rpc_command_v2_0: rpc_test=#{rpc_test}") + + command = Msf::RPC::JSON::RpcCommand.new(framework) + + # Add class methods + command.register_method(Msf::RPC::JSON::V2_0::RpcTest.method(:add)) + command.register_method(Msf::RPC::JSON::V2_0::RpcTest.method(:add), name: 'add_alias') + # Add instance methods + command.register_method(rpc_test.method(:get_instance_rand_num)) + command.register_method(rpc_test.method(:add_instance_rand_num)) + + command + end + end +end \ No newline at end of file diff --git a/lib/msf/core/rpc/json/v1_0/rpc_command.rb b/lib/msf/core/rpc/json/v1_0/rpc_command.rb new file mode 100644 index 0000000000..dd37929665 --- /dev/null +++ b/lib/msf/core/rpc/json/v1_0/rpc_command.rb @@ -0,0 +1,117 @@ +require 'base64' +require 'msf/core/rpc' + +module Msf::RPC::JSON + module V1_0 + class RpcCommand < ::Msf::RPC::JSON::RpcCommand + METHOD_GROUP_SEPARATOR = '.' + + MODULE_EXECUTE_KEY = 'module.execute' + PAYLOAD_MODULE_TYPE_KEY = 'payload' + PAYLOAD_KEY = 'payload' + + def initialize(framework, execute_timeout: 7200) + super(framework, execute_timeout: execute_timeout) + + # The legacy Msf::RPC::Service will not be started, however, it will be used to proxy + # requests to existing handlers. This frees the command from having to act as the + # service to RPC_Base subclasses and expose accessors for tokens and users. + @legacy_rpc_service = ::Msf::RPC::Service.new(@framework, { + execute_timeout: @execute_timeout + }) + + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.initialize(): @framework=#{@framework}, @framework.object_id=#{@framework.object_id}") + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.initialize(): @legacy_rpc_service=#{@legacy_rpc_service}, @legacy_rpc_service.handlers=#{@legacy_rpc_service.handlers}") + end + + def register_method(method, name: nil) + raise "#{self.class.name}##{__method__} is not implemented" + end + + # Call method on the receiver object previously registered. + def execute(method, params) + result = execute_internal(method, params) + + # post process result + result = post_process_result(result, method, params) + + result + end + + private + + # Call method on the receiver object previously registered. + def execute_internal(method, params) + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): method=#{method}, params=#{params}") + + # parse method string + group, base_method = parse_method_group(method) + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): group=#{group}, base_method=#{base_method}") # TODO: remove + + method_name = "rpc_#{base_method}" + method_name_noauth = "rpc_#{base_method}_noauth" + + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): method_name=#{base_method}, method_name_noauth=#{method_name_noauth}, @legacy_rpc_service.handlers[group]=#{@legacy_rpc_service.handlers[group]}") + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): @legacy_rpc_service.handlers[group].nil?=#{@legacy_rpc_service.handlers[group].nil?}") + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): @legacy_rpc_service.handlers[group].respond_to?(method_name)=#{@legacy_rpc_service.handlers[group].respond_to?(method_name)}") + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): @legacy_rpc_service.handlers[group].respond_to?(method_name_noauth)=#{@legacy_rpc_service.handlers[group].respond_to?(method_name_noauth)}") + + + handler = (find_handler(@legacy_rpc_service.handlers, group, method_name) || find_handler(@legacy_rpc_service.handlers, group, method_name_noauth)) + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): handler=#{handler}") + if handler.nil? + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): raising MethodNotFound...") + raise MethodNotFound.new(method) + end + + if handler.respond_to?(method_name_noauth) + method_name = method_name_noauth + end + + + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.execute(): calling method_name=#{method_name}...") + ::Timeout.timeout(@execute_timeout) do + params = prepare_params(params) + if params.nil? + return handler.send(method_name) + elsif params.is_a?(Array) + return handler.send(method_name, *params) + else + return handler.send(method_name, params) + end + end + end + + def parse_method_group(method) + idx = method.rindex(METHOD_GROUP_SEPARATOR) + if idx.nil? + group = nil + base_method = method + else + group = method[0..idx - 1] + base_method = method[idx + 1..-1] + end + return group, base_method + end + + def find_handler(handlers, group, method_name) + handler = nil + if !handlers[group].nil? && handlers[group].respond_to?(method_name) + handler = handlers[group] + end + + handler + end + + def post_process_result(result, method, params) + if method == MODULE_EXECUTE_KEY && params.size >= 2 && + params[0] == PAYLOAD_MODULE_TYPE_KEY && result.key?(PAYLOAD_KEY) + result[PAYLOAD_KEY] = Base64.strict_encode64(result[PAYLOAD_KEY]) + $stderr.puts("Msf::RPC::JSON::V1_0::RpcCommand.post_process_result(): converted result key '#{PAYLOAD_KEY}': new value=#{result[PAYLOAD_KEY]}") + end + + result + end + end + end +end \ No newline at end of file diff --git a/lib/msf/core/rpc/json/v2_0/rpc_test.rb b/lib/msf/core/rpc/json/v2_0/rpc_test.rb new file mode 100644 index 0000000000..615262b793 --- /dev/null +++ b/lib/msf/core/rpc/json/v2_0/rpc_test.rb @@ -0,0 +1,30 @@ +module Msf::RPC::JSON::V2_0 + class RpcTest + + def initialize + r = Random.new + @rand_num = r.rand(0..100) + $stderr.puts("Msf::RPC::JSON::V2_0::RpcTest.initialize(): @rand_num=#{@rand_num}") + end + + def self.add(x, y) + result = x + y + $stderr.puts("Msf::RPC::JSON::V2_0::RpcTest.add(): x=#{x}, y=#{y}, result=#{result}") + + result + end + + def get_instance_rand_num + $stderr.puts("Msf::RPC::JSON::V2_0::RpcTest instance.get_instance_rand_num(): @rand_num=#{@rand_num}") + + @rand_num + end + + def add_instance_rand_num(x) + @rand_num = @rand_num + x + $stderr.puts("Msf::RPC::JSON::V2_0::RpcTest instance.add_instance_rand_num(): x=#{x}, @rand_num=#{@rand_num}") + + @rand_num + 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 new file mode 100644 index 0000000000..2501b4a05b --- /dev/null +++ b/lib/msf/core/web_services/json_rpc_app.rb @@ -0,0 +1,69 @@ +require 'sinatra/base' +require 'swagger/blocks' +require 'sysrandom/securerandom' +require 'warden' +require 'msf/core/rpc' +require 'msf/core/db_manager/http/authentication' +require 'msf/core/db_manager/http/servlet_helper' +require 'msf/core/db_manager/http/servlet/auth_servlet' +require 'msf/core/db_manager/http/servlet/user_servlet' +require 'msf/core/web_services/servlet/json_rpc_servlet' + +class JsonRpcApp < Sinatra::Base + helpers ServletHelper + helpers Msf::RPC::JSON::DispatcherHelper + + # Servlet registration + register AuthServlet + register UserServlet + register JsonRpcServlet + + set :framework, Msf::Simple::Framework.create({}) + set :dispatchers, {} + + 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 new file mode 100644 index 0000000000..dcbce72157 --- /dev/null +++ b/lib/msf/core/web_services/servlet/json_rpc_servlet.rb @@ -0,0 +1,36 @@ +require 'msf/core/rpc' + +module JsonRpcServlet + + def self.api_path + '/api/:version/json-rpc' + end + + def self.registered(app) + app.post JsonRpcServlet.api_path, &post_rpc + end + + ####### + private + ####### + + def self.post_rpc + lambda { + warden.authenticate! + begin + body = request.body.read + $stderr.puts("JsonRpcServlet: body=#{body}") + tmp_params = sanitize_params(params) + $stderr.puts("JsonRpcServlet: tmp_params=#{tmp_params}") + + data = get_dispatcher(settings.dispatchers, tmp_params[:version], 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 + } + end +end \ No newline at end of file diff --git a/msf-json-rpc.ru b/msf-json-rpc.ru new file mode 100644 index 0000000000..1bd93c2205 --- /dev/null +++ b/msf-json-rpc.ru @@ -0,0 +1,21 @@ +# msf-json-rpc.ru +# Start using thin: +# thin --rackup msf-json-rpc.ru --address localhost --port 8081 --environment development --tag msf-json-rpc start +# + +require 'pathname' +@framework_path = '.' +root = Pathname.new(@framework_path).expand_path +@framework_lib_path = root.join('lib') +$LOAD_PATH << @framework_lib_path unless $LOAD_PATH.include?(@framework_lib_path) + +require 'msfenv' + +if ENV['MSF_LOCAL_LIB'] + $LOAD_PATH << ENV['MSF_LOCAL_LIB'] unless $LOAD_PATH.include?(ENV['MSF_LOCAL_LIB']) +end + +# Note: setup Rails environment before calling require +require 'msf/core/web_services/json_rpc_app' + +run JsonRpcApp