Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/api/json_rpc_spec.rb
57975 views
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
vuln: {},
229
message: 'The target is not exploitable.',
230
reason: nil
231
}
232
}
233
}
234
expect(last_json_response).to include(expected_completed_response)
235
end
236
end
237
238
context 'when the module does not support a check method' do
239
before do
240
mock_rack_env('development')
241
end
242
243
let(:module_name) { 'scanner/http/title' }
244
245
it 'returns successful job results' do
246
create_job
247
expect(last_response).to_not be_ok
248
expected_error_response = {
249
error: {
250
code: -32000,
251
data: {
252
backtrace: include(a_kind_of(String))
253
},
254
message: 'Application server error: This module does not support check.'
255
},
256
id: 1
257
}
258
expect(last_json_response).to include(expected_error_response)
259
end
260
end
261
262
context 'when the check command raises a known msf error' do
263
before(:each) do
264
allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do |mod|
265
mod.fail_with(Msf::Module::Failure::UnexpectedReply, 'Expected failure reason')
266
end
267
end
268
269
it 'returns the error results' do
270
create_job
271
expect(last_response).to be_ok
272
expect(last_json_response).to include(a_valid_result_uuid)
273
274
uuid = last_json_response['result']['uuid']
275
276
wait_for_expect do
277
get_job_results(uuid)
278
279
expect(last_response).to be_ok
280
expect_error_status(last_json_response)
281
end
282
283
expected_error_response = {
284
result: {
285
status: 'errored',
286
error: 'unexpected-reply: Expected failure reason'
287
}
288
}
289
expect(last_json_response).to include(expected_error_response)
290
end
291
end
292
293
context 'when the check command has an unexpected error' do
294
include_context 'Msf::Framework#threads cleaner'
295
296
before(:each) do
297
allow_any_instance_of(::Msf::Auxiliary::Scanner).to receive(:check) do
298
raise 'Unexpected module error'
299
end
300
end
301
302
it 'returns the error results' do
303
create_job
304
expect(last_response).to be_ok
305
expect(last_json_response).to include(a_valid_result_uuid)
306
307
uuid = last_json_response['result']['uuid']
308
309
wait_for_expect do
310
get_job_results(uuid)
311
312
expect(last_response).to be_ok
313
expect_error_status(last_json_response)
314
end
315
316
expected_error_response = {
317
result: {
318
status: 'errored',
319
error: "Unexpected module error"
320
}
321
}
322
expect(last_json_response).to include(expected_error_response)
323
end
324
end
325
326
context 'when there is a sinatra level application error in the development environment' do
327
before(:each) do
328
allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')
329
mock_rack_env('development')
330
end
331
332
it 'returns the error results' do
333
create_job
334
335
expect(last_response).to be_server_error
336
expected_error_response = {
337
error: {
338
code: -32000,
339
data: {
340
backtrace: include(a_kind_of(String))
341
},
342
message: 'Application server error: Sinatra level exception raised'
343
},
344
id: 1
345
}
346
expect(last_json_response).to include(expected_error_response)
347
end
348
end
349
350
context 'when rack middleware raises an error in the development environment' do
351
before(:each) do
352
allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')
353
mock_rack_env('development')
354
end
355
356
it 'returns the error results' do
357
create_job
358
359
expect(last_response).to be_server_error
360
expected_error_response = {
361
error: {
362
code: -32000,
363
data: {
364
backtrace: include(a_kind_of(String))
365
},
366
message: 'Application server error: Middleware error raised'
367
},
368
id: 1
369
}
370
expect(last_json_response).to include(expected_error_response)
371
end
372
end
373
374
context 'when rack middleware raises an error in the production environment' do
375
before(:each) do
376
allow_any_instance_of(::Rack::Protection::AuthenticityToken).to receive(:accepts?).and_raise(Exception, 'Middleware error raised')
377
mock_rack_env('production')
378
end
379
380
it 'returns the error results' do
381
create_job
382
383
expect(last_response).to be_server_error
384
expected_error_response = {
385
error: {
386
code: -32000,
387
message: 'Application server error: Middleware error raised'
388
},
389
id: 1
390
}
391
expect(last_json_response).to include(expected_error_response)
392
end
393
end
394
395
context 'when there is a sinatra level application error in the production environment' do
396
before(:each) do
397
allow_any_instance_of(Msf::RPC::JSON::Dispatcher).to receive(:process).and_raise(Exception, 'Sinatra level exception raised')
398
mock_rack_env('production')
399
end
400
401
it 'returns the error results' do
402
create_job
403
404
expect(last_response).to be_server_error
405
expected_error_response = {
406
error: {
407
code: -32000,
408
message: 'Application server error: Sinatra level exception raised'
409
},
410
id: 1
411
}
412
expect(last_json_response).to include(expected_error_response)
413
end
414
end
415
end
416
417
describe 'analyze' do
418
let(:host_ip) { Faker::Internet.private_ip_v4_address }
419
let(:host) do
420
{
421
workspace: 'default',
422
host: host_ip,
423
state: 'alive',
424
os_name: 'Windows',
425
os_flavor: 'Enterprize',
426
os_sp: 'SP2',
427
os_lang: 'English',
428
arch: 'ARCH_X86',
429
mac: '97-42-51-F2-A7-A7',
430
scope: 'eth2',
431
virtual_host: 'VMWare'
432
}
433
end
434
435
let(:vuln) do
436
{
437
workspace: 'default',
438
host: host_ip,
439
name: 'Exploit Name',
440
info: 'Human readable description of the vuln',
441
refs: vuln_refs
442
}
443
end
444
445
context 'when there are modules available' do
446
let(:vuln_refs) do
447
%w[
448
CVE-2017-0143
449
]
450
end
451
452
before(:each) do
453
framework.modules.add_module_path('./modules')
454
end
455
456
context 'with no options' do
457
it 'returns the list of known modules associated with a reported host' do
458
report_host(host)
459
expect(last_response).to be_ok
460
461
report_vuln(vuln)
462
expect(last_response).to be_ok
463
464
expected_response = {
465
jsonrpc: '2.0',
466
result: {
467
host: {
468
address: host_ip,
469
modules: [
470
{
471
mname: "exploit/windows/smb/ms17_010_eternalblue",
472
mtype: "exploit",
473
options: {
474
invalid: [],
475
missing: [],
476
},
477
state: "READY_FOR_TEST",
478
description: "ready for testing"
479
},
480
{
481
mname: "exploit/windows/smb/ms17_010_psexec",
482
mtype: "exploit",
483
options: {
484
invalid: [],
485
missing: [ "credential" ],
486
},
487
state: "REQUIRES_CRED",
488
description: "credentials are required"
489
},
490
{
491
mname: "exploit/windows/smb/smb_doublepulsar_rce",
492
mtype: "exploit",
493
options: {
494
invalid: [],
495
missing: [],
496
},
497
state: "READY_FOR_TEST",
498
description: "ready for testing"
499
}
500
]
501
}
502
},
503
id: 1
504
}
505
506
analyze_host(
507
{
508
workspace: 'default',
509
host: host_ip
510
}
511
)
512
expect(last_json_response).to include(expected_response)
513
end
514
end
515
516
context 'when payloads requirements are specified' do
517
it 'returns the list of known modules associated with a reported host' do
518
report_host(host)
519
expect(last_response).to be_ok
520
521
report_vuln(vuln)
522
expect(last_response).to be_ok
523
524
# Note: Currently the API doesn't return any differentiating output that a particular module is suitable
525
# with the requested payload
526
expected_response = {
527
jsonrpc: '2.0',
528
result: {
529
host: {
530
address: host_ip,
531
modules: [
532
{
533
mname: "exploit/windows/smb/ms17_010_eternalblue",
534
mtype: "exploit",
535
options: {
536
invalid: [],
537
missing: [ "payload_match" ],
538
},
539
state: "MISSING_PAYLOAD",
540
description: "none of the requested payloads match"
541
},
542
{
543
mname: "exploit/windows/smb/ms17_010_psexec",
544
mtype: "exploit",
545
options: {
546
invalid: [],
547
missing: [ "credential", "payload_match" ],
548
},
549
state: "REQUIRES_CRED",
550
description: "credentials are required, none of the requested payloads match"
551
},
552
{
553
mname: "exploit/windows/smb/smb_doublepulsar_rce",
554
mtype: "exploit",
555
options: {
556
invalid: [],
557
missing: ["payload_match"],
558
},
559
state: "MISSING_PAYLOAD",
560
description: "none of the requested payloads match"
561
}
562
]
563
}
564
},
565
id: 1
566
}
567
568
analyze_host(
569
{
570
workspace: 'default',
571
host: host_ip,
572
analyze_options: {
573
payloads: [
574
'windows/meterpreter_reverse_http'
575
]
576
}
577
}
578
)
579
expect(last_json_response).to include(expected_response)
580
end
581
end
582
end
583
584
context 'when there are no modules found' do
585
let(:vuln_refs) do
586
['CVE-NO-MATCHING-MODULES-1234']
587
end
588
589
it 'returns an empty list of modules' do
590
report_host(host)
591
expect(last_response).to be_ok
592
593
report_vuln(vuln)
594
expect(last_response).to be_ok
595
596
expected_response = {
597
jsonrpc: '2.0',
598
result: {
599
host: {
600
address: host_ip,
601
modules: []
602
}
603
},
604
id: 1
605
}
606
607
analyze_host(
608
{
609
workspace: 'default',
610
host: host_ip
611
}
612
)
613
expect(last_json_response).to include(expected_response)
614
end
615
end
616
end
617
end
618
619