Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress <= 4.16.11 - Unauthenticated Arbitrary Shortcode Execution via Checkout Billing Fields
Description
The Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress plugin for WordPress is vulnerable to arbitrary shortcode execution in all versions up to, and including, 4.16.11. This is due to the plugin allowing user-supplied billing field values from the checkout process to be interpolated into shortcode template strings that are subsequently processed without proper sanitization of shortcode syntax. This makes it possible for unauthenticated attackers to execute arbitrary shortcodes by submitting crafted billing field values during the checkout process.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:NTechnical Details
<=4.16.11What Changed in the Fix
Changes introduced in v4.16.12
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate unauthenticated arbitrary shortcode execution in ProfilePress <= 4.16.11. ### 1. Vulnerability Summary The ProfilePress plugin is vulnerable to arbitrary shortcode execution via user-supplied billing fields during the checkout process. T…
Show full research plan
This research plan outlines the steps required to demonstrate unauthenticated arbitrary shortcode execution in ProfilePress <= 4.16.11.
1. Vulnerability Summary
The ProfilePress plugin is vulnerable to arbitrary shortcode execution via user-supplied billing fields during the checkout process. The plugin accepts billing information (e.g., first name, last name, address) via AJAX requests and, in some contexts, interpolates these values into strings that are subsequently processed by the WordPress shortcode engine (do_shortcode()). Because the input is not sanitized for shortcode syntax (square brackets), an attacker can inject malicious shortcode tags into billing fields which will be executed by the server.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Actions:
ppress_process_checkoutorppress_update_order_review(both havenoprivhandlers). - Authentication: Unauthenticated.
- Vulnerable Parameter: Billing fields, specifically
ppress_billing_first_name,ppress_billing_last_name, or otherppress_billing_*parameters. - Preconditions: A Membership Plan must exist, and a Checkout Page must be configured with the
[profilepress-checkout]shortcode to obtain a valid nonce.
3. Code Flow
- Entry Point: An unauthenticated user sends an AJAX request to
ppress_update_order_revieworppress_process_checkout. - Controller:
ProfilePress\Core\Membership\Controllers\CheckoutController(and itsCheckoutTrait) handles these actions. - Processing: The plugin captures billing fields from the
$_POSTsuperglobal. - Interpolation: During order review updates or processing, these fields are stored in the session/cart and may be used to populate template strings for the "Order Summary" or "Checkout Sidebar".
- Sink: The resulting string is passed to
do_shortcode(). If a billing field contains[shortcode_name], WordPress will parse and execute it.
4. Nonce Acquisition Strategy
The checkout actions are protected by a nonce named ppress_checkout_nonce.
- Setup: Create a Membership Plan and a Checkout Page.
wp ppress_plan create --name="Gold" --price=10(Note: Use WP-CLI to create a plan if the plugin provides a command, otherwise usewp post createfor a plan CPT if identified, but standardwp post createfor the page is mandatory).wp post create --post_type=page --post_title="Checkout" --post_status=publish --post_content='[profilepress-checkout]'
- Navigation: Navigate to the Checkout page using
browser_navigate. - Extraction: ProfilePress localizes its parameters in a global JavaScript object. Use
browser_evalto extract the nonce.- Target Variable:
ppress_checkout_params - Key:
nonce - Command:
browser_eval("ppress_checkout_params.nonce")
- Target Variable:
5. Exploitation Strategy
The goal is to trigger shortcode execution via the ppress_update_order_review action, as this typically returns the rendered HTML of the checkout summary, providing immediate feedback.
Step 1: Setup Membership Plan
If a plan doesn't exist, create one to ensure the checkout page renders correctly.
# Example if using standard WP-CLI to create the necessary CPT for a plan
wp post create --post_type=ppress_forms --post_title="Checkout Form" --post_status=publish
# (In a real environment, you'd ensure a plan ID is available)
Step 2: Obtain Nonce and Plan ID
Navigate to the page containing [profilepress-checkout].
Extract plan_id (from URL or page source) and the nonce.
Step 3: Submit Malicious AJAX Request
Send a POST request to admin-ajax.php using the http_request tool.
- Action:
ppress_update_order_review - Payload:
Note:POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=ppress_update_order_review& ppress_checkout_nonce=[NONCE]& ppress_billing_first_name=[profilepress-login]& ppress_billing_last_name=Tester& plan_id=[PLAN_ID][profilepress-login]is a standard ProfilePress shortcode that renders a login form. If successful, the response HTML will contain a login form where the first name should be.
6. Test Data Setup
- Membership Plan: Create a basic membership plan (ID likely 1).
- Checkout Page: A page with
[profilepress-checkout]at/checkout/. - Shortcode for PoC:
[profilepress-login]or[audio src="https://example.com/test.mp3"](to see the audio player HTML).
7. Expected Results
- The HTTP response from the AJAX request should contain the rendered HTML of the injected shortcode.
- For
[profilepress-login], the response should contain form elements like<input name="log" ...>. - For
[audio], the response should contain<div class="wp-audio-shortcode">.
8. Verification Steps
- Inspect Response: Check the JSON response from
ppress_update_order_review. Thedatafield usually contains the rendered HTML fragments. - Search for Strings: Look for HTML tags unique to the injected shortcode (e.g.,
pp-login-form-container).
9. Alternative Approaches
If ppress_update_order_review does not return the rendered field, attempt the exploit via ppress_process_checkout:
- Submit the checkout with
ppress_billing_first_nameset to[profilepress-login]. - If the checkout completes (even if payment fails/pending), check the "My Account" page or the Order Confirmation page.
- Use
wp post list --post_type=ppress_ordersto find the created order and check if the shortcode executed in the order notes or summary viewable in the admin dashboard (if testing for Cross-Site Scripting via Shortcode or information disclosure).
Summary
The ProfilePress plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode execution via checkout billing fields. This occurs because user-supplied billing information, submitted during the checkout AJAX process, is interpolated into template strings (such as the order summary) which are subsequently processed by the WordPress shortcode engine without sanitizing for shortcode syntax.
Vulnerable Code
// src/Membership/Controllers/CheckoutController.php lines 41-42 add_action('wp_ajax_ppress_update_order_review', [$this, 'update_order_review']); add_action('wp_ajax_nopriv_ppress_update_order_review', [$this, 'update_order_review']); --- // Within the update_order_review method (typically found in CheckoutTrait included by CheckoutController) // The plugin processes billing fields from $_POST and renders fragments for the checkout sidebar. // The vulnerability occurs when billing fields are merged into HTML strings passed to do_shortcode(). $billing_first_name = ppressPOST_var('ppress_billing_first_name', ''); // ... values are stored in session or cart objects ... // Sink usually involves rendering a template containing the field and parsing it: $order_summary_html = $this->render_order_summary_template(); echo do_shortcode($order_summary_html);
Security Fix
@@ -300,6 +300,17 @@ throw new \Exception(json_encode($customer_id->get_error_messages())); } + $changePlanSub = SubscriptionFactory::fromId($change_plan_sub_id); + + if ( + $changePlanSub->exists() && + ! empty($customer_id) && + $customer_id !== $changePlanSub->get_customer_id()) { + throw new \Exception( + esc_html__('You are not allowed to switch from this plan.', 'wp-user-avatar') + ); + } + $order_id = $this->create_order($customer_id, $cart_vars); if (is_wp_error($order_id)) { @@ -331,19 +342,17 @@ } else { - $sub = SubscriptionFactory::fromId($change_plan_sub_id); - - if ($sub->exists() && $sub->get_customer_id() == $customer_id) { + if ($changePlanSub->exists() && $changePlanSub->get_customer_id() == $customer_id) { // do not send subscription cancelled email remove_action('ppress_subscription_cancelled', [SubscriptionCancelledNotification::init(), 'dispatch_email'], 10); remove_action('ppress_subscription_expired', [SubscriptionExpiredNotification::init(), 'dispatch_email'], 10); - $sub->cancel(true); - $sub->expire(); + $changePlanSub->cancel(true); + $changePlanSub->expire(); - SubscriptionFactory::fromId($subscription_id)->update_meta('_upgraded_from_sub_id', $sub->get_id()); - $sub->update_meta('_upgraded_to_sub_id', $subscription_id); + SubscriptionFactory::fromId($subscription_id)->update_meta('_upgraded_from_sub_id', $changePlanSub->get_id()); + $changePlanSub->update_meta('_upgraded_to_sub_id', $subscription_id); } /** @var CheckoutResponse $process_payment */
Exploit Outline
1. Identify a membership plan ID and navigate to the checkout page containing the [profilepress-checkout] shortcode. 2. Extract the 'ppress_checkout_nonce' from the 'ppress_checkout_params' global JavaScript object on the page. 3. Send an unauthenticated POST request to '/wp-admin/admin-ajax.php' with the action set to 'ppress_update_order_review'. 4. Include the extracted nonce, the plan ID, and a malicious shortcode payload (e.g., '[profilepress-login]') in a billing field parameter like 'ppress_billing_first_name'. 5. The server response will contain rendered HTML fragments for the checkout sidebar, where the injected shortcode has been executed and replaced with its corresponding functional HTML output.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.