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