This blog post is about Themify Ultra theme vulnerability. If you’re a Themify Ultra user, please update the theme to at least version 7.3.6.
You can sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.
For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.
About the Themify Ultra Theme
The theme Themify Ultra (premium version), which is estimated to have over 70,000 active installations, is one of the more popular premium themes for designers and developers.
This a premium theme that provides us full control from header to footer, either site-wide or per individual page. This means that we can make our entire site share the same look and feel or create a unique look for every page.
The security vulnerability
This plugin suffers from multiple vulnerabilities. All of the vulnerabilities were patched in version 7.3.6 of the plugin.
Authenticated Arbitrary File Upload
This vulnerability allows authenticated users to upload and extract files from a zip file. Since there is no proper permission check and limitation on what kind of files are allowed to be extracted, this could lead to any authenticated user uploading a .php files to the server to gain Remote Code Execution (RCE). The described vulnerability was assigned CVE-2023-46149.
The underlying vulnerability is located in multiple functions such as themify_plupload
, customizer_import
and row_bulk_import
:
function themify_plupload() {
$imgid = $_POST['imgid'];
! empty( $_POST[ '_ajax_nonce' ] ) && check_ajax_referer($imgid . 'themify-plupload');
/** Decide whether to send this image to Media. @var String */
$add_to_media_library = isset( $_POST['tomedia'] ) ? $_POST['tomedia'] : false;
/** If post ID is set, uploaded image will be attached to it. @var String */
$postid = isset( $_POST['topost'] )? $_POST['topost'] : '';
/** Handle file upload storing file|url|type. @var Array */
$file = wp_handle_upload($_FILES[$imgid . 'async-upload'], array('test_form' => true, 'action' => 'themify_plupload'));
// if $file returns error, return it and exit the function
if ( isset( $file['error'] ) && ! empty( $file['error'] ) ) {
echo json_encode($file);
exit;
}
//let's see if it's an image, a zip file or something else
$ext = explode('/', $file['type']);
// Import routines
if( 'zip' === $ext[1] || 'rar' === $ext[1] || 'plain' === $ext[1] ){
$url = wp_nonce_url('admin.php?page=themify');
if (false === ($creds = request_filesystem_credentials($url) ) ) {
return true;
}
if ( ! WP_Filesystem($creds) ) {
request_filesystem_credentials($url, '', true);
return true;
}
global $wp_filesystem;
if ( 'zip' === $ext[1] || 'rar' === $ext[1] ) {
$upload_dir = themify_get_cache_dir();
unzip_file( $file['file'], $upload_dir['path'] );
------------------------ CUTTED HERE ------------------------
function customizer_import() {
$imgid = $_POST['imgid'];
! empty( $_POST[ '_ajax_nonce' ] ) && check_ajax_referer($imgid . 'themify-plupload');
/** Handle file upload storing file|url|type. @var Array */
$file = wp_handle_upload($_FILES[$imgid . 'async-upload'], array('test_form' => true, 'action' => 'themify_plupload_customizer'));
// if $file returns error, return it and exit the function
if (isset($file['error']) && !empty($file['error'])) {
echo json_encode($file);
exit;
}
//let's see if it's an image, a zip file or something else
$ext = explode('/', $file['type']);
// Import routines
if ('zip' === $ext[1] || 'rar' === $ext[1] || 'plain' === $ext[1]) {
$url = wp_nonce_url('customize.php');
if (false === ($creds = request_filesystem_credentials($url) )) {
return true;
}
if (!WP_Filesystem($creds)) {
request_filesystem_credentials($url, '', true);
return true;
}
global $wp_filesystem;
$base_path = themify_upload_dir();
$base_path = trailingslashit($base_path['path']);
if ('zip' === $ext[1] || 'rar' === $ext[1]) {
unzip_file($file['file'], $base_path);
------------------------ CUTTED HERE ------------------------
public function row_bulk_import() {
$imgid = $_POST['imgid'];
!empty( $_POST['_ajax_nonce'] ) && check_ajax_referer( $imgid . 'themify-plupload' );
/** Handle file upload storing file|url|type. @var Array */
$file = wp_handle_upload( $_FILES[ $imgid . 'async-upload' ], array( 'test_form' => true, 'action' => 'tbuilder_plupload_layout' ) );
// if $file returns error, return it and exit the function
if ( !empty( $file['error'] ) ) {
echo json_encode( $file );
exit;
}
//let's see if it's an image, a zip file or something else
$ext = explode( '/', $file['type'] );
// Import routines
if ( 'zip' === $ext[1] || 'rar' === $ext[1] || 'plain' === $ext[1] ) {
$url = wp_nonce_url( 'edit.php' );
if ( false === ( $creds = request_filesystem_credentials( $url ) ) ) {
return true;
}
if ( !WP_Filesystem( $creds ) ) {
request_filesystem_credentials( $url, '', true );
return true;
}
global $wp_filesystem;
$base_path = themify_upload_dir();
$base_path = trailingslashit( $base_path['path'] );
if ( 'zip' === $ext[1] || 'rar' === $ext[1] ) {
unzip_file( $file['file'], $base_path );
------------------------ CUTTED HERE ------------------------
The three functions are attached to an wp_ajax
action and don’t have proper permission or role check. The nonce check using check_ajax_referer($imgid . 'themify-plupload')
is not enough to prevent access since the nonce value could be fetched from any authenticated user such as the Subscriber role.
The functions allow a user to upload a compressed file such as zip or rar and it will be directly extracted using the unzip_file function. There is no check in which files could be extracted, resulting in arbitrary file upload.
Note that this vulnerability can be reproduced with the Subscriber role on a default installation of the theme without any additional conditions or requirements.
Authenticated Arbitrary Settings Change
This vulnerability allows authenticated users to update any settings or options on the WordPress site. Since there is no proper permission check and limitation on what meta key can be updated, this could lead to site takeover or privilege escalation. The described vulnerability was assigned CVE-2023-46148.
The underlying vulnerability is located in save_option
function :
/**
* Save a blog option.
*
* @since 1.0.0
*/
function save_option() {
check_ajax_referer('tf_nonce', 'nonce');
if (isset($_POST['option']) && isset($_POST['value'])) {
update_option($_POST['option'], stripslashes($_POST['value']));
echo 'saved';
} else {
echo 'notsaved';
}
die();
}
The function is attached to the wp_ajax_themify_customizer_save_option
ajax action. There is no permission or role validation on the function. There is indeed exist nonce validation, but the nonce value could be simply fetched from any authenticated user such as the Subscriber role.
With the addition of no limitation on which meta key could be changed on the site option, this vulnerability could lead to site takeover or privilege escalation.
Note that this vulnerability can be reproduced with the Subscriber role on a default installation of the theme without any additional conditions or requirements.
Authenticated Privilege Escalation
This vulnerability allows authenticated users to escalate their privilege to any roles on the WordPress site. Since there is no proper permission check and limitation on which role could be set for Sign Up Form, this could lead to privilege escalation. The described vulnerability was assigned CVE-2023-46145.
The underlying vulnerability is located in update_live
function :
static function update_live() {
if ( empty( $_POST['bid'] ) || empty( $_POST['data'] ) ) {
return false;
}
check_ajax_referer( 'tf_nonce', 'nonce' );
ThemifyBuilder_Data_Manager::save_data( stripslashes_deep($_POST['data']), $_POST['bid'] );
$response['status'] = 'success';
wp_send_json( $response );
}
The function could be used to save data on the layout feature provided by the theme. There is no permission or role validation on the function. There is indeed exist nonce validation, but the nonce value could be simply fetched from any authenticated user such as the Subscriber role.
One of the layout templates that is available is Sign Up Form. This layout template allows us to make a custom registration flow and design. We notice that the template allows the user to specify the role using u_role parameter that would be used to register a user. With this condition, authenticated user could just update the data of any published layout to be a Sign Up Form with the u_role
parameter set to the highest role such as administrator.
public function get_live_default() {
return array(
'success_action' => 'c',
'l_name' => __( 'Name', 'themify' ),
'l_firstname' => __( 'First', 'themify' ),
'l_lastname' => __( 'Last', 'themify' ),
'l_username' => __( 'Username', 'themify' ),
'l_email' => __( 'Email', 'themify' ),
'l_password' => __( 'Password', 'themify' ),
'l_bio' => __( 'Short Bio', 'themify' ),
'l_submit' => __( 'Submit', 'themify' ),
'desc' => __( 'Share a little information about yourself.', 'themify' ),
'u_role' => 'subscriber',
'e_user' => '1',
'e_admin' => '1',
'optin' => 'no',
'optin_label' => __( 'Subscribe to my newsletter', 'themify' ),
'gdpr_label' => __( 'I consent to my submitted data being collected and stored', 'themify' ),
);
}
Note that this vulnerability can be reproduced with the Subscriber role on a default installation of the theme without any additional conditions or requirements.
Authenticated PHP Object Injection
This issue occurs when user-supplied input is not properly sanitized before being passed to the PHP unserialize function. Since PHP allows object serialization, an authenticated user could pass ad-hoc serialized strings to a vulnerable unserialize
call, resulting in an arbitrary PHP object(s) injection into the application scope.
Since there is also no proper permission check on the affected functions, this could lead to various impacts from leaking sensitive data to remote code execution, depending on an available object that could be utilized. The described vulnerability was assigned CVE-2023-46147.
The underlying vulnerability is located in themify_import_colors
function :
function themify_import_colors() {
check_ajax_referer('tf_nonce', 'nonce');
$response['status'] = 'ERROR';
$response['msg'] = __( 'Oopsss ... .Something went wrong.', 'themify' );
if ( isset( $_FILES['file'] ) ) {
$fileContent = themify_get_file_contents( $_FILES['file']['tmp_name'] );
$new_data = unserialize( $fileContent );
------------------------ CUTTED HERE ------------------------
The function could be used to import colors. There is no permission or role validation on the function. There is indeed exist nonce validation, but the nonce value could be simply fetched from any authenticated user such as the Subscriber role.
Authenticated user could simply upload a file that contain a PHP Object Injection payload that will trigger when the code try to perform unserialize( $fileContent )
on the file data.
Note that this vulnerability can be reproduced with the Subscriber role on a default installation of the theme without any additional conditions or requirements.
The patch
For the arbitrary file upload cases, the vendor decided to apply permission check on the affected functions and limit the upload and extraction of the compressed file. The diff patch can be seen below :
For the arbitrary settings change, applying permission and nonce check on the affected function should fix the issue. The patch diff can be seen below:
For the privilege escalation vulnerability, the vendor decided to apply permission check on the update_live
function as well removing u_role
parameter from the layout template options. The diff patch can be seen below:
For the PHP object injection vulnerability, the vendor decided to limit the usage of unserialize function with allowed_clasess
set to false. This will made the process very limited and user could not execute a class object. The diff patch can be seen below :
Conclusion
The most important thing when implementing an action or process is to apply permission or role and nonce validation. Permission or role check could be validated using current_user_can
function and nonce value could be validated using wp_verify_nonce
or check_ajax_referer
.
If the component has a decompress or unzip process, only accept a limited set of whitelisted file type or extension to be extracted. Sometimes, there will be a feature for a custom registration process, always pay more attention to the role assigned to the custom registration process and limit what role the user could be register.
In general, we do not recommend using unserialize function to process data that could be partially or fully controlled by user input. We recommend using JSON instead of serialization to process more complex data structures. If the unserialize process is still needed on the application, we recommend at least configuring the allowed_classes
option set to false
.
Timeline
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.