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/acceptance/meterpreter_spec.rb
Views: 11766
1
require 'acceptance_spec_helper'
2
require 'base64'
3
4
RSpec.describe 'Meterpreter' do
5
include_context 'wait_for_expect'
6
7
# Tests to ensure that Meterpreter is consistent across all implementations/operation systems
8
METERPRETER_PAYLOADS = Acceptance::Session.with_session_name_merged(
9
{
10
python: Acceptance::Session::PYTHON_METERPRETER,
11
php: Acceptance::Session::PHP_METERPRETER,
12
java: Acceptance::Session::JAVA_METERPRETER,
13
mettle: Acceptance::Session::METTLE_METERPRETER,
14
windows_meterpreter: Acceptance::Session::WINDOWS_METERPRETER
15
}
16
)
17
18
allure_test_environment = AllureRspec.configuration.environment_properties
19
20
let_it_be(:current_platform) { Acceptance::Session::current_platform }
21
22
# @!attribute [r] port_allocator
23
# @return [Acceptance::PortAllocator]
24
let_it_be(:port_allocator) { Acceptance::PortAllocator.new }
25
26
# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly
27
let_it_be(:driver) do
28
driver = Acceptance::ConsoleDriver.new
29
driver
30
end
31
32
# Opens a test console with the test loadpath specified
33
# @!attribute [r] console
34
# @return [Acceptance::Console]
35
let_it_be(:console) do
36
console = driver.open_console
37
38
# Load the test modules
39
console.sendline('loadpath test/modules')
40
console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)
41
console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)
42
console.recvuntil(/\d+ exploit modules[^\n]*\n/)
43
console.recvuntil(/\d+ post modules[^\n]*\n/)
44
console.recvuntil(Acceptance::Console.prompt)
45
46
# Read the remaining console
47
# console.sendline "quit -y"
48
# console.recv_available
49
50
console
51
end
52
53
METERPRETER_PAYLOADS.each do |meterpreter_name, meterpreter_config|
54
meterpreter_runtime_name = "#{meterpreter_name}#{ENV.fetch('METERPRETER_RUNTIME_VERSION', '')}"
55
56
describe meterpreter_runtime_name, focus: meterpreter_config[:focus] do
57
meterpreter_config[:payloads].each.with_index do |payload_config, payload_config_index|
58
describe(
59
Acceptance::Session.human_name_for_payload(payload_config).to_s,
60
if: (
61
Acceptance::Session.run_meterpreter?(meterpreter_config) &&
62
Acceptance::Session.supported_platform?(payload_config)
63
)
64
) do
65
let(:payload) { Acceptance::Payload.new(payload_config) }
66
67
class LocalPath
68
attr_reader :path
69
70
def initialize(path)
71
@path = path
72
end
73
end
74
75
let(:session_tlv_logging_file) do
76
# LocalPath.new('/tmp/php_session_tlv_log.txt')
77
Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')
78
end
79
80
let(:meterpreter_logging_file) do
81
# LocalPath.new('/tmp/php_log.txt')
82
Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')
83
end
84
85
let(:payload_stdout_and_stderr_file) do
86
# LocalPath.new('/tmp/php_log.txt')
87
Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')
88
end
89
90
let(:default_global_datastore) do
91
{
92
SessionTlvLogging: "file:#{session_tlv_logging_file.path}"
93
}
94
end
95
96
let(:test_environment) { allure_test_environment }
97
98
let(:default_module_datastore) do
99
{
100
AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,
101
lport: port_allocator.next,
102
lhost: '127.0.0.1',
103
MeterpreterDebugLogging: "rpath:#{meterpreter_logging_file.path}"
104
}
105
end
106
107
let(:executed_payload) do
108
file = File.open(payload_stdout_and_stderr_file.path, 'w')
109
driver.run_payload(
110
payload,
111
{
112
out: file,
113
err: file
114
}
115
)
116
end
117
118
# The shared payload process and session instance that will be reused across the test run
119
#
120
let(:payload_process_and_session_id) do
121
console.sendline "use #{payload.name}"
122
console.recvuntil(Acceptance::Console.prompt)
123
124
# Set global options
125
console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)
126
console.recvuntil(Acceptance::Console.prompt)
127
128
# Generate the payload
129
console.sendline payload.generate_command(default_module_datastore: default_module_datastore)
130
console.recvuntil(/Writing \d+ bytes[^\n]*\n/)
131
generate_result = console.recvuntil(Acceptance::Console.prompt)
132
133
expect(generate_result.lines).to_not include(match('generation failed'))
134
wait_for_expect do
135
expect(payload.size).to be > 0
136
end
137
138
console.sendline payload.handler_command(default_module_datastore: default_module_datastore)
139
console.recvuntil(/Started reverse TCP handler[^\n]*\n/)
140
payload_process = executed_payload
141
session_id = nil
142
143
# Wait for the session to open, or break early if the payload is detected as dead
144
wait_for_expect do
145
unless payload_process.alive?
146
break
147
end
148
149
session_opened_matcher = /Meterpreter session (\d+) opened[^\n]*\n/
150
session_message = ''
151
begin
152
session_message = console.recvuntil(session_opened_matcher, timeout: 1)
153
rescue Acceptance::ChildProcessRecvError
154
# noop
155
end
156
157
session_id = session_message[session_opened_matcher, 1]
158
expect(session_id).to_not be_nil
159
end
160
161
[payload_process, session_id]
162
end
163
164
# @param [String] path The file path to read the content of
165
# @return [String] The file contents if found
166
def get_file_attachment_contents(path)
167
return 'none resent' unless File.exist?(path)
168
169
content = File.binread(path)
170
content.blank? ? 'file created - but empty' : content
171
end
172
173
before :each do |example|
174
next unless example.respond_to?(:parameter)
175
176
# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI
177
test_environment.each do |key, value|
178
example.parameter(key, value)
179
end
180
end
181
182
after :all do
183
driver.close_payloads
184
console.reset
185
end
186
187
context "#{Acceptance::Session.current_platform}" do
188
describe "#{Acceptance::Session.current_platform}/#{meterpreter_runtime_name} Meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload" do
189
it(
190
"exposes available metasploit commands",
191
if: (
192
# Assume that regardless of payload, staged/unstaged/etc, the Meterpreter will have the same commands available
193
# So only run this test when config_index == 0
194
payload_config_index == 0 && Acceptance::Session.supported_platform?(payload_config)
195
# Run if ENV['SESSION'] = 'java php' etc
196
Acceptance::Session.run_meterpreter?(meterpreter_config) &&
197
# Only run payloads / tests, if the host machine can run them
198
Acceptance::Session.supported_platform?(payload_config)
199
)
200
) do
201
begin
202
replication_commands = []
203
current_payload_status = ''
204
205
# Ensure we have a valid session id; We intentionally omit this from a `before(:each)` to ensure the allure attachments are generated if the session dies
206
payload_process, session_id = payload_process_and_session_id
207
expect(payload_process).to(be_alive, proc do
208
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
209
210
Allure.add_attachment(
211
name: 'Failed payload blob',
212
source: Base64.strict_encode64(File.binread(payload_process.payload_path)),
213
type: Allure::ContentType::TXT
214
)
215
216
current_payload_status
217
end)
218
expect(session_id).to_not(be_nil, proc do
219
"There should be a session present"
220
end)
221
222
resource_command = "resource scripts/resource/meterpreter_compatibility.rc"
223
replication_commands << resource_command
224
console.sendline(resource_command)
225
result = console.recvuntil(Acceptance::Console.prompt)
226
227
available_commands = result.lines(chomp: true).find do |line|
228
line.start_with?("{") && line.end_with?("}") && JSON.parse(line)
229
rescue JSON::ParserError => _e
230
next
231
end
232
expect(available_commands).to_not be_nil
233
234
available_commands_json = JSON.parse(available_commands, symbolize_names: true)
235
# Generate an allure attachment, a report can be generated afterwards
236
Allure.add_attachment(
237
name: 'available commands',
238
source: JSON.pretty_generate(available_commands_json),
239
type: Allure::ContentType::JSON,
240
test_case: false
241
)
242
expect(available_commands_json[:sessions].length).to be 1
243
expect(available_commands_json[:sessions].first[:commands]).to_not be_empty
244
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
245
test_run_error = e
246
end
247
248
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
249
# still generated if the session dies in a weird way etc
250
251
# Payload process cleanup / verification
252
# The payload process wasn't initially marked as dead - let's close it
253
if payload_process.present? && current_payload_status.blank?
254
begin
255
if payload_process.alive?
256
current_payload_status = "Process still alive after running test suite"
257
payload_process.close
258
else
259
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
260
end
261
rescue => e
262
Allure.add_attachment(
263
name: 'driver.close_payloads failure information',
264
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
265
type: Allure::ContentType::TXT
266
)
267
end
268
end
269
270
console_reset_error = nil
271
current_console_data = console.all_data
272
begin
273
console.reset
274
rescue => e
275
console_reset_error = e
276
Allure.add_attachment(
277
name: 'console.reset failure information',
278
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
279
type: Allure::ContentType::TXT
280
)
281
end
282
283
payload_configuration_details = payload.as_readable_text(
284
default_global_datastore: default_global_datastore,
285
default_module_datastore: default_module_datastore
286
)
287
288
replication_steps = <<~EOF
289
## Load test modules
290
loadpath test/modules
291
292
#{payload_configuration_details}
293
294
## Replication commands
295
#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}
296
EOF
297
298
Allure.add_attachment(
299
name: 'payload configuration and replication',
300
source: replication_steps,
301
type: Allure::ContentType::TXT
302
)
303
304
Allure.add_attachment(
305
name: 'payload output if available',
306
source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",
307
type: Allure::ContentType::TXT
308
)
309
310
Allure.add_attachment(
311
name: 'payload debug log if available',
312
source: get_file_attachment_contents(meterpreter_logging_file.path),
313
type: Allure::ContentType::TXT
314
)
315
316
Allure.add_attachment(
317
name: 'session tlv logging if available',
318
source: get_file_attachment_contents(session_tlv_logging_file.path),
319
type: Allure::ContentType::TXT
320
)
321
322
Allure.add_attachment(
323
name: 'console data',
324
source: current_console_data,
325
type: Allure::ContentType::TXT
326
)
327
328
raise test_run_error if test_run_error
329
raise console_reset_error if console_reset_error
330
end
331
end
332
333
meterpreter_config[:module_tests].each do |module_test|
334
describe module_test[:name].to_s, focus: module_test[:focus] do
335
it(
336
"#{Acceptance::Session.current_platform}/#{meterpreter_runtime_name} meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests",
337
if: (
338
# Run if ENV['SESSION'] = 'java php' etc
339
Acceptance::Session.run_meterpreter?(meterpreter_config) &&
340
# Run if ENV['SESSION_MODULE_TEST'] = 'test/cmd_exec' etc
341
Acceptance::Session.run_meterpreter_module_test?(module_test[:name]) &&
342
# Only run payloads / tests, if the host machine can run them
343
Acceptance::Session.supported_platform?(payload_config) &&
344
Acceptance::Session.supported_platform?(module_test) &&
345
# Skip tests that are explicitly skipped, or won't pass in the current environment
346
!Acceptance::Session.skipped_module_test?(module_test, allure_test_environment)
347
),
348
# test metadata - will appear in allure report
349
module_test: module_test[:name]
350
) do
351
begin
352
replication_commands = []
353
current_payload_status = ''
354
355
known_failures = module_test.dig(:lines, :all, :known_failures) || []
356
known_failures += module_test.dig(:lines, current_platform, :known_failures) || []
357
known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
358
359
required_lines = module_test.dig(:lines, :all, :required) || []
360
required_lines += module_test.dig(:lines, current_platform, :required) || []
361
required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
362
363
# Ensure we have a valid session id; We intentionally omit this from a `before(:each)` to ensure the allure attachments are generated if the session dies
364
payload_process, session_id = payload_process_and_session_id
365
366
expect(payload_process).to(be_alive, proc do
367
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
368
369
Allure.add_attachment(
370
name: 'Failed payload blob',
371
source: Base64.strict_encode64(File.binread(payload_process.payload_path)),
372
type: Allure::ContentType::TXT
373
)
374
375
current_payload_status
376
end)
377
expect(session_id).to_not(be_nil, proc do
378
"There should be a session present"
379
end)
380
381
use_module = "use #{module_test[:name]}"
382
run_module = "run session=#{session_id} AddEntropy=true Verbose=true"
383
384
replication_commands << use_module
385
console.sendline(use_module)
386
console.recvuntil(Acceptance::Console.prompt)
387
388
replication_commands << run_module
389
console.sendline(run_module)
390
391
# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:
392
# console.interact
393
394
# Expect the test module to complete
395
test_result = console.recvuntil('Post module execution completed')
396
397
# Ensure there are no failures, and assert tests are complete
398
aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do
399
# Skip any ignored lines from the validation input
400
validated_lines = test_result.lines.reject do |line|
401
is_acceptable = known_failures.any? do |acceptable_failure|
402
line.include?(acceptable_failure.value) &&
403
acceptable_failure.if?(test_environment)
404
end || line.match?(/Passed: \d+; Failed: \d+/)
405
406
is_acceptable
407
end
408
409
validated_lines.each do |test_line|
410
test_line = Acceptance::Session.uncolorize(test_line)
411
expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"
412
end
413
414
# Assert all expected lines are present
415
required_lines.each do |required|
416
next unless required.if?(test_environment)
417
418
expect(test_result).to include(required.value)
419
end
420
421
# Assert all ignored lines are present, if they are not present - they should be removed from
422
# the calling config
423
known_failures.each do |acceptable_failure|
424
next if acceptable_failure.flaky?(test_environment)
425
next unless acceptable_failure.if?(test_environment)
426
427
expect(test_result).to include(acceptable_failure.value)
428
end
429
end
430
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
431
test_run_error = e
432
end
433
434
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
435
# still generated if the session dies in a weird way etc
436
437
# Payload process cleanup / verification
438
# The payload process wasn't initially marked as dead - let's close it
439
if payload_process.present? && current_payload_status.blank?
440
begin
441
if payload_process.alive?
442
current_payload_status = "Process still alive after running test suite"
443
payload_process.close
444
else
445
current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"
446
end
447
rescue => e
448
Allure.add_attachment(
449
name: 'driver.close_payloads failure information',
450
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
451
type: Allure::ContentType::TXT
452
)
453
end
454
end
455
456
console_reset_error = nil
457
current_console_data = console.all_data
458
begin
459
console.reset
460
rescue => e
461
console_reset_error = e
462
Allure.add_attachment(
463
name: 'console.reset failure information',
464
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
465
type: Allure::ContentType::TXT
466
)
467
end
468
469
payload_configuration_details = payload.as_readable_text(
470
default_global_datastore: default_global_datastore,
471
default_module_datastore: default_module_datastore
472
)
473
474
replication_steps = <<~EOF
475
## Load test modules
476
loadpath test/modules
477
478
#{payload_configuration_details}
479
480
## Replication commands
481
#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}
482
EOF
483
484
Allure.add_attachment(
485
name: 'payload configuration and replication',
486
source: replication_steps,
487
type: Allure::ContentType::TXT
488
)
489
490
Allure.add_attachment(
491
name: 'payload output if available',
492
source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",
493
type: Allure::ContentType::TXT
494
)
495
496
Allure.add_attachment(
497
name: 'payload debug log if available',
498
source: get_file_attachment_contents(meterpreter_logging_file.path),
499
type: Allure::ContentType::TXT
500
)
501
502
Allure.add_attachment(
503
name: 'session tlv logging if available',
504
source: get_file_attachment_contents(session_tlv_logging_file.path),
505
type: Allure::ContentType::TXT
506
)
507
508
Allure.add_attachment(
509
name: 'console data',
510
source: current_console_data,
511
type: Allure::ContentType::TXT
512
)
513
514
test_assertions = JSON.pretty_generate(
515
{
516
required_lines: required_lines.map(&:to_h),
517
known_failures: known_failures.map(&:to_h),
518
}
519
)
520
Allure.add_attachment(
521
name: 'test assertions',
522
source: test_assertions,
523
type: Allure::ContentType::TXT
524
)
525
526
raise test_run_error if test_run_error
527
raise console_reset_error if console_reset_error
528
end
529
end
530
end
531
end
532
end
533
end
534
end
535
end
536
end
537
538