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