postgres_spec.rb 13 KB

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