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