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