CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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