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