Contest Gallery <= 30.0.2 - Authenticated (Author+) Privilege Escalation via 'RegistryUserRole' Parameter
Description
The Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 30.0.2 via the `RegistryUserRole` parameter. This is due to the plugin's admin menu being registered at the `edit_posts` capability level — granting Contributor-level users access to the plugin's admin pages and a valid `cg_admin` nonce — while the option-saving handler in `change-options-and-sizes.php` performs no `current_user_can()` capability check beyond `check_admin_referer('cg_admin')`, and the `RegistryUserRole` value is processed only through `sanitize_text_field()` and `htmlentities()` without restriction to an allowlist of permitted role names. This makes it possible for authenticated attackers, with author-level access and above, to overwrite the plugin's stored `RegistryUserRole` option with `administrator`, which the `cg_create_wp_user_from_google_user` function then reads back from the `contest_gal1ery_registry_and_login_options` database table without any allowlist validation and passes directly to `wp_update_user()`, effectively promoting a newly registered Google sign-in account to Administrator.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=30.0.2What Changed in the Fix
Changes introduced in v30.0.3
Source Code
WordPress.org SVNThis exploitation research plan targets **CVE-2026-12165**, an authenticated privilege escalation vulnerability in the **Contest Gallery** plugin (<= 30.0.2). ### 1. Vulnerability Summary The vulnerability exists due to a combination of improper capability checks and insufficient input validation. …
Show full research plan
This exploitation research plan targets CVE-2026-12165, an authenticated privilege escalation vulnerability in the Contest Gallery plugin (<= 30.0.2).
1. Vulnerability Summary
The vulnerability exists due to a combination of improper capability checks and insufficient input validation. The plugin registers its admin menu using the edit_posts capability, which is granted to users with the Contributor role and above. This allows these users to access the plugin's administration pages and retrieve a valid cg_admin nonce.
The option-saving logic (specifically handling the RegistryUserRole parameter) uses check_admin_referer('cg_admin') to prevent CSRF but fails to verify if the current user has the authority to change system-level roles (i.e., it lacks a current_user_can('manage_options') check). Furthermore, the RegistryUserRole is saved to the database without being validated against an allowlist of safe roles. An attacker can set this to administrator. When a new user registers via the Google Sign-In feature, the function cg_create_wp_user_from_google_user reads this value and promotes the new user to an Administrator.
2. Attack Vector Analysis
- Vulnerable Endpoint: The settings update page/handler (likely
admin.php?page=...involving the logic inchange-options-and-sizes.php). - Vulnerable Parameter:
options[registry-login][RegistryUserRole](or similar POST structure processed bycg_update_registry_and_login_options_v14). - Authentication Level: Authenticated, Contributor role or higher (requires
edit_postscapability). - Preconditions: The Google Sign-In feature must be available (enabled in version 30.0.2+).
3. Code Flow
- Entry Point: An authenticated Contributor user accesses the Contest Gallery settings in the WordPress dashboard.
- Nonce Retrieval: The settings page enqueues scripts/forms that include the
cg_adminnonce. - Option Update: The user sends a POST request to the options handler.
- Insecure Processing: The handler (referencing
functions/general/registry/update/cg-registry-update-functions.php) callscg_update_registry_and_login_options_v14.- It validates the nonce via
check_admin_referer('cg_admin'). - It does not check
current_user_can('manage_options').
- It validates the nonce via
- Persistence: The
RegistryUserRolevalue is saved into the table{prefix}contest_gal1ery_registry_and_login_options. - Escalation Sink: In
functions/google/cg-create-wp-user-from-google-user.php:- The function retrieves the role:
$RegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1"); - It creates a new user and updates their role:
wp_update_user( array( 'ID' => $WpUserId, 'role' => $RegistryUserRole ) );
- The function retrieves the role:
4. Nonce Acquisition Strategy
The cg_admin nonce is required. Since the menu is registered with edit_posts, a Contributor can access the admin pages.
- Log in as a Contributor.
- Navigate to the Contest Gallery settings page (e.g.,
wp-admin/admin.php?page=contest-gallery-options). - Extract the nonce from the HTML or localized JS variables.
- Action: Use
browser_evalto find the nonce if it's localized, or inspect the form for a hidden input named_wpnonce.
5. Exploitation Strategy
The goal is to modify the RegistryUserRole to administrator.
Step 1: Update the Role Option
- Tool:
http_request - Method: POST
- URL:
http://localhost:8080/wp-admin/admin.php?page=contest-gallery-options(Verify the exact page slug viawp menu list). - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
_wpnonce:[Extracted_cg_admin_nonce]_wp_http_referer:/wp-admin/admin.php?page=contest-gallery-optionsoptions[registry-login][RegistryUserRole]:administrator- Other required form fields (e.g.,
LogoutLink,BackToGalleryLink) should be sent with their current values to avoid breaking the plugin.
Step 2: Trigger Privilege Escalation (Conceptual)
- A new user registers via Google Sign-In. The plugin will now use the malicious
RegistryUserRolevalue to assign the 'administrator' role.
6. Test Data Setup
- Install Contest Gallery version 30.0.2.
- Create a user with the Contributor role:
wp user create attacker attacker@example.com --role=contributor --user_pass=password - Ensure the plugin is active and initialized.
7. Expected Results
- The POST request returns a
302 Redirector a success message. - The database value for
RegistryUserRolein thecontest_gal1ery_registry_and_login_optionstable is changed toadministrator.
8. Verification Steps
After the HTTP request, use WP-CLI to verify the change in the custom table:
wp db query "SELECT RegistryUserRole FROM $(wp db prefix)contest_gal1ery_registry_and_login_options WHERE GeneralID = 1"
If the output is administrator, the escalation vector is confirmed.
9. Alternative Approaches
If the cg_admin nonce is not available on the main options page, check the "Registry & Login" sub-page specifically, as the vulnerability is tied to the RegistryUserRole parameter which resides there. If direct file access to change-options-and-sizes.php is possible (check for ABSPATH guards), that may be a secondary vector, though the admin menu registration is the primary intended path for this exploit.
Summary
The Contest Gallery plugin for WordPress is vulnerable to privilege escalation because its administration settings are accessible to users with the 'edit_posts' capability (Author level) and lack proper role validation. An attacker can overwrite the 'RegistryUserRole' setting with 'administrator', which causes the plugin to grant administrative privileges to any new user who subsequently registers through the Google Sign-In feature.
Vulnerable Code
// v10/v10-admin/options/change-options-and-sizes.php check_admin_referer('cg_admin'); // ... if($dbVersion>=14){// for new galleries after 14 $RegistryUserRole = ''; $RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : '')); }else{// for older galleries before 14 $RegistryUserRole = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : '')); $RegistryUserRoleForRegistryAndLoginOptions = ''; } --- // functions/google/cg-create-wp-user-from-google-user.php $RegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1"); // ... wp_update_user( array( 'ID' => $WpUserId, 'role' => $RegistryUserRole ) );
Security Fix
@@ -13,6 +13,10 @@ //echo 'This request contained ' . count( $_POST, COUNT_RECURSIVE ) . ' POST vars, ' . count( $_GET ) . ' GET vars, and ' . count( $_COOKIE ) . ' Cookies.'; //die; +if(!is_user_logged_in() || !current_user_can('publish_posts') || !cg_user_has_backend_access()){ + cg_die_missing_backend_access(); +} + check_admin_referer('cg_admin'); $id = absint($_GET['option_id']); @@ -1236,10 +1240,24 @@ $CheckCookieAlertMessage = (isset($_POST['CheckCookieAlertMessage'])) ? contest_gal1ery_htmlentities_and_preg_replace($_POST['CheckCookieAlertMessage']) : $CheckCookieAlertMessage; if($dbVersion>=14){// for new galleries after 14 + $currentRegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1"); + }else{// for older galleries before 14 + $currentRegistryUserRole = $unsavingValues->RegistryUserRole; + } + $currentRegistryUserRole = cg_get_safe_registry_user_role($currentRegistryUserRole,$dbVersion); + + if(current_user_can('manage_options')){ + $postedRegistryUserRole = (isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : $currentRegistryUserRole; + $safeRegistryUserRole = cg_get_safe_registry_user_role($postedRegistryUserRole,$dbVersion); + }else{ + $safeRegistryUserRole = $currentRegistryUserRole; + } + + if($dbVersion>=14){// for new galleries after 14 $RegistryUserRole = ''; - $RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : '')); + $RegistryUserRoleForRegistryAndLoginOptions = $safeRegistryUserRole; }else{// for older galleries before 14 - $RegistryUserRole = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : '')); + $RegistryUserRole = $safeRegistryUserRole; $RegistryUserRoleForRegistryAndLoginOptions = ''; }
Exploit Outline
The exploit requires an authenticated user with at least Author-level permissions (or any user possessing the 'edit_posts' capability). 1. Log in to the WordPress admin panel as an Author. 2. Navigate to the Contest Gallery settings page to retrieve a valid 'cg_admin' nonce (localized in JavaScript or present in the form). 3. Send a POST request to the plugin's settings handler (v10/v10-admin/options/change-options-and-sizes.php) with the following payload: - `_wpnonce`: The extracted 'cg_admin' nonce. - `RegistryUserRole`: 'administrator'. 4. This action overwrites the plugin's internal setting for default registration roles without checking if the requester has 'manage_options' capabilities. 5. To complete the escalation, the attacker (or a victim) registers a new account using the Google Sign-In feature. The plugin retrieves the malicious 'administrator' role from the database and assigns it to the new user via `wp_update_user()`.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.