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