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/smb_spec.rb
Views: 11766
1
require 'acceptance_spec_helper'
2
3
RSpec.describe 'SMB sessions and SMB modules' do
4
include_context 'wait_for_expect'
5
6
RHOST_REGEX = /\d+\.\d+\.\d+\.\d+:\d+/
7
8
tests = {
9
smb: {
10
target: {
11
session_module: "auxiliary/scanner/smb/smb_login",
12
type: 'SMB',
13
platforms: [:linux, :osx, :windows],
14
datastore: {
15
global: {},
16
module: {
17
username: ENV.fetch('SMB_USERNAME', 'acceptance_tests_user'),
18
password: ENV.fetch('SMB_PASSWORD', 'acceptance_tests_password'),
19
rhost: ENV.fetch('SMB_RHOST', '127.0.0.1'),
20
rport: ENV.fetch('SMB_RPORT', '445'),
21
}
22
}
23
},
24
module_tests: [
25
{
26
name: "post/test/smb",
27
platforms: [:linux, :osx, :windows],
28
targets: [:session],
29
skipped: false,
30
},
31
{
32
name: "auxiliary/scanner/smb/smb_lookupsid",
33
platforms: [:linux, :osx, :windows],
34
targets: [:session, :rhost],
35
skipped: false,
36
lines: {
37
all: {
38
required: [
39
"PIPE(lsarpc) LOCAL",
40
/User( *)(Administrator|nobody)/,
41
/Group( *)(None|Domain (Admins|Users|Guests|Computers))/,
42
],
43
},
44
}
45
},
46
{
47
name: "auxiliary/scanner/smb/smb_enumusers",
48
platforms: [:linux, :osx, :windows],
49
targets: [:session, :rhost],
50
skipped: false,
51
lines: {
52
all: {
53
required: [
54
"acceptance_tests_user",
55
],
56
},
57
}
58
},
59
{
60
name: "auxiliary/scanner/smb/pipe_auditor",
61
platforms: [:linux, :osx, :windows],
62
targets: [:session, :rhost],
63
skipped: false,
64
lines: {
65
all: {
66
required: [
67
/Pipes: (\\([a-zA-Z]*)(, )?)*/,
68
],
69
known_failures: [
70
/Inaccessible named pipe:/,
71
/The server responded with an unexpected status code: STATUS_OBJECT_NAME_NOT_FOUND/,
72
]
73
},
74
}
75
},
76
{
77
name: "auxiliary/scanner/smb/smb_enumshares",
78
platforms: [:linux, :osx, :windows],
79
targets: [:session, :rhost],
80
skipped: false,
81
lines: {
82
all: {
83
required: [
84
"modifiable - (DISK)",
85
"readonly - (DISK)",
86
"IPC$ - (IPC|SPECIAL) IPC Service",
87
],
88
},
89
}
90
},
91
]
92
}
93
}
94
95
allure_test_environment = AllureRspec.configuration.environment_properties
96
97
let_it_be(:current_platform) { Acceptance::Session::current_platform }
98
99
# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly
100
let_it_be(:driver) do
101
driver = Acceptance::ConsoleDriver.new
102
driver
103
end
104
105
# Opens a test console with the test loadpath specified
106
# @!attribute [r] console
107
# @return [Acceptance::Console]
108
let_it_be(:console) do
109
console = driver.open_console
110
111
# Load the test modules
112
console.sendline('loadpath test/modules')
113
console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)
114
console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)
115
console.recvuntil(/\d+ exploit modules[^\n]*\n/)
116
console.recvuntil(/\d+ post modules[^\n]*\n/)
117
console.recvuntil(Acceptance::Console.prompt)
118
119
# Read the remaining console
120
# console.sendline "quit -y"
121
# console.recv_available
122
123
features = %w[
124
smb_session_type
125
]
126
127
features.each do |feature|
128
console.sendline("features set #{feature} true")
129
console.recvuntil(Acceptance::Console.prompt)
130
end
131
132
console
133
end
134
135
# Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking
136
# This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope
137
def with_test_harness(module_test)
138
begin
139
replication_commands = []
140
141
known_failures = module_test.dig(:lines, :all, :known_failures) || []
142
known_failures += module_test.dig(:lines, current_platform, :known_failures) || []
143
known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
144
145
required_lines = module_test.dig(:lines, :all, :required) || []
146
required_lines += module_test.dig(:lines, current_platform, :required) || []
147
required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
148
149
yield replication_commands
150
151
# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:
152
# console.interact
153
154
# Expect the test module to complete
155
module_type = module_test[:name].split('/').first
156
test_result = console.recvuntil("#{module_type.capitalize} module execution completed")
157
158
# Ensure there are no failures, and assert tests are complete
159
aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do
160
# Skip any ignored lines from the validation input
161
validated_lines = test_result.lines.reject do |line|
162
is_acceptable = known_failures.any? do |acceptable_failure|
163
is_matching_line = acceptable_failure.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value)
164
is_matching_line &&
165
acceptable_failure.if?(test_environment)
166
end || line.match?(/Passed: \d+; Failed: \d+/)
167
168
is_acceptable
169
end
170
171
validated_lines.each do |test_line|
172
test_line = Acceptance::Session.uncolorize(test_line)
173
expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"
174
end
175
176
# Assert all expected lines are present
177
required_lines.each do |required|
178
next unless required.if?(test_environment)
179
if required.value.is_a?(Regexp)
180
expect(test_result).to match(required.value)
181
else
182
expect(test_result).to include(required.value)
183
end
184
end
185
186
# Assert all ignored lines are present, if they are not present - they should be removed from
187
# the calling config
188
known_failures.each do |acceptable_failure|
189
next if acceptable_failure.flaky?(test_environment)
190
next unless acceptable_failure.if?(test_environment)
191
192
if acceptable_failure.value.is_a?(Regexp)
193
expect(test_result).to match(acceptable_failure.value)
194
else
195
expect(test_result).to include(acceptable_failure.value)
196
end
197
end
198
end
199
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
200
test_run_error = e
201
end
202
203
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
204
# still generated if the session dies in a weird way etc
205
206
console_reset_error = nil
207
current_console_data = console.all_data
208
begin
209
console.reset
210
rescue => e
211
console_reset_error = e
212
Allure.add_attachment(
213
name: 'console.reset failure information',
214
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
215
type: Allure::ContentType::TXT
216
)
217
end
218
219
target_configuration_details = target.as_readable_text(
220
default_global_datastore: default_global_datastore,
221
default_module_datastore: default_module_datastore
222
)
223
224
replication_steps = <<~EOF
225
## Load test modules
226
loadpath test/modules
227
228
#{target_configuration_details}
229
230
## Replication commands
231
#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}
232
EOF
233
234
Allure.add_attachment(
235
name: 'payload configuration and replication',
236
source: replication_steps,
237
type: Allure::ContentType::TXT
238
)
239
240
Allure.add_attachment(
241
name: 'console data',
242
source: current_console_data,
243
type: Allure::ContentType::TXT
244
)
245
246
test_assertions = JSON.pretty_generate(
247
{
248
required_lines: required_lines.map(&:to_h),
249
known_failures: known_failures.map(&:to_h),
250
}
251
)
252
Allure.add_attachment(
253
name: 'test assertions',
254
source: test_assertions,
255
type: Allure::ContentType::TXT
256
)
257
258
raise test_run_error if test_run_error
259
raise console_reset_error if console_reset_error
260
end
261
262
tests.each do |runtime_name, test_config|
263
runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}"
264
265
describe "#{Acceptance::Session.current_platform}/#{runtime_name}", focus: test_config[:focus] do
266
test_config[:module_tests].each do |module_test|
267
describe(
268
module_test[:name],
269
if: (
270
Acceptance::Session.supported_platform?(module_test)
271
)
272
) do
273
let(:target) { Acceptance::Target.new(test_config[:target]) }
274
275
let(:default_global_datastore) do
276
{
277
}
278
end
279
280
let(:test_environment) { allure_test_environment }
281
282
let(:default_module_datastore) do
283
{
284
lhost: '127.0.0.1'
285
}
286
end
287
288
# The shared session id that will be reused across the test run
289
let(:session_id) do
290
console.sendline "use #{target.session_module}"
291
console.recvuntil(Acceptance::Console.prompt)
292
293
# Set global options
294
console.sendline target.setg_commands(default_global_datastore: default_global_datastore)
295
console.recvuntil(Acceptance::Console.prompt)
296
297
console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true })
298
299
session_id = nil
300
# Wait for the session to open, or break early if the payload is detected as dead
301
wait_for_expect do
302
session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/
303
session_message = ''
304
begin
305
session_message = console.recvuntil(session_opened_matcher, timeout: 1)
306
rescue Acceptance::ChildProcessRecvError
307
# noop
308
end
309
310
session_id = session_message[session_opened_matcher, 1]
311
expect(session_id).to_not be_nil
312
end
313
314
session_id
315
end
316
317
before :each do |example|
318
next unless example.respond_to?(:parameter)
319
320
# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI
321
test_environment.each do |key, value|
322
example.parameter(key, value)
323
end
324
end
325
326
after :all do
327
driver.close_payloads
328
console.reset
329
end
330
331
context "when targeting a session", if: module_test[:targets].include?(:session) do
332
it(
333
"#{Acceptance::Session.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests"
334
) do
335
with_test_harness(module_test) do |replication_commands|
336
# 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
337
expect(session_id).to_not(be_nil, proc do
338
"There should be a session present"
339
end)
340
341
use_module = "use #{module_test[:name]}"
342
run_module = "run session=#{session_id} Verbose=true"
343
344
replication_commands << use_module
345
console.sendline(use_module)
346
console.recvuntil(Acceptance::Console.prompt)
347
348
replication_commands << run_module
349
console.sendline(run_module)
350
351
# Assertions will happen after this block ends
352
end
353
end
354
end
355
356
context "when targeting an rhost", if: module_test[:targets].include?(:rhost) do
357
it(
358
"#{Acceptance::Session.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests"
359
) do
360
with_test_harness(module_test) do |replication_commands|
361
use_module = "use #{module_test[:name]}"
362
run_module = "run #{target.datastore_options(default_module_datastore: default_module_datastore)} Verbose=true"
363
364
replication_commands << use_module
365
console.sendline(use_module)
366
console.recvuntil(Acceptance::Console.prompt)
367
368
replication_commands << run_module
369
console.sendline(run_module)
370
371
# Assertions will happen after this block ends
372
end
373
end
374
end
375
end
376
end
377
end
378
end
379
end
380
381