Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/plugins/mcp/rpc_resolver_spec.rb
74512 views
1
# frozen_string_literal: true
2
3
require 'spec_helper'
4
require 'rex/text'
5
require Metasploit::Framework.root.join('plugins/mcp.rb').to_path
6
7
RSpec.describe Msf::Plugin::MCP do
8
include_context 'Msf::UIDriver'
9
10
let(:framework) { instance_double(Msf::Framework) }
11
let(:output) { driver_output }
12
let(:base_opts) { { 'LocalOutput' => output } }
13
14
let(:threads_manager) do
15
instance_double('Msf::Framework::ThreadManager').tap do |tm|
16
allow(tm).to receive(:spawn).and_return(Thread.new {})
17
end
18
end
19
20
before do
21
allow(framework).to receive(:threads).and_return(threads_manager)
22
stub_const('Msf::MCP::Metasploit::Client', Class.new do
23
def initialize(**_args); end
24
def authenticate(*_args); 'token'; end
25
def shutdown; end
26
end)
27
stub_const('Msf::MCP::Security::RateLimiter', Class.new do
28
def initialize(**_args); end
29
end)
30
stub_const('Msf::MCP::Server', Class.new do
31
def initialize(**_args); end
32
def start(**_args); end
33
def shutdown; end
34
end)
35
36
mock_dispatcher = instance_double(described_class::McpCommandDispatcher)
37
allow(mock_dispatcher).to receive(:plugin=)
38
allow_any_instance_of(described_class).to receive(:add_console_dispatcher).and_return(mock_dispatcher)
39
allow_any_instance_of(described_class).to receive(:remove_console_dispatcher)
40
end
41
42
subject(:plugin) { described_class.new(framework, base_opts) }
43
44
describe '#resolve_rpc_config' do
45
describe 'introspection of loaded msgrpc plugin' do
46
let(:msgrpc_server) do
47
instance_double(
48
'Msf::RPC::Service',
49
srvhost: '127.0.0.1',
50
srvport: 55552,
51
users: { 'msf' => 'introspected_pass' },
52
options: { ssl: true }
53
)
54
end
55
56
let(:msgrpc_plugin) do
57
instance_double('Msf::Plugin::MSGRPC', name: 'msgrpc', server: msgrpc_server)
58
end
59
60
let(:plugins_collection) do
61
[msgrpc_plugin]
62
end
63
64
before do
65
allow(framework).to receive(:plugins).and_return(plugins_collection)
66
end
67
68
it 'extracts the host from the msgrpc server' do
69
config = plugin.send(:resolve_rpc_config, {})
70
expect(config[:host]).to eq('127.0.0.1')
71
end
72
73
it 'extracts the port from the msgrpc server' do
74
config = plugin.send(:resolve_rpc_config, {})
75
expect(config[:port]).to eq(55552)
76
end
77
78
it 'extracts the username from the msgrpc server users hash' do
79
config = plugin.send(:resolve_rpc_config, {})
80
expect(config[:user]).to eq('msf')
81
end
82
83
it 'extracts the password from the msgrpc server users hash' do
84
config = plugin.send(:resolve_rpc_config, {})
85
expect(config[:pass]).to eq('introspected_pass')
86
end
87
88
it 'extracts the ssl setting from the msgrpc server options' do
89
config = plugin.send(:resolve_rpc_config, {})
90
expect(config[:ssl]).to eq(true)
91
end
92
93
it 'does not set auto_started_rpc flag' do
94
plugin.send(:resolve_rpc_config, {})
95
expect(plugin.auto_started_rpc).to eq(false)
96
end
97
end
98
99
describe 'auto-start path' do
100
let(:plugins_collection) do
101
instance_double('Msf::PluginManager').tap do |pm|
102
allow(pm).to receive(:find).and_return(nil)
103
allow(pm).to receive(:load).and_return(true)
104
end
105
end
106
107
before do
108
allow(framework).to receive(:plugins).and_return(plugins_collection)
109
allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')
110
end
111
112
it 'generates a password of at least 8 characters' do
113
config = plugin.send(:resolve_rpc_config, {})
114
expect(config[:pass].length).to be >= 8
115
end
116
117
it 'prints credentials to the console' do
118
plugin.send(:resolve_rpc_config, {})
119
expect(@output.join("\n")).to match(/msf/)
120
expect(@output.join("\n")).to match(/abcdefghijkl/)
121
end
122
123
it 'sets auto_started_rpc flag to true' do
124
plugin.send(:resolve_rpc_config, {})
125
expect(plugin.auto_started_rpc).to eq(true)
126
end
127
128
it 'loads the msgrpc plugin via framework.plugins' do
129
expect(plugins_collection).to receive(:load).with('msgrpc', hash_including('Pass' => 'abcdefghijkl'))
130
plugin.send(:resolve_rpc_config, {})
131
end
132
133
it 'uses "msf" as the default username' do
134
config = plugin.send(:resolve_rpc_config, {})
135
expect(config[:user]).to eq('msf')
136
end
137
138
it 'defaults host to 127.0.0.1' do
139
config = plugin.send(:resolve_rpc_config, {})
140
expect(config[:host]).to eq('127.0.0.1')
141
end
142
143
it 'defaults port to 55552' do
144
config = plugin.send(:resolve_rpc_config, {})
145
expect(config[:port]).to eq(55_552)
146
end
147
end
148
149
describe 'explicit credentials path overrides introspected values' do
150
let(:msgrpc_server) do
151
instance_double(
152
'Msf::RPC::Service',
153
srvhost: '10.0.0.1',
154
srvport: 55553,
155
users: { 'other_user' => 'other_pass' },
156
options: { ssl: false }
157
)
158
end
159
160
let(:msgrpc_plugin) do
161
instance_double('Msf::Plugin::MSGRPC', name: 'msgrpc', server: msgrpc_server)
162
end
163
164
let(:plugins_collection) do
165
[msgrpc_plugin]
166
end
167
168
before do
169
allow(framework).to receive(:plugins).and_return(plugins_collection)
170
end
171
172
it 'uses explicit RpcUser instead of introspected user' do
173
config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })
174
expect(config[:user]).to eq('admin')
175
end
176
177
it 'uses explicit RpcPass instead of introspected password' do
178
config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })
179
expect(config[:pass]).to eq('explicit_pass')
180
end
181
182
it 'uses introspected host/port/ssl when not explicitly overridden' do
183
config = plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })
184
expect(config[:host]).to eq('10.0.0.1')
185
expect(config[:port]).to eq(55553)
186
expect(config[:ssl]).to eq(false)
187
end
188
189
it 'allows explicit RpcHost to override the introspected host' do
190
config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })
191
expect(config[:host]).to eq('192.0.2.0')
192
end
193
194
it 'does not set auto_started_rpc flag' do
195
plugin.send(:resolve_rpc_config, { 'RpcUser' => 'admin', 'RpcPass' => 'explicit_pass' })
196
expect(plugin.auto_started_rpc).to eq(false)
197
end
198
end
199
200
describe 'error when only one of RpcUser/RpcPass provided' do
201
let(:plugins_collection) do
202
instance_double('Msf::PluginManager').tap do |pm|
203
allow(pm).to receive(:find).and_return(nil)
204
allow(pm).to receive(:load).and_return(true)
205
end
206
end
207
208
before do
209
allow(framework).to receive(:plugins).and_return(plugins_collection)
210
allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')
211
end
212
213
it 'raises an error when RpcUser is provided without RpcPass' do
214
expect {
215
plugin.send(:validate_options!, { 'RpcUser' => 'msf' })
216
}.to raise_error(Msf::MCP::Config::ValidationError, /RpcPass/)
217
end
218
219
it 'raises an error when RpcPass is provided without RpcUser' do
220
expect {
221
plugin.send(:validate_options!, { 'RpcPass' => 'secret' })
222
}.to raise_error(Msf::MCP::Config::ValidationError, /RpcUser/)
223
end
224
end
225
226
describe 'connection to external RPC when RpcHost+RpcPass provided without msgrpc loaded' do
227
let(:plugins_collection) do
228
instance_double('Msf::PluginManager').tap do |pm|
229
allow(pm).to receive(:find).and_return(nil)
230
allow(pm).to receive(:load).and_return(true)
231
end
232
end
233
234
before do
235
allow(framework).to receive(:plugins).and_return(plugins_collection)
236
allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')
237
end
238
239
it 'uses the provided RpcHost' do
240
config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })
241
expect(config[:host]).to eq('192.0.2.0')
242
end
243
244
it 'uses the provided RpcPass' do
245
config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })
246
expect(config[:pass]).to eq('remote_pass')
247
end
248
249
it 'does not auto-start msgrpc when explicit RpcPass is provided' do
250
plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })
251
expect(plugin.auto_started_rpc).to eq(false)
252
end
253
254
it 'does not set auto_started_rpc flag' do
255
plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })
256
expect(plugin.auto_started_rpc).to eq(false)
257
end
258
end
259
260
describe 'RpcUser defaults to "msf" when only RpcHost+RpcPass are provided' do
261
let(:plugins_collection) do
262
instance_double('Msf::PluginManager').tap do |pm|
263
allow(pm).to receive(:find).and_return(nil)
264
allow(pm).to receive(:load).and_return(true)
265
end
266
end
267
268
before do
269
allow(framework).to receive(:plugins).and_return(plugins_collection)
270
end
271
272
it 'defaults RpcUser to "msf"' do
273
config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcPass' => 'remote_pass' })
274
expect(config[:user]).to eq('msf')
275
end
276
277
it 'uses explicit RpcUser when provided alongside RpcHost+RpcPass' do
278
config = plugin.send(:resolve_rpc_config, { 'RpcHost' => '192.0.2.0', 'RpcUser' => 'custom', 'RpcPass' => 'remote_pass' })
279
expect(config[:user]).to eq('custom')
280
end
281
end
282
end
283
end
284
285