SECURITY ADVISORY / 01

CVE-2024-4010 Exploit & Vulnerability Analysis

Complete CVE-2024-4010 security advisory with proof of concept (PoC), exploit details, and patch analysis for email-subscribers.

email-subscribers products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An authenticated WordPress user with subscriber-level access and above can execute arbitrary SQL queries and trigger PHP object injection by sending a specially crafted AJAX request to the Email Subscribers by Icegram Express plugin.

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>

action=es_campaign&sub_action=update_campaign_status&campaign_ids[]=999 OR 1=1&new_status=active

The attacker observes that the query executes without validation — the campaign_ids parameter bypasses integer type enforcement, allowing SQL injection operators to be passed directly into the database query. The server returns a success response, confirming the malicious operation executed. In parallel, if the attacker crafts a request with a malicious serialized PHP object in the meta field, the plugin deserializes it unsafely, triggering object instantiation chains that can lead to remote code execution.

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>

action=es_campaign&sub_action=save_campaign&meta=O:8:"BadClass":1:{s:4:"file";s:20:"/tmp/shell.php";}

What the Patch Did

Before:

$campaign_ids = $args['campaign_ids'];
$new_status   = $args['new_status'];

if ( ! empty( $campaign_ids ) ) {
    $status_updated = ES()->campaigns_db->update_status( $campaign_ids, $new_status );
    return $status_updated;
}

After:

if ( is_array( $args ) && isset( $args['campaign_ids'], $args['new_status'] ) ) {
    $campaign_ids = isset($args['campaign_ids']) ? $args['campaign_ids'] : array();
    $campaign_ids = array_map('absint', $campaign_ids);
    $new_status   = sanitize_text_field( $args['new_status'] );

    if (!empty($campaign_ids)) {
        $status_updated = ES()->campaigns_db->update_status( $campaign_ids, $new_status );
        return $status_updated;
    }
}

The patch added three input validation controls. First, it validates that $args is actually an array using is_array(), and that required keys exist via isset(). Second, it applies array_map('absint', $campaign_ids) to coerce all campaign ID values to unsigned integers, eliminating any non-numeric payload. Third, it applies sanitize_text_field() to the status string to remove dangerous characters. The status values also now validate against expected enum states, preventing injection of unexpected SQL tokens.

For deserialization vulnerabilities, the patch replaced all calls to WordPress's native maybe_unserialize() with a custom ig_es_maybe_unserialize() function that implements safer deserialization, likely validating the structure before instantiation or using JSON-based alternatives.

SQL Query Deserialization Before:

$meta = ! empty( $data['meta'] ) ? maybe_unserialize( $data['meta'] ) : array();

SQL Query Deserialization After:

$meta = ! empty( $data['meta'] ) ? ig_es_maybe_unserialize( $data['meta'] ) : array();

Root Cause

CWE-20 (Improper Input Validation) and CWE-89 (SQL Injection): The AJAX handler handle_ajax_request() in the campaign controller accepts user-supplied campaign_ids and new_status directly from $_POST (via $args) without type checking or whitelist validation. The campaign_ids parameter is expected to be an array of integers but is passed directly to ES()->campaigns_db->update_status(), which constructs a SQL query by concatenating the IDs into an IN() clause without parameterization. An attacker can inject SQL syntax into campaign IDs — for example, 999 OR 1=1 — which reaches the database layer unchecked.

CWE-502 (Deserialization of Untrusted Data): The meta field is deserialized using maybe_unserialize(), which internally calls PHP's native unserialize() on arbitrary user-controlled strings. PHP's unserialize creates object instances during deserialization, allowing an attacker to craft a malicious serialized object (using a gadget chain in the codebase or WordPress itself) that executes code during object construction or destruction.

Why It Works

The load-bearing line in the campaign ID fix is $campaign_ids = array_map('absint', $campaign_ids);. This single operation converts every element in the array to an unsigned integer; any non-numeric portion is truncated to zero. An attacker cannot inject SQL operators because absint('999 OR 1=1') becomes 999. Without this line, the entire fix is cosmetic — the isset() check only verifies keys exist, not their type or safety.

The engineer added is_array() and isset() as depth-of-defense, ensuring the handler fails gracefully if $args is malformed, preventing unexpected type coercion bugs. sanitize_text_field() on new_status follows the same pattern: it removes HTML tags and dangerous characters, but the real control is absint() on the IDs. The deserialization fix is complementary — it moves the handler away from PHP's native unserialize() altogether, eliminating the gadget-chain attack surface entirely.

Hardening Checklist

  • Always apply absint(), intval(), or floatval() to numeric parameters before passing them to database queries. In WordPress, use array_map() for array values. Type coercion must happen before the value reaches SQL construction.

  • Use $wpdb->prepare() with placeholders for every user-controlled SQL value, even integers. Replace string concatenation (e.g., implode() into IN clauses) with parameterized queries: $wpdb->prepare("WHERE id IN (%d, %d)", $id1, $id2).

  • Never call PHP's native unserialize() on user input. Replace it with json_decode() if structured data is needed, or implement a custom whitelist-based deserializer that validates object classes before instantiation.

  • Wrap AJAX handler methods with is_array() and isset() checks before accessing $args keys. WordPress passes $_POST as $args, which is user-controlled; defensive checks prevent type confusion bugs.

  • Make helper methods private unless they must be overridden or called externally. In the patch, format_campaign_data() changed from public to private, reducing the attack surface by preventing external callers from bypassing internal validation.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2024-4010

Frequently asked questions about CVE-2024-4010

What is CVE-2024-4010?

CVE-2024-4010 is a security vulnerability identified in email-subscribers. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2024-4010?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2024-4010. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2024-4010 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting email-subscribers. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2024-4010?

CVE-2024-4010 affects email-subscribers. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2024-4010?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for email-subscribers.

What is the CVSS score for CVE-2024-4010?

The severity rating and CVSS scoring for CVE-2024-4010 affecting email-subscribers is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.