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/command_shell_spec.rb
Views: 11766
1
require 'acceptance_spec_helper'
2
require 'base64'
3
4
RSpec.describe 'CommandShell' do
5
include_context 'wait_for_expect'
6
7
# Tests to ensure that CMD/Powershell/Linux is consistent across all implementations/operation systems
8
COMMAND_SHELL_PAYLOADS = Acceptance::Session.with_session_name_merged(
9
{
10
powershell: Acceptance::Session::POWERSHELL,
11
cmd: Acceptance::Session::CMD,
12
linux: Acceptance::Session::LINUX
13
}
14
)
15
16
allure_test_environment = AllureRspec.configuration.environment_properties
17
18
let_it_be(:current_platform) { Acceptance::Session::current_platform }
19
20
# @!attribute [r] port_allocator
21
# @return [Acceptance::PortAllocator]
22
let_it_be(:port_allocator) { Acceptance::PortAllocator.new }
23
24
# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly
25
let_it_be(:driver) do
26
driver = Acceptance::ConsoleDriver.new
27
driver
28
end
29
30
# Opens a test console with the test loadpath specified
31
# @!attribute [r] console
32
# @return [Acceptance::Console]
33
let_it_be(:console) do
34
console = driver.open_console
35
36
# Load the test modules
37
console.sendline('loadpath test/modules')
38
console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)
39
console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)
40
console.recvuntil(/\d+ exploit modules[^\n]*\n/)
41
console.recvuntil(/\d+ post modules[^\n]*\n/)
42
console.recvuntil(Acceptance::Console.prompt)
43
44
# Read the remaining console
45
# console.sendline "quit -y"
46
# console.recv_available
47
48
console
49
end
50
51
COMMAND_SHELL_PAYLOADS.each do |command_shell_name, command_shell_config|
52
command_shell_runtime_name = "#{command_shell_name}#{ENV.fetch('COMMAND_SHELL_RUNTIME_VERSION', '')}"
53
54
describe command_shell_runtime_name, focus: command_shell_config[:focus] do
55
command_shell_config[:payloads].each.with_index do |payload_config, payload_config_index|
56
describe(
57
Acceptance::Session.human_name_for_payload(payload_config).to_s,
58
if: (
59
Acceptance::Session.run_session?(command_shell_config) &&
60
Acceptance::Session.supported_platform?(payload_config)
61
)
62
) do
63
let(:payload) { Acceptance::Payload.new(payload_config) }
64
65
class LocalPath
66
attr_reader :path
67
68
def initialize(path)
69
@path = path
70
end
71
end
72
73
let(:session_tlv_logging_file) do
74
# LocalPath.new('/tmp/php_session_tlv_log.txt')
75
Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')
76
end
77
78
let(:command_shell_logging_file) do
79
# LocalPath.new('/tmp/php_log.txt')
80
Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')
81
end
82
83
let(:payload_stdout_and_stderr_file) do
84
# LocalPath.new('/tmp/php_log.txt')
85
Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')
86
end
87
88
let(:default_global_datastore) do
89
{
90
SessionTlvLogging: "file:#{session_tlv_logging_file.path}"
91
}
92
end
93
94
let(:test_environment) { allure_test_environment }
95
96
let(:default_module_datastore) do
97
{
98
AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,
99
lport: port_allocator.next,
100
lhost: '127.0.0.1'
101
}
102
end
103
104
let(:executed_payload) do
105
file = File.open(payload_stdout_and_stderr_file.path, 'w')
106
driver.run_payload(
107
payload,
108
{
109
out: file,
110
err: file
111
}
112
)
113
end
114
115
# The shared payload process and session instance that will be reused across the test run
116
#
117
let(:payload_process_and_session_id) do
118
console.sendline "use #{payload.name}"
119
console.recvuntil(Acceptance::Console.prompt)
120
121
# Set global options
122
console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)
123
console.recvuntil(Acceptance::Console.prompt)
124
125
# Generate the payload
126
console.sendline payload.generate_command(default_module_datastore: default_module_datastore)
127
console.recvuntil(/Writing \d+ bytes[^\n]*\n/)
128
generate_result = console.recvuntil(Acceptance::Console.prompt)
129
130
expect(generate_result.lines).to_not include(match('generation failed'))
131
wait_for_expect do
132
expect(payload.size).to be > 0
133
end
134
135
console.sendline payload.handler_command(default_module_datastore: default_module_datastore)
136
console.recvuntil(/Started reverse TCP handler[^\n]*\n/)
137
payload_process = executed_payload
138
session_id = nil
139
140
# Wait for the session to open, or break early if the payload is detected as dead
141
larger_retry_count_for_powershell = 600
142
wait_for_expect(larger_retry_count_for_powershell) do
143
unless payload_process.alive?
144
break
145
end
146
147
session_opened_matcher = /session (\d+) opened[^\n]*\n/
148
session_message = ''
149
begin
150
session_message = console.recvuntil(session_opened_matcher, timeout: 1)
151
rescue Acceptance::ChildProcessRecvError
152
# noop
153
end
154
155
session_id = session_message[session_opened_matcher, 1]
156
expect(session_id).to_not be_nil
157
end
158
159
[payload_process, session_id]
160
end
161
162
# @param [String] path The file path to read the content of
163
# @return [String] The file contents if found
164
def get_file_attachment_contents(path)
165
return 'none resent' unless File.exist?(path)
166
167
content = File.binread(path)
168
content.blank? ? 'file created - but empty' : content
169
end
170
171
before :each do |example|
172
next unless example.respond_to?(:parameter)
173
174
# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI
175
test_environment.each do |key, value|
176
example.parameter(key, value)
177
end
178
end
179
180
after :all do
181
driver.close_payloads
182
console.reset
183
end
184
185
context "#{Acceptance::Session.current_platform}" do
186
command_shell_config[:module_tests].each do |module_test|
187
describe module_test[:name].to_s, focus: module_test[:focus] do
188
it(
189
"#{Acceptance::Session.current_platform}/#{command_shell_runtime_name} command shell successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests",
190
if: (
191
Acceptance::Session.run_session?(command_shell_config) &&
192
# Run if ENV['SESSION_MODULE_TEST'] = 'post/test/cmd_exec' etc
193
Acceptance::Session.run_session_module_test?(module_test[:name]) &&
194
# Only run payloads / tests, if the host machine can run them
195
Acceptance::Session.supported_platform?(payload_config) &&
196
Acceptance::Session.supported_platform?(module_test) &&
197
# Skip tests that are explicitly skipped, or won't pass in the current environment
198
!Acceptance::Session.skipped_module_test?(module_test, allure_test_environment)
199
),
200
# test metadata - will appear in allure report
201
module_test: module_test[:name]
202
) do
203
begin
204
replication_commands = []
205
current_payload_status = ''
206
207
known_failures = module_test.dig(:lines, :all, :known_failures) || []
208
known_failures += module_test.dig(:lines, current_platform, :known_failures) || []
209
known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
210
211
required_lines = module_test.dig(:lines, :all, :required) || []
212
required_lines += module_test.dig(:lines, current_platform, :required) || []
213
required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
214
215
# 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
216
payload_process, session_id = payload_process_and_session_id
217
218
expect(payload_process).to(be_alive, proc do
219
$stderr.puts "Made it inside expect payload_process: #{payload_process}"
220
$stderr.puts "Is the process alive?: #{payload_process.alive?}"
221
$stderr.puts "Process wait.thread?: #{payload_process.wait_thread}"
222
$stderr.puts "We have access to .wait_thread, but do we have access to .wait_thread.value?: #{payload_process.alive?}"
223
224
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}"
225
226
$stderr.puts "Made it after current_payload_status: #{payload_process}"
227
$stderr.puts "Is the process alive?: #{payload_process.alive?}"
228
229
Allure.add_attachment(
230
name: 'Failed payload blob',
231
source: Base64.strict_encode64(File.binread(payload_process.payload_path)),
232
type: Allure::ContentType::TXT
233
)
234
235
current_payload_status
236
end)
237
expect(session_id).to_not(be_nil, proc do
238
"There should be a session present"
239
end)
240
241
use_module = "use #{module_test[:name]}"
242
run_module = "run session=#{session_id} AddEntropy=true Verbose=true"
243
244
replication_commands << use_module
245
console.sendline(use_module)
246
console.recvuntil(Acceptance::Console.prompt)
247
248
replication_commands << run_module
249
console.sendline(run_module)
250
251
# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:
252
# console.interact
253
254
# Expect the test module to complete
255
test_result = console.recvuntil('Post module execution completed')
256
257
# Ensure there are no failures, and assert tests are complete
258
aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do
259
# Skip any ignored lines from the validation input
260
validated_lines = test_result.lines.reject do |line|
261
is_acceptable = known_failures.any? do |acceptable_failure|
262
line.include?(acceptable_failure.value) &&
263
acceptable_failure.if?(test_environment)
264
end || line.match?(/Passed: \d+; Failed: \d+/)
265
266
is_acceptable
267
end
268
269
validated_lines.each do |test_line|
270
test_line = Acceptance::Session.uncolorize(test_line)
271
expect(test_line).to_not include('FAILED', '[-] '), "Unexpected error: #{test_line}"
272
end
273
274
# Assert all expected lines are present
275
required_lines.each do |required|
276
next unless required.if?(test_environment)
277
278
expect(test_result).to include(required.value)
279
end
280
281
# Assert all ignored lines are present, if they are not present - they should be removed from
282
# the calling config
283
known_failures.each do |acceptable_failure|
284
next if acceptable_failure.flaky?(test_environment)
285
next unless acceptable_failure.if?(test_environment)
286
287
expect(test_result).to include(acceptable_failure.value)
288
end
289
end
290
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
291
test_run_error = e
292
end
293
294
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
295
# still generated if the session dies in a weird way etc
296
297
# Payload process cleanup / verification
298
# The payload process wasn't initially marked as dead - let's close it
299
if payload_process.present? && current_payload_status.blank?
300
begin
301
if payload_process.alive?
302
current_payload_status = "Process still alive after running test suite"
303
payload_process.close
304
else
305
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}"
306
end
307
rescue => e
308
Allure.add_attachment(
309
name: 'driver.close_payloads failure information',
310
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
311
type: Allure::ContentType::TXT
312
)
313
end
314
end
315
316
console_reset_error = nil
317
current_console_data = console.all_data
318
begin
319
console.reset
320
rescue => e
321
console_reset_error = e
322
Allure.add_attachment(
323
name: 'console.reset failure information',
324
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
325
type: Allure::ContentType::TXT
326
)
327
end
328
329
payload_configuration_details = payload.as_readable_text(
330
default_global_datastore: default_global_datastore,
331
default_module_datastore: default_module_datastore
332
)
333
334
replication_steps = <<~EOF
335
## Load test modules
336
loadpath test/modules
337
338
#{payload_configuration_details}
339
340
## Replication commands
341
#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}
342
EOF
343
344
Allure.add_attachment(
345
name: 'payload configuration and replication',
346
source: replication_steps,
347
type: Allure::ContentType::TXT
348
)
349
350
Allure.add_attachment(
351
name: 'payload output if available',
352
source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",
353
type: Allure::ContentType::TXT
354
)
355
356
Allure.add_attachment(
357
name: 'session tlv logging if available',
358
source: get_file_attachment_contents(session_tlv_logging_file.path),
359
type: Allure::ContentType::TXT
360
)
361
362
Allure.add_attachment(
363
name: 'console data',
364
source: current_console_data,
365
type: Allure::ContentType::TXT
366
)
367
368
test_assertions = JSON.pretty_generate(
369
{
370
required_lines: required_lines.map(&:to_h),
371
known_failures: known_failures.map(&:to_h),
372
}
373
)
374
Allure.add_attachment(
375
name: 'test assertions',
376
source: test_assertions,
377
type: Allure::ContentType::TXT
378
)
379
380
raise test_run_error if test_run_error
381
raise console_reset_error if console_reset_error
382
end
383
end
384
end
385
end
386
end
387
end
388
end
389
end
390
end
391
392