Land #10682, add JSON RPC framework and msfrpc v1.0 API endpoints

GSoC/Meterpreter_Web_Console
Brent Cook 2018-09-28 15:21:02 -05:00
commit 572d430429
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
13 changed files with 1117 additions and 11 deletions

View File

@ -19,6 +19,11 @@ module ServletHelper
set_json_data_response(response: '')
end
def set_raw_response(data, code: 200)
headers = { 'Content-Type' => 'application/json' }
[code, headers, data]
end
def set_json_response(data, includes = nil, code = 200)
headers = { 'Content-Type' => 'application/json' }
[code, headers, to_json(data, includes)]

View File

@ -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

View File

@ -0,0 +1,216 @@
require 'json'
require 'msf/core/rpc'
module Msf::RPC::JSON
class Dispatcher
JSON_RPC_VERSION = '2.0'
JSON_RPC_REQUIRED_MEMBERS = %i(jsonrpc method)
JSON_RPC_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]
}
attr_reader :framework
attr_reader :command
# Instantiate a Dispatcher.
# @param framework [Msf::Simple::Framework] Framework wrapper instance
def initialize(framework)
@framework = framework
@command = nil
end
# Set the command.
# @param command [RpcCommand] the command used by the Dispatcher.
def set_command(command)
@command = command
end
# Process the JSON-RPC request.
# @param source [String] the JSON-RPC request
# @return [String] JSON-RPC response that encapsulates the RPC result
# if successful; otherwise, a JSON-RPC error response.
def process(source)
begin
request = parse_json_request(source)
if request.is_a?(Array)
# 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
# Validate and execute the JSON-RPC request.
# @param request [Hash] the JSON-RPC request
# @returns [RpcCommand] an RpcCommand for the specified version
# @raise [InvalidParams] ArgumentError occurred during execution.
# @raise [ApplicationServerError] General server-error wrapper around an
# Msf::RPC::Exception that occurred during execution.
# @returns [Hash] JSON-RPC response that encapsulates the RPC result
# if successful; otherwise, a JSON-RPC error response.
def process_request(request)
begin
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])
# 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
raise ApplicationServerError.new(e.message, data: { code: e.code })
end
end
# Validate the JSON-RPC request.
# @param request [Hash] the JSON-RPC request
# @returns [Boolean] true if the JSON-RPC request is a valid; otherwise, false.
def validate_rpc_request(request)
# validate request is an object
return false unless request.is_a?(Hash)
# validate request contains required members
JSON_RPC_REQUIRED_MEMBERS.each { |member| return false unless request.key?(member) }
return false if request[:jsonrpc] != JSON_RPC_VERSION
# validate request members are correct types
request.each do |member, value|
return false if JSON_RPC_MEMBER_TYPES.key?(member) &&
!JSON_RPC_MEMBER_TYPES[member].one? { |type| value.is_a?(type) }
end
true
end
# Parse the JSON document source into a Hash or Array with symbols for the names (keys).
# @param source [String] the JSON source
# @raise [ParseError] Invalid JSON was received by the server.
# An error occurred on the server while parsing the JSON text.
# @return [Hash or Array] Hash or Array representation of source
def parse_json_request(source)
begin
JSON.parse(source, symbolize_names: true)
rescue
raise ParseError.new
end
end
# Serialize data as JSON string.
# @param data [Hash] data
# @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
# Create a JSON-RPC success response.
# @param result [Object] the RPC method's return value
# @param request [Hash] the JSON-RPC request
# @returns [Hash] JSON-RPC success response.
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)
response
end
# Create a JSON-RPC error response.
# @param error [RpcError] a RpcError instance
# @param request [Hash] the JSON-RPC request
# @returns [Hash] JSON-RPC error response.
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)
response
end
# Adds response id based on request id.
# @param response [Hash] the JSON-RPC response
# @param request [Hash] the JSON-RPC request
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

View File

@ -0,0 +1,31 @@
require 'msf/core/rpc'
module Msf::RPC::JSON
module DispatcherHelper
# Get an RPC Dispatcher for the RPC version. Creates a new instance and stores
# it in the dispatchers hash if one does not already exist for the version.
# @param dispatchers [Hash] hash of version Symbol - Msf::RPC::JSON::Dispatcher object pairs
# @param version [Symbol] the RPC version
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @returns [Msf::RPC::JSON::Dispatcher] an RPC Dispatcher for the specified version
def get_dispatcher(dispatchers, version, framework)
unless dispatchers.key?(version)
dispatchers[version] = create_dispatcher(version, framework)
end
dispatchers[version]
end
# Create an RPC Dispatcher composed of an RpcCommand for the provided version.
# @param version [Symbol] the RPC version
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @returns [Msf::RPC::JSON::Dispatcher] an RPC Dispatcher for the specified version
def create_dispatcher(version, framework)
command = RpcCommandFactory.create(version, framework)
dispatcher = Dispatcher.new(framework)
dispatcher.set_command(command)
dispatcher
end
end
end

View File

@ -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 %<name>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: %<msg>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

View File

@ -0,0 +1,53 @@
module Msf::RPC::JSON
class RpcCommand
attr_reader :framework
attr_accessor :execute_timeout
# Instantiate an RpcCommand.
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @param execute_timeout [Integer] execute timeout duration in seconds
def initialize(framework, execute_timeout: 7200)
@framework = framework
@execute_timeout = execute_timeout
@methods = {}
end
# Add a method to the RPC Command
# @param method [Method] the Method
# @param name [String] the name the method is register under. The method name is used if nil.
# @returns [Method] the Method.
def register_method(method, name: nil)
if name.nil?
if method.is_a?(Method)
name = method.name.to_s
else
name = method.to_s
end
end
@methods[name] = method
end
# Invokes the method on the receiver object with the specified params,
# returning the method's return value.
# @param method [String] the RPC method name
# @param params [Array, Hash] parameters for the RPC call
# @raise [MethodNotFound] The method does not exist
# @raise [Timeout::Error] The method failed to terminate in @execute_timeout seconds
# @returns [Object] the method's return value.
def execute(method, params)
unless @methods.key?(method)
raise MethodNotFound.new(method)
end
::Timeout.timeout(@execute_timeout) do
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
end
end

View File

@ -0,0 +1,43 @@
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
# Create an RpcCommand for the provided version.
# @param version [Symbol] the RPC version
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @raise [ArgumentError] invalid RPC version
# @returns [RpcCommand] an RpcCommand for the specified version
def self.create(version, framework)
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
# Creates an RpcCommand for a demonstration RPC version 2.0.
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @returns [RpcCommand] an RpcCommand for a demonstration RPC version 2.0
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()
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

View File

@ -0,0 +1,152 @@
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'
# Instantiate an RpcCommand.
# @param framework [Msf::Simple::Framework] Framework wrapper instance
# @param execute_timeout [Integer] execute timeout duration in seconds
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
})
end
# @raise [RuntimeError] The method is not implemented
def register_method(method, name: nil)
raise "#{self.class.name}##{__method__} is not implemented"
end
# Invokes the method on the receiver object with the specified params,
# returning the method's return value.
# @param method [String] the RPC method name
# @param params [Array, Hash] parameters for the RPC call
# @returns [Object] the method's return value.
def execute(method, params)
result = execute_internal(method, params)
result = post_process_result(result, method, params)
result
end
private
# Internal method that invokes the method on the receiver object with
# the specified params, returning the method's return value.
# @param method [String] the RPC method name
# @param params [Array, Hash] parameters for the RPC call
# @raise [MethodNotFound] The method does not exist
# @raise [Timeout::Error] The method failed to terminate in @execute_timeout seconds
# @returns [Object] the method's return value.
def execute_internal(method, params)
group, base_method = parse_method_group(method)
method_name = "rpc_#{base_method}"
method_name_noauth = "rpc_#{base_method}_noauth"
handler = (find_handler(@legacy_rpc_service.handlers, group, method_name) || find_handler(@legacy_rpc_service.handlers, group, method_name_noauth))
if handler.nil?
raise MethodNotFound.new(method)
end
if handler.respond_to?(method_name_noauth)
method_name = method_name_noauth
end
::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
# Parse method string in the format "group.base_method_name".
# @param method [String] the RPC method name
# @returns [Array] Tuple of strings, group and base_method
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
# Find the concrete Msf::RPC::RPC_Base handler for the group and method name.
# @param handlers [Hash] hash of group String - Msf::RPC::RPC_Base object pairs
# @param group [String] the RPC group
# @param method_name [String] the RPC method name
# @returns [Msf::RPC::RPC_Base] concrete Msf::RPC::RPC_Base instance if one exists; otherwise, nil.
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
# Prepare params for use by RPC methods by converting all hashes
# inside of Arrays to use strings for their names (keys).
# @param params [Object] parameters for the RPC call
# @returns [Object] If params is an Array all hashes it contains will be
# modified; otherwise, the object will simply pass-through.
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
end
clean_params
end
# Stringify the names (keys) in hash.
# @param hash [Hash] input hash
# @returns [Hash] a new hash with strings for the keys.
def stringify_names(hash)
JSON.parse(JSON.dump(hash), symbolize_names: false)
end
# Perform custom post processing of the execute result data.
# @param result [Object] the method's return value
# @param method [String] the RPC method name
# @param params [Array, Hash] parameters for the RPC call
# @returns [Object] processed method's return value
def post_process_result(result, method, params)
# post-process payload module result for JSON output
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])
end
result
end
end
end
end

View File

@ -0,0 +1,24 @@
module Msf::RPC::JSON::V2_0
# Receiver class for demonstration RPC version 2.0.
class RpcTest
def initialize
r = Random.new
@rand_num = r.rand(0..100)
end
def self.add(x, y)
x + y
end
def get_instance_rand_num
@rand_num
end
def add_instance_rand_num(x)
@rand_num = @rand_num + x
@rand_num
end
end
end

View File

@ -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

View File

@ -0,0 +1,34 @@
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
#######
# 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
}
end
end

21
msf-json-rpc.ru Normal file
View File

@ -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

View File

@ -0,0 +1,301 @@
require 'spec_helper'
require 'json'
require 'msf/core/rpc'
RSpec.describe Msf::RPC::JSON::Dispatcher do
include_context 'Msf::Simple::Framework'
def to_json(data)
return nil if data.nil?
json = data.to_json
return json.to_s
end
describe '#process' do
before(:each) do
# prepare a dispatcher for all of the tests
@dispatcher = Msf::RPC::JSON::Dispatcher.new(framework)
end
context 'invalid JSON-RPC request' do
before(:each) do
# mock RpcCommand behavior as it isn't relevant for JSON-RPC validation
cmd = instance_double('RpcCommand')
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_return({})
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_return({})
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_return({})
@dispatcher.set_command(cmd)
end
context 'is not valid JSON' do
it 'contains only a string' do
expected_response = {
jsonrpc: '2.0',
error: {
code: -32700,
message: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
},
id: nil
}
expect(@dispatcher.process("Ce n'est pas un JSON")).to eq(expected_response.to_json)
end
end
context 'is not a valid request object' do
expected_response = {
jsonrpc: '2.0',
error: {
code: -32600,
message: 'The JSON sent is not a valid Request object.'
},
id: nil
}
it 'does not contain required jsonrpc member' do
request = '{ "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'does not contain required method member' do
request = '{ "jsonrpc": "2.0" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'does not contain valid JSON-RPC version number' do
request = '{ "jsonrpc": "1.0", "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty JSON object' do
expect(@dispatcher.process('{}')).to eq(expected_response.to_json)
end
it 'is an array with an empty JSON object' do
expect(@dispatcher.process('[{}]')).to eq([expected_response].to_json)
end
it 'is an array with an empty array' do
expect(@dispatcher.process('[[]]')).to eq([expected_response].to_json)
end
it 'is an array with a string' do
expect(@dispatcher.process('["bad"]')).to eq([expected_response].to_json)
end
it 'is an array with a number' do
expect(@dispatcher.process('[123456]')).to eq([expected_response].to_json)
end
it 'is an array with true' do
expect(@dispatcher.process('[true]')).to eq([expected_response].to_json)
end
it 'is an array with false' do
expect(@dispatcher.process('[false]')).to eq([expected_response].to_json)
end
it 'is an array with null' do
expect(@dispatcher.process('[null]')).to eq([expected_response].to_json)
end
context 'contains incorrect data type' do
context 'jsonrpc' do
it 'is a number' do
request = '{ "jsonrpc": 2.0, "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty JSON object' do
request = '{ "jsonrpc": {}, "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty array' do
request = '{ "jsonrpc": [], "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is null' do
request = '{ "jsonrpc": null, "method": "unit-test" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
end
context 'method' do
it 'is a number' do
request = '{ "jsonrpc": "2.0", "method": 123456 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty JSON object' do
request = '{ "jsonrpc": "2.0", "method": {} }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty array' do
request = '{ "jsonrpc": "2.0", "method": [] }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is null' do
request = '{ "jsonrpc": "2.0", "method": null }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
end
context 'params' do
it 'is a number' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": 123456 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is a string' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": "bad-params" }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is true' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": true }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is false' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": false }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is null' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": null }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
end
context 'id' do
it 'is an empty JSON object' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": {} }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an empty array' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": [] }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is an array that contains a number' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": [1] }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is a number that contain fractional parts' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 3.14 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is true' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": true }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'is false' do
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": false }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
end
end
end
end
context 'errors on JSON-RPC method execute' do
it 'does not contain valid method name' do
# mock RpcCommand behavior for MethodNotFound exception
method_name = 'DNE'
cmd = instance_double('RpcCommand')
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
@dispatcher.set_command(cmd)
expected_response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'The method %<name>s does not exist.' % { name: method_name }
},
id: 1
}
request = '{ "jsonrpc": "2.0", "method": "DNE", "params": [], "id": 1 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'does not contain valid method params' do
# mock RpcCommand behavior for InvalidParams exception
cmd = instance_double('RpcCommand')
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_raise(ArgumentError)
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_raise(ArgumentError)
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(ArgumentError)
@dispatcher.set_command(cmd)
expected_response = {
jsonrpc: '2.0',
error: {
code: -32602,
message: 'Invalid method parameter(s).'
},
id: 1
}
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": ["method-has-no-params"], "id": 1 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'throws Msf::RPC::Exception' do
# mock RpcCommand behavior for Msf::RPC::Exception exception
error_code = 123
error_msg = 'unit-test'
cmd = instance_double('RpcCommand')
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
@dispatcher.set_command(cmd)
expected_response = {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Application server error: %<msg>s' % { msg: error_msg },
data: {
code: error_code
}
},
id: 1
}
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 1 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
it 'throws StandardError' do
# mock RpcCommand behavior for StandardError exception
error_msg = 'unit-test'
cmd = instance_double('RpcCommand')
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
@dispatcher.set_command(cmd)
expected_response = {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Application server error: %<msg>s' % { msg: error_msg }
},
id: 1
}
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 1 }'
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
end
end
end
end