smb_spec.rb 13 KB

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