mysql_spec.rb 12 KB

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