ldap_spec.rb 14 KB

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