CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/api/json_rpc_spec.rb
Views: 11766
1
require 'spec_helper'
2
require 'rack/test'
3
require 'rack/protection'
4
5
# These tests ensure the full end to end functionality of metasploit's JSON RPC
6
# endpoint. There are multiple layers of possible failure in our API, and unit testing
7
# alone will not cover all edge cases. For instance, middleware may raise exceptions
8
# and return HTML to the calling client unintentionally - which will break our JSON
9
# response contract. These test should help catch such scenarios.
10
RSpec.describe "Metasploit's json-rpc" do
11
include Rack::Test::Methods
12
include_context 'Msf::DBManager'
13
include_context 'Metasploit::Framework::Spec::Constants cleaner'
14
include_context 'Msf::Framework#threads cleaner', verify_cleanup_required: false
15
include_context 'wait_for_expect'
16
17
let(:health_check_url) { '/api/v1/health' }
18
let(:rpc_url) { '/api/v1/json-rpc' }
19
let(:module_name) { 'scanner/ssl/openssl_heartbleed' }
20
let(:a_valid_result_uuid) { { result: hash_including({ uuid: match(/\w+/) }) } }
21
let(:app) { ::Msf::WebServices::JsonRpcApp.new }
22
23
before(:example) do
24
framework.modules.add_module_path(File.join(FILE_FIXTURES_PATH, 'json_rpc'))
25
app.settings.framework = framework
26
end
27
28
after(:example) do
29
# Sinatra's settings are implemented as a singleton, and must be explicitly reset between runs
30
app.settings.dispatchers.clear
31
end
32
33
def report_host(host)
34
post rpc_url, {
35
jsonrpc: '2.0',
36
method: 'db.report_host',
37
id: 1,
38
params: [
39
host
40
]
41
}.to_json
42
end
43
44
def report_vuln(vuln)
45
post rpc_url, {
46
jsonrpc: '2.0',
47
method: 'db.report_vuln',
48
id: 1,
49
params: [
50
vuln
51
]
52
}.to_json
53
end
54
55
def analyze_host(host)
56
post rpc_url, {
57
jsonrpc: '2.0',
58
method: 'db.analyze_host',
59
id: 1,
60
params: [
61
host
62
]
63
}.to_json
64
end
65
66
def create_job
67
post rpc_url, {
68
jsonrpc: '2.0',
69
method: 'module.check',
70
id: 1,
71
params: [
72
'auxiliary',
73
module_name,
74
{
75
RHOSTS: '192.0.2.0'
76
}
77
]
78
}.to_json
79
end
80
81
def get_job_results(uuid)
82
post rpc_url, {
83
jsonrpc: '2.0',
84
method: 'module.results',
85
id: 1,
86
params: [
87
uuid
88
]
89
}.to_json
90
end
91
92
def get_rpc_health_check
93
post rpc_url, {
94
jsonrpc: '2.0',
95
method: 'health.check',
96
id: 1,
97
params: []
98
}.to_json
99
end
100
101
def get_rest_health_check
102
get health_check_url
103
end
104
105
def last_json_response
106
JSON.parse(last_response.body).with_indifferent_access
107
end
108
109
def expect_completed_status(rpc_response)
110
expect(rpc_response).to include({ result: hash_including({ status: 'completed' }) })
111
end
112
113
def expect_error_status(rpc_response)
114
expect(rpc_response).to include({ result: hash_including({ status: 'errored' }) })
115
end
116
117
def mock_rack_env(mock_rack_env_value)
118
allow(ENV).to receive(:[]).and_wrap_original do |original_env, key|
119
if key == 'RACK_ENV'
120
mock_rack_env_value
121
else
122
original_env[key]
123
end
124
end
125
end
126
127
describe 'health status' do
128
context 'when using the REST health check functionality' do
129
it 'passes the health check' do
130
expected_response = {
131
data: {
132
status: 'UP'
133
}
134
}
135
136
get_rest_health_check
137
expect(last_response).to be_ok
138
expect(last_json_response).to include(expected_response)
139
end
140
end
141
142
context 'when there is an issue' do
143
before(:each) do
144
allow(framework).to receive(:version).and_raise 'Mock error'
145
end
146
147
it 'fails the health check' do
148
expected_response = {
149
data: {
150
status: 'DOWN'
151
}
152
}
153
154
get_rest_health_check
155
156
expect(last_response.status).to be 503
157
expect(last_json_response).to include(expected_response)
158
end
159
end
160
161
context 'when using the RPC health check functionality' do
162
context 'when the service is healthy' do
163
it 'passes the health check' do
164
expected_response = {
165
id: 1,
166
jsonrpc: '2.0',
167
result: {
168
status: 'UP'
169
}
170
}
171
172
get_rpc_health_check
173
expect(last_response).to be_ok
174
expect(last_json_response).to include(expected_response)
175
end
176
end
177
178
context 'when there is an issue' do
179
before(:each) do
180
allow(framework).to receive(:version).and_raise 'Mock error'
181
end
182
183
it 'fails the health check' do
184
expected_response = {
185
id: 1,
186
jsonrpc: '2.0',
187
result: {
188
status: 'DOWN'
189
}
190
}
191
192
get_rpc_health_check
193
194
expect(last_response).to be_ok
195
expect(last_json_response).to include(expected_response)
196
end
197
end
198
end
199
end
200
201
describe 'Running a check job and verifying results' do
202
context 'when the module returns check code safe' do
203
before(:each) do
204
allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do
205
::Msf::Exploit::CheckCode::Safe
206
end
207
end
208
209
it 'returns successful job results' do
210
create_job
211
expect(last_response).to be_ok
212
expect(last_json_response).to include(a_valid_result_uuid)
213
214
uuid = last_json_response['result']['uuid']
215
wait_for_expect do
216
get_job_results(uuid)
217
218
expect(last_response).to be_ok
219
expect_completed_status(last_json_response)
220
end
221
222
expected_completed_response = {
223
result: {
224
status: 'completed',
225
result: {
226
code: 'safe',
227
details: {},
228
message: 'The target is not exploitable.',
229
reason: nil
230
}
231
}
232
}
233
expect(last_json_response).to include(expected_completed_response)
234
end
235
end
236
237
context 'when the module does not support a check method' do
238
before do
239
mock_rack_env('development')
240
end
241
242
let(:module_name) { 'scanner/http/title' }
243
244
it 'returns successful job results' do
245
create_job
246
expect(last_response).to_not be_ok
247
expected_error_response = {
248
error: {
249
code: -32000,
250
data: {
251
backtrace: include(a_kind_of(String))
252
},
253
message: 'Application server error: This module does not support check.'
254
},
255
id: 1
256
}
257
expect(last_json_response).to include(expected_error_response)
258
end
259
end
260
261
context 'when the check command raises a known msf error' do
262
before(:each) do
263
allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do |mod|
264
mod.fail_with(Msf::Module::Failure::UnexpectedReply, 'Expected failure reason')
265
end
266
end
267
268
it 'returns the error results' do
269
create_job
270
expect(last_response).to be_ok
271
expect(last_json_response).to include(a_valid_result_uuid)
272
273
uuid = last_json_response['result']['uuid']
274
275
wait_for_expect do
276
get_job_results(uuid)
277
278
expect(last_response).to be_ok
279
expect_error_status(last_json_response)
280
end
281
282
expected_error_response = {
283
result: {
284
status: 'errored',
285
error: 'unexpected-reply: Expected failure reason'
286
}
287
}
288
expect(last_json_response).to include(expected_error_response)
289
end
290
end
291
292
context 'when the check command has an unexpected error' do
293
include_context 'Msf::Framework#threads cleaner'
294
295
before(:each) do
296
allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do
297
raise 'Unexpected module error'
298
end
299
end
300
301
it 'returns the error results' do
302
create_job
303
expect(last_response).to be_ok
304
expect(last_json_response).to include(a_valid_result_uuid)
305
306
uuid = last_json_response['result']['uuid']
307
308
wait_for_expect do
309
get_job_results(uuid)
310
311
expect(last_response).to be_ok
312
expect_error_status(last_json_response)
313
end
314
315
expected_error_response = {
316
result: {
317
status: 'errored',
318
error: "Unexpected module error"
319
}
320
}
321
expect(last_json_response).to include(expected_error_response)
322
end
323
end
324
325
context 'when there is a sinatra level application error in the development environment' do
326
before(:each) do
327
allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')
328
mock_rack_env('development')
329
end
330
331
it 'returns the error results' do
332
create_job
333
334
expect(last_response).to be_server_error
335
expected_error_response = {
336
error: {
337
code: -32000,
338
data: {
339
backtrace: include(a_kind_of(String))
340
},
341
message: 'Application server error: Sinatra level exception raised'
342
},
343
id: 1
344
}
345
expect(last_json_response).to include(expected_error_response)
346
end
347
end
348
349
context 'when rack middleware raises an error in the development environment' do
350
before(:each) do
351
allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')
352
mock_rack_env('development')
353
end
354
355
it 'returns the error results' do
356
create_job
357
358
expect(last_response).to be_server_error
359
expected_error_response = {
360
error: {
361
code: -32000,
362
data: {
363
backtrace: include(a_kind_of(String))
364
},
365
message: 'Application server error: Middleware error raised'
366
},
367
id: 1
368
}
369
expect(last_json_response).to include(expected_error_response)
370
end
371
end
372
373
context 'when rack middleware raises an error in the production environment' do
374
before(:each) do
375
allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')
376
mock_rack_env('production')
377
end
378
379
it 'returns the error results' do
380
create_job
381
382
expect(last_response).to be_server_error
383
expected_error_response = {
384
error: {
385
code: -32000,
386
message: 'Application server error: Middleware error raised'
387
},
388
id: 1
389
}
390
expect(last_json_response).to include(expected_error_response)
391
end
392
end
393
394
context 'when there is a sinatra level application error in the production environment' do
395
before(:each) do
396
allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')
397
mock_rack_env('production')
398
end
399
400
it 'returns the error results' do
401
create_job
402
403
expect(last_response).to be_server_error
404
expected_error_response = {
405
error: {
406
code: -32000,
407
message: 'Application server error: Sinatra level exception raised'
408
},
409
id: 1
410
}
411
expect(last_json_response).to include(expected_error_response)
412
end
413
end
414
end
415
416
describe 'analyze' do
417
let(:host_ip) { Faker::Internet.private_ip_v4_address }
418
let(:host) do
419
{
420
workspace: 'default',
421
host: host_ip,
422
state: 'alive',
423
os_name: 'Windows',
424
os_flavor: 'Enterprize',
425
os_sp: 'SP2',
426
os_lang: 'English',
427
arch: 'ARCH_X86',
428
mac: '97-42-51-F2-A7-A7',
429
scope: 'eth2',
430
virtual_host: 'VMWare'
431
}
432
end
433
434
let(:vuln) do
435
{
436
workspace: 'default',
437
host: host_ip,
438
name: 'Exploit Name',
439
info: 'Human readable description of the vuln',
440
refs: vuln_refs
441
}
442
end
443
444
context 'when there are modules available' do
445
let(:vuln_refs) do
446
%w[
447
CVE-2017-0143
448
]
449
end
450
451
before(:each) do
452
framework.modules.add_module_path('./modules')
453
end
454
455
context 'with no options' do
456
it 'returns the list of known modules associated with a reported host' do
457
report_host(host)
458
expect(last_response).to be_ok
459
460
report_vuln(vuln)
461
expect(last_response).to be_ok
462
463
expected_response = {
464
jsonrpc: '2.0',
465
result: {
466
host: {
467
address: host_ip,
468
modules: [
469
{
470
mname: "exploit/windows/smb/ms17_010_eternalblue",
471
mtype: "exploit",
472
options: {
473
invalid: [],
474
missing: [],
475
},
476
state: "READY_FOR_TEST",
477
description: "ready for testing"
478
},
479
{
480
mname: "exploit/windows/smb/ms17_010_psexec",
481
mtype: "exploit",
482
options: {
483
invalid: [],
484
missing: [ "credential" ],
485
},
486
state: "REQUIRES_CRED",
487
description: "credentials are required"
488
},
489
{
490
mname: "exploit/windows/smb/smb_doublepulsar_rce",
491
mtype: "exploit",
492
options: {
493
invalid: [],
494
missing: [],
495
},
496
state: "READY_FOR_TEST",
497
description: "ready for testing"
498
}
499
]
500
}
501
},
502
id: 1
503
}
504
505
analyze_host(
506
{
507
workspace: 'default',
508
host: host_ip
509
}
510
)
511
expect(last_json_response).to include(expected_response)
512
end
513
end
514
515
context 'when payloads requirements are specified' do
516
it 'returns the list of known modules associated with a reported host' do
517
report_host(host)
518
expect(last_response).to be_ok
519
520
report_vuln(vuln)
521
expect(last_response).to be_ok
522
523
# Note: Currently the API doesn't return any differentiating output that a particular module is suitable
524
# with the requested payload
525
expected_response = {
526
jsonrpc: '2.0',
527
result: {
528
host: {
529
address: host_ip,
530
modules: [
531
{
532
mname: "exploit/windows/smb/ms17_010_eternalblue",
533
mtype: "exploit",
534
options: {
535
invalid: [],
536
missing: [ "payload_match" ],
537
},
538
state: "MISSING_PAYLOAD",
539
description: "none of the requested payloads match"
540
},
541
{
542
mname: "exploit/windows/smb/ms17_010_psexec",
543
mtype: "exploit",
544
options: {
545
invalid: [],
546
missing: [ "credential", "payload_match" ],
547
},
548
state: "REQUIRES_CRED",
549
description: "credentials are required, none of the requested payloads match"
550
},
551
{
552
mname: "exploit/windows/smb/smb_doublepulsar_rce",
553
mtype: "exploit",
554
options: {
555
invalid: [],
556
missing: ["payload_match"],
557
},
558
state: "MISSING_PAYLOAD",
559
description: "none of the requested payloads match"
560
}
561
]
562
}
563
},
564
id: 1
565
}
566
567
analyze_host(
568
{
569
workspace: 'default',
570
host: host_ip,
571
analyze_options: {
572
payloads: [
573
'windows/meterpreter_reverse_http'
574
]
575
}
576
}
577
)
578
expect(last_json_response).to include(expected_response)
579
end
580
end
581
end
582
583
context 'when there are no modules found' do
584
let(:vuln_refs) do
585
['CVE-NO-MATCHING-MODULES-1234']
586
end
587
588
it 'returns an empty list of modules' do
589
report_host(host)
590
expect(last_response).to be_ok
591
592
report_vuln(vuln)
593
expect(last_response).to be_ok
594
595
expected_response = {
596
jsonrpc: '2.0',
597
result: {
598
host: {
599
address: host_ip,
600
modules: []
601
}
602
},
603
id: 1
604
}
605
606
analyze_host(
607
{
608
workspace: 'default',
609
host: host_ip
610
}
611
)
612
expect(last_json_response).to include(expected_response)
613
end
614
end
615
end
616
end
617
618