Path: blob/master/spec/plugins/mcp/rpc_resolver_spec.rb
74512 views
# frozen_string_literal: true12require 'spec_helper'3require 'rex/text'4require Metasploit::Framework.root.join('plugins/mcp.rb').to_path56RSpec.describe Msf::Plugin::MCP do7include_context 'Msf::UIDriver'89let(:framework) { instance_double(Msf::Framework) }10let(:output) { driver_output }11let(:base_opts) { { 'LocalOutput' => output } }1213let(:threads_manager) do14instance_double('Msf::Framework::ThreadManager').tap do |tm|15allow(tm).to receive(:spawn).and_return(Thread.new {})16end17end1819before do20allow(framework).to receive(:threads).and_return(threads_manager)21stub_const('Msf::MCP::Metasploit::Client', Class.new do22def initialize(**_args); end23def authenticate(*_args); 'token'; end24def shutdown; end25end)26stub_const('Msf::MCP::Security::RateLimiter', Class.new do27def initialize(**_args); end28end)29stub_const('Msf::MCP::Server', Class.new do30def initialize(**_args); end31def start(**_args); end32def shutdown; end33end)3435mock_dispatcher = instance_double(described_class::McpCommandDispatcher)36allow(mock_dispatcher).to receive(:plugin=)37allow_any_instance_of(described_class).to receive(:add_console_dispatcher).and_return(mock_dispatcher)38allow_any_instance_of(described_class).to receive(:remove_console_dispatcher)39end4041subject(:plugin) { described_class.new(framework, base_opts) }4243describe '#resolve_rpc_config' do44describe 'introspection of loaded msgrpc plugin' do45let(:msgrpc_server) do46instance_double(47'Msf::RPC::Service',48srvhost: '127.0.0.1',49srvport: 55552,50users: { 'msf' => 'introspected_pass' },51options: { ssl: true }52)53end5455let(:msgrpc_plugin) do56instance_double('Msf::Plugin::MSGRPC', name: 'msgrpc', server: msgrpc_server)57end5859let(:plugins_collection) do60[msgrpc_plugin]61end6263before do64allow(framework).to receive(:plugins).and_return(plugins_collection)65end6667it 'extracts the host from the msgrpc server' do68config = plugin.send(:resolve_rpc_config, {})69expect(config[:host]).to eq('127.0.0.1')70end7172it 'extracts the port from the msgrpc server' do73config = plugin.send(:resolve_rpc_config, {})74expect(config[:port]).to eq(55552)75end7677it 'extracts the username from the msgrpc server users hash' do78config = plugin.send(:resolve_rpc_config, {})79expect(config[:user]).to eq('msf')80end8182it 'extracts the password from the msgrpc server users hash' do83config = plugin.send(:resolve_rpc_config, {})84expect(config[:pass]).to eq('introspected_pass')85end8687it 'extracts the ssl setting from the msgrpc server options' do88config = plugin.send(:resolve_rpc_config, {})89expect(config[:ssl]).to eq(true)90end9192it 'does not set auto_started_rpc flag' do93plugin.send(:resolve_rpc_config, {})94expect(plugin.auto_started_rpc).to eq(false)95end96end9798describe 'auto-start path' do99let(:plugins_collection) do100instance_double('Msf::PluginManager').tap do |pm|101allow(pm).to receive(:find).and_return(nil)102allow(pm).to receive(:load).and_return(true)103end104end105106before do107allow(framework).to receive(:plugins).and_return(plugins_collection)108allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')109end110111it 'generates a password of at least 8 characters' do112config = plugin.send(:resolve_rpc_config, {})113expect(config[:pass].length).to be >= 8114end115116it 'prints credentials to the console' do117plugin.send(:resolve_rpc_config, {})118expect(@output.join("\n")).to match(/msf/)119expect(@output.join("\n")).to match(/abcdefghijkl/)120end121122it 'sets auto_started_rpc flag to true' do123plugin.send(:resolve_rpc_config, {})124expect(plugin.auto_started_rpc).to eq(true)125end126127it 'loads the msgrpc plugin via framework.plugins' do128expect(plugins_collection).to receive(:load).with('msgrpc', hash_including('Pass' => 'abcdefghijkl'))129plugin.send(:resolve_rpc_config, {})130end131132it 'uses "msf" as the default username' do133config = plugin.send(:resolve_rpc_config, {})134expect(config[:user]).to eq('msf')135end136137it 'defaults host to 127.0.0.1' do138config = plugin.send(:resolve_rpc_config, {})139expect(config[:host]).to eq('127.0.0.1')140end141142it 'defaults port to 55552' do143config = plugin.send(:resolve_rpc_config, {})144expect(config[:port]).to eq(55_552)145end146end147148describe 'explicit credentials path overrides introspected values' do149let(:msgrpc_server) do150instance_double(151'Msf::RPC::Service',152srvhost: '10.0.0.1',153srvport: 55553,154users: { 'other_user' => 'other_pass' },155options: { ssl: false }156)157end158159let(:msgrpc_plugin) do160instance_double('Msf::Plugin::MSGRPC', name: 'msgrpc', server: msgrpc_server)161end162163let(:plugins_collection) do164[msgrpc_plugin]165end166167before do168allow(framework).to receive(:plugins).and_return(plugins_collection)169end170171it 'uses explicit RpcUser instead of introspected user' do172config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })173expect(config[:user]).to eq('admin')174end175176it 'uses explicit RpcPass instead of introspected password' do177config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })178expect(config[:pass]).to eq('explicit_pass')179end180181it 'uses introspected host/port/ssl when not explicitly overridden' do182config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })183expect(config[:host]).to eq('10.0.0.1')184expect(config[:port]).to eq(55553)185expect(config[:ssl]).to eq(false)186end187188it 'allows explicit RpcHost to override the introspected host' do189config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })190expect(config[:host]).to eq('192.0.2.0')191end192193it 'does not set auto_started_rpc flag' do194plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })195expect(plugin.auto_started_rpc).to eq(false)196end197end198199describe 'error when only one of RpcUser/RpcPass provided' do200let(:plugins_collection) do201instance_double('Msf::PluginManager').tap do |pm|202allow(pm).to receive(:find).and_return(nil)203allow(pm).to receive(:load).and_return(true)204end205end206207before do208allow(framework).to receive(:plugins).and_return(plugins_collection)209allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')210end211212it 'raises an error when RpcUser is provided without RpcPass' do213expect {214plugin.send(:validate_options!, { 'RpcUser' => 'msf' })215}.to raise_error(Msf::MCP::Config::ValidationError, /RpcPass/)216end217218it 'raises an error when RpcPass is provided without RpcUser' do219expect {220plugin.send(:validate_options!, { 'RpcPass' => 'secret' })221}.to raise_error(Msf::MCP::Config::ValidationError, /RpcUser/)222end223end224225describe 'connection to external RPC when RpcHost+RpcPass provided without msgrpc loaded' do226let(:plugins_collection) do227instance_double('Msf::PluginManager').tap do |pm|228allow(pm).to receive(:find).and_return(nil)229allow(pm).to receive(:load).and_return(true)230end231end232233before do234allow(framework).to receive(:plugins).and_return(plugins_collection)235allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')236end237238it 'uses the provided RpcHost' do239config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })240expect(config[:host]).to eq('192.0.2.0')241end242243it 'uses the provided RpcPass' do244config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })245expect(config[:pass]).to eq('remote_pass')246end247248it 'does not auto-start msgrpc when explicit RpcPass is provided' do249plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })250expect(plugin.auto_started_rpc).to eq(false)251end252253it 'does not set auto_started_rpc flag' do254plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })255expect(plugin.auto_started_rpc).to eq(false)256end257end258259describe 'RpcUser defaults to "msf" when only RpcHost+RpcPass are provided' do260let(:plugins_collection) do261instance_double('Msf::PluginManager').tap do |pm|262allow(pm).to receive(:find).and_return(nil)263allow(pm).to receive(:load).and_return(true)264end265end266267before do268allow(framework).to receive(:plugins).and_return(plugins_collection)269end270271it 'defaults RpcUser to "msf"' do272config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })273expect(config[:user]).to eq('msf')274end275276it 'uses explicit RpcUser when provided alongside RpcHost+RpcPass' do277config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcUser' => 'custom', 'RpcPass' => 'remote_pass' })278expect(config[:user]).to eq('custom')279end280end281end282end283284285