WPLMS
Unauthenticated Arbitrary File Upload
WPLMS
Subscriber+ Arbitrary File Upload
WPLMS
Sutedent+ Arbitrary File Upload
WPLMS
Unauthenticated Privilege Escalation
WPLMS
Subscriber+ Privilege Escalation
WPLMS
Unauthenticated SQL Injection
WPLMS
Subscriber+ SQL Injection
VibeBP
Unauthenticated Privilege Escalation
VibeBP
Unauthenticated SQL Injection
VibeBP
Subscriber+ SQL Injection
This blog post is about the WPLMS and VibeBP vulnerabilities. If you’re a WPLMS and VibeBP user, please update the plugin to at least version 1.9.9.5.3 and 1.9.9.7.7 respectively.
If you are a Patchstack customer, you are protected from this vulnerability already, and no further action is required from you.
For plugin developers, we have security audit services and Enterprise API for hosting companies.
About the WPLMS and VibeBP Plugins
Both of the plugins are required plugins for the WPLMS theme, which has over 28,000 sales. The theme itself is a popular premium LMS theme for WordPress. This theme is developed by VibeThemes.
This premium LMS theme is used for creating online courses, managing students, and selling educational content. It integrates with WooCommerce, BuddyPress, and more, offering features like quizzes, certificates, and instructor dashboards.
The security vulnerabilities
The WPLMS and VibeBP plugins suffer from multiple critical vulnerabilities.
The first vulnerability is Arbitrary file upload. This vulnerability allows unauthenticated and authenticated users to upload arbitrary files to the server. In the worst-case scenario, this could lead to Remote Code Execution (RCE) when the users upload PHP files.
The second vulnerability is Privilege Escalation. This vulnerability allows unauthenticated and authenticated users with minimum roles such as Subscriber role to register as any role on an impacted website, including privileged roles such as Administrator. In the worst-case scenario, this could lead to an attacker’s full takeover of the website and malicious code installed on the server.
The third vulnerability is SQL Injection. This vulnerability allows unauthenticated and authenticated users with minimum roles such as the Subscriber role to execute malicious SQL queries and gain information leaks from the database.
All of the mentioned vulnerabilities in this article are patched on different versions of WPLMS and VibeBP plugin. We strongly recommend updating to at least version 1.9.9.5.3 for WPLMS and version 1.9.9.7.7 for the VibeBP plugin to ensure protection from all of the critical vulnerabilities.
WPLMS: Unauthenticated Arbitrary File Upload
This vulnerability is assigned CVE-2024-56046. The vulnerable code exists in the wplms_form_uploader_plupload function, found in includes/vibe-shortcodes/shortcodes.php:
function wplms_form_uploader_plupload(){
check_ajax_referer('wplms_form_uploader_plupload');
if (empty($_FILES) || $_FILES['file']['error']) {
die('{"OK": 0, "info": "Failed to move uploaded file."}');
}
$chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
$chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
$upload_dir_base = wp_upload_dir();
$folderPath = $upload_dir_base['basedir']."/wplms_form_uploader";
if(function_exists('is_dir') && !is_dir($folderPath)){
if(function_exists('mkdir'))
mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
}
$filePath = $folderPath."/$fileName";
// Open temp file
if($chunk == 0)
$perm = "wb" ;
else
$perm = "ab";
$out = @fopen("{$filePath}.part",$perm );
if ($out) {
// Read binary input stream and append it to temp file
$in = @fopen($_FILES['file']['tmp_name'], "rb");
if ($in) {
while ($buff = fread($in, 4096))
fwrite($out, $buff);
} else
die('{"OK": 0, "info": "Failed to open input stream."}');
@fclose($in);
@fclose($out);
@unlink($_FILES['file']['tmp_name']);
} else
die('{"OK": 0, "info": "Failed to open output stream."}');
// Check if file has been uploaded
if (!$chunks || $chunk == $chunks - 1) {
// Strip the temp .part suffix off
rename("{$filePath}.part", $filePath);
}
die('{"OK": 1, "info": "Upload successful."}');
exit;
}
This function is called by the wp_ajax_nopriv_wplms_form_uploader_plupload action and can be accessed by an unauthenticated user. There is no proper check on the $_FILES and $_REQUEST[“name”] which is used as the uploaded file name on the server, resulting in arbitrary file upload on the server.
WPLMS: Subscriber+ Arbitrary File Upload
This vulnerability is assigned CVE-2024-56050. The vulnerable code exists in the wp_ajax_zip_upload function, found in includes/vibe-shortcodes/upload_handler.php:
function wp_ajax_zip_upload(){
$arr = array();
$file = $_FILES['uploadedfile']['tmp_name'];
$dir = explode(".",$_FILES['uploadedfile']['name']);
$dir[0] = str_replace(" ","_",$dir[0]);
$target = $this->getUploadsPath().$dir[0];
$index = count($dir) -1;
if (!isset($dir[$index]) || $dir[$index] != "zip")
$arr[0] = __('The Upload file must be zip archive','wplms');
else{
while(file_exists($target)){
$r = rand(1,10);
$target .= $r;
$dir[0] .= $r;
}
if (!empty($file))
$arr = $this->extractZip($file,$target,$dir[0]);
else
$arr[0] = __('File too big','wplms');
}
echo json_encode($arr);
die();
}
This function is called by the wp_ajax_zip_upload action and can be accessed by any authenticated user such as a Subscriber role user. In this function, users are allowed to upload a ZIP file and it will be processed by $this->extractZip function:
function extractZip($fileName,$target,$dir){
$arr = array();
$zip = new ZipArchive;
$res = $zip->open($fileName);
if ($res === TRUE) {
$zip->extractTo($target);
$zip->close();
$file = $this->getFile($target);
;
if($file){
$arr[0] = 'uploaded';
$arr[1] = $this->getUploadsUrl().$dir."/".$file;
$arr[2] = $dir;
$arr[3] =$file;
$arr[4] = $this->getUploadsPath().$dir;
}else{
$arr[0] = __('Please upload zip file, Index.html file not found in package','wplms').$target.print_r($file);
$this->rrmdir($target);
}
}else{
$arr[0] = __('Upload failed !','wplms');;
}
return $arr;
}
The extractZip function itself simply extracted all of the files inside of the uploaded ZIP file to $target location which users can control via $_FILES[‘uploadedfile’][‘name’] value on the wp_ajax_zip_upload function. Since there is no pre-check on the files inside of the ZIP, users can just host a PHP file inside of the ZIP file and upload the ZIP file to the server.
WPLMS: Student+ Arbitrary File Upload
This vulnerability is assigned CVE-2024-56052. The vulnerable code exists in the wplms_assignment_plupload function, found in includes/assignments/assignments.php:
function wplms_assignment_plupload(){
check_ajax_referer('wplms_assignment_plupload');
if(!is_user_logged_in())
die('user not logged in');
$user_id = get_current_user_id();
if (empty($_FILES) || $_FILES['file']['error']) {
die('{"OK": 0, "info": "Failed to move uploaded file."}');
}
$chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
$chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
$upload_dir_base = wp_upload_dir();
$assignment_id = $_POST['assignment_id'];
$folderPath = $upload_dir_base['basedir']."/wplms_assignments_folder/".$user_id.'/'.$assignment_id;
if(function_exists('is_dir') && !is_dir($folderPath)){
if(function_exists('mkdir'))
mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
}
$filePath = $folderPath."/$fileName";
/*if(function_exists('file_exists') && file_exists($filePath)){
echo __(' Chunks upload error ','wplms'). $fileName.__(' already exists.Please rename your file and try again ','wplms');
die();
}*/
// Open temp file
if($chunk == 0) $perm = "wb" ;
else $perm = "ab";
$out = @fopen("{$filePath}.part",$perm );
if ($out) {
// Read binary input stream and append it to temp file
$in = @fopen($_FILES['file']['tmp_name'], "rb");
if ($in) {
while ($buff = fread($in, 4096))
fwrite($out, $buff);
} else
die('{"OK": 0, "info": "Failed to open input stream."}');
@fclose($in);
@fclose($out);
@unlink($_FILES['file']['tmp_name']);
} else
die('{"OK": 0, "info": "Failed to open output stream."}');
// Check if file has been uploaded
if (!$chunks || $chunk == $chunks - 1) {
// Strip the temp .part suffix off
rename("{$filePath}.part", $filePath);
}
die('{"OK": 1, "info": "Upload successful."}');
exit;
}
This function is called by the wp_ajax_wplms_assignment_plupload action. Although the function has a nonce check and is_user_logged_in check, the function can still be accessed by users with a Student role. There is no proper check on the $_FILES and $_REQUEST[“name”] which is used as the uploaded file name on the server, resulting in arbitrary file upload on the server.
WPLMS: Unauthenticated Privilege Escalation
This vulnerability is assigned CVE-2024-56043. The vulnerable code exists in the wplms_register_user function, found in includes/vibe-shortcodes/ajaxcalls.php:
function wplms_register_user(){
if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'bp_new_signup') || !isset($_POST['settings'])){
echo '<div class="message">'.__('Security check Failed. Contact Administrator.','wplms').'</div>';
die();
}
$flag = 0;
$settings = json_decode(stripslashes($_POST['settings']));
if(empty($settings)){
$flag = 1;
}
------------- CUT HERE -------------
$user_args = $user_fields = $save_settings = array();
if(empty($flag)){
------------- CUT HERE -------------
foreach($settings as $setting){
if(!empty($setting->id)){
$settings2[] = $setting->id;
if($setting->id == 'signup_username'){
$user_args['user_login'] = $setting->value;
}else if($setting->id == 'signup_email'){
$user_args['user_email'] = $setting->value;
}else if($setting->id == 'signup_password'){
$user_args['user_pass'] = $setting->value;
}else{
if(strpos($setting->id,'field') !== false){
$f = explode('_',$setting->id);
$field_id = $f[1];
if(strpos($field_id, '[')){ //checkbox
$v = str_replace('[','',$field_id);
$v = str_replace(']','',$v);
$field_id = $v;
if(is_Array($user_fields[$field_id]['value'])){
$user_fields[$field_id]['value'][] = $setting->value;
}else{
$user_fields[$field_id] = array('value'=>array($setting->value));
}
}else{
if(is_numeric($field_id) && !isset($f[2])){
$user_fields[$field_id] = array('value'=>$setting->value);
}else{
if(in_array($f[2],array('day','month','year'))){
$user_fields['field_' . $field_id . '_'.$f[2]] = $setting->value;
}else{
$user_fields[$field_id]['visibility']=$setting->value;
}
}
}
}else{
if(isset($form_settings[$setting->id])){
$form_settings[$setting->id] = 0; // use it for empty check
if($setting->id=='default_role'){
$save_settings[$setting->id]=$setting->value;
$user_args['role'] = $setting->value;
}
if($setting->id=='member_type'){
$save_settings[$setting->id]=$setting->value;
$member_type=$setting->value;
}
if($setting->id=='wplms_user_bp_group'){
if(in_array($setting->value,$reg_form_settings['settings']['wplms_user_bp_group']) || $reg_form_settings['settings']['wplms_user_bp_group'] === array('enable_user_select_group')){
$save_settings[$setting->id]=$setting->value;
$wplms_user_bp_group = $setting->value;
}else{
echo '<div class="message_wrap"><div class="message error">'._x('Invalid Group selection','error message when group is not valid','wplms').'<span></span></div></div>';
die();
}
}
}
}
}
}
}
if(!in_array('wplms_user_bp_group', $settings2)){
if(!empty($reg_form_settings['settings']['wplms_user_bp_group']) && is_array($reg_form_settings['settings']['wplms_user_bp_group']) && $reg_form_settings['settings']['wplms_user_bp_group'] !== array('enable_user_select_group') && count($reg_form_settings['settings']['wplms_user_bp_group'])==1){
$wplms_user_bp_group = $reg_form_settings['settings']['wplms_user_bp_group'][0];
}
}
}
------------- CUT HERE -------------
/*
FORM SETTINGS
*/
if(empty($form_settings['hide_username'])){
$user_args['user_login'] = $user_args['user_email'];
}
$user_id = 0;
if(empty($form_settings['skip_mail'])){
$user_id = wp_insert_user($user_args);
------------- CUT HERE -------------
This function is called by the wp_ajax_nopriv_wplms_register_user action and is used to process registration form submission. First, users are able to arbitrarily set the $settings object. Then, there is a $user_args object which is used to construct user data for registration. The code will assign $user_args[‘role’] with $setting->value which is simply a value from $settings[‘default_role’]. Lastly, the function will register the user using wp_insert_user($user_args). Since there is no proper check on the $user_args[‘role’] value, users can just supply arbitrary roles on the registration process and can escalate their privilege to any role including the Administrator role.
WPLMS: Subscriber+ Privilege Escalation
This vulnerability is assigned CVE-2024-56048. The vulnerable code exists in the update_license_key function, found in includes/vibe-customtypes/includes/musettings.php:
function update_license_key(){
if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'security')){
_e('Security check Failed. Contact Administrator.','wplms');
die();
}
if(empty($_POST['addon']) || empty($_POST['key'])){
_e('Unable to update key.','wplms');
die();
}
update_option($_POST['addon'],$_POST['key']);
echo apply_filters('wplms_addon_license_key_updated',__('Key Updated.','wplms'));
die();
}
This function is called by the wp_ajax_vibe_update_license_key action and can be accessed by any authenticated users such as a Subscriber role since the nonce can be fetched from a Subscriber role account and there is no proper permission check on the function. Since there is no restriction on the $_POST[‘addon’] and $_POST[‘key’] variables passed to the update_option function, this results in an Arbitrary Option Update and allows the user to set any of the site options to any value. With this, users can simply enable the users_can_register option and then set the default_role option to any role such as the Administrator role. This will result in open registration on the WP site and the assigned role upon registration is Administrator role.
WPLMS: Unauthenticated SQL Injection
This vulnerability is assigned CVE-2024-56042. Originally, we found around 20+ affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the get_instructor_commissions_chart function, found in includes/vibe-course-module/includes/api/v3/class-api-commissions.php:
function get_instructor_commissions_chart($request){
$user_id = $request->get_param('id');
$course_id =$request->get_param('course_id');
$date_start = $request->get_param('date_start');
$date_end = $request->get_param('date_end');
$currency = $request->get_param('currency');
------------ CUT HERE ------------
$and_where = '';
$start_date = '';
$end_date = '';
$group_by = ' GROUP BY select_parameter';
$select = 'MONTH(activity.date_recorded) as select_parameter';
if(!empty($course_id)){
$and_where .= " AND activity.item_id = $course_id ";
}else{
------------ CUT HERE ------------
}
if(!empty($currency)) {
$and_where .= " AND meta2.meta_value = '".$currency."' ";
}
------------ CUT HERE ------------
global $wpdb;
global $bp;
$results = $wpdb->get_results( "
SELECT ".$select.", sum(meta.meta_value) as commission
FROM {$bp->activity->table_name} AS activity
LEFT JOIN {$bp->activity->table_name_meta} as meta ON activity.id = meta.activity_id
LEFT JOIN {$bp->activity->table_name_meta} as meta2 ON activity.id = meta2.activity_id
WHERE activity.component = 'course'
AND activity.type = 'course_commission'
AND activity.user_id = {$user_id}
AND meta.meta_key LIKE '_commission%'
AND meta2.meta_key LIKE '_currency%'
".$and_where."
".$group_by,ARRAY_A);
------------ CUT HERE ------------
}
This function is handling a REST endpoint of /wp-json/wplms/v1/commissions/instructor/<ID>/chart. Let’s take a look at the REST endpoint registration process:
register_rest_route( $this->namespace, '/instructor/(?P<id>\d+)/chart/', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_instructor_commissions_chart' ),
'permission_callback' => array( $this, 'commissions_request_validate' ),
'args' => array(
'id' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
}
),
),
),
));
The REST endpoint itself has a permission check using commissions_request_validate:
function commissions_request_validate($request){
$user_id = $request->get_param('id');
$user = get_userdata( $user_id );
if ( $user === false ) {
return false;
} else {
return true;
}
------------ CUT HERE ------------
As we can see, unauthenticated users can simply bypass the check by providing any valid user ID value. Back to the get_instructor_commissions_chart function, there is no proper escaping process on $course_id and $currency resulting in SQL Injection. Note that we are not able to inject the $user_id variable because the value is coming from $request->get_param(‘id’) which only allows valid numeric values via the validate_callback args.
WPLMS: Subscriber+ SQL Injection
This vulnerability is assigned CVE-2024-56047. Originally, we found around 10+ affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the search_users_in_chat function, found in includes/vibe-course-module/includes/api/v3/class-api-user-controller.php:
function search_users_in_chat($request){
global $wpdb;
$user_initials = $request->get_param('user_initials');
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}users WHERE `user_nicename` LIKE '%{$user_initials}%'", ARRAY_A );
$return = array('status'=>1,'message'=>'','users'=>array());
if(!empty($results)){
foreach($results as $result){
$return['users'][]=apply_filters('wplms_api_search_users_in_chat',array(
'name'=> bp_core_get_user_displayname($result['ID']),
'id'=> intval($result['ID']),
'image'=> bp_core_fetch_avatar(array('item_id' => $result['ID'],'type'=>'thumb', 'html' => false)),
'type'=> (user_can(intval($result['ID']),'manage_options')?_x('Administrator','Chat search result user type','wplms'):(user_can($result['ID'],'edit_posts')?_x('Instructor','Chat search result user type','wplms'):_x('Student','Chat search result user type','wplms')))
));
}
}else{
$return = array('status'=> 0,'message'=>_x('No user found !','Chat search result','wplms'),'users'=>array());
}
return new WP_REST_Response($return, 200);
}
This function handles a REST endpoint of /wp-json/wplms/v2/user/alluser and can be accessed by any authenticated users. We are able to inject SQL query to $user_initials which is constructed from $request->get_param(‘user_initials’) since there is no proper escaping.
VibeBP: Unauthenticated Privilege Escalation
This vulnerability is assigned CVE-2024-56040. The vulnerable code exists in the vibebp_register_user function, found in includes/class.ajax.php:
function vibebp_register_user(){
if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'bp_new_signup') || !isset($_POST['settings'])){
echo '<div class="message">'.__('Security check Failed. Contact Administrator.','wplms').'</div>';
die();
}
$flag = 0;
$settings = json_decode(stripslashes($_POST['settings']));
if(empty($settings)){
$flag = 1;
}
------------- CUT HERE -------------
$user_args = $user_fields = $save_settings = array();
if(empty($flag)){
------------- CUT HERE -------------
foreach($settings as $setting){
if(!empty($setting->id)){
$settings2[] = $setting->id;
if($setting->id == 'signup_username'){
$user_args['user_login'] = $setting->value;
}else if($setting->id == 'signup_email'){
$user_args['user_email'] = $setting->value;
}else if($setting->id == 'signup_password'){
$user_args['user_pass'] = $setting->value;
}else{
if(strpos($setting->id,'field') !== false){
$f = explode('_',$setting->id);
$field_id = $f[1];
if(strpos($field_id, '[')){ //checkbox
$v = str_replace('[','',$field_id);
$v = str_replace(']','',$v);
$field_id = $v;
if(is_Array($user_fields[$field_id]['value'])){
$user_fields[$field_id]['value'][] = $setting->value;
}else{
$user_fields[$field_id] = array('value'=>array($setting->value));
}
}else{
if(is_numeric($field_id) && !isset($f[2])){
$user_fields[$field_id] = array('value'=>$setting->value);
}else{
if(in_array($f[2],array('day','month','year'))){
$user_fields['field_' . $field_id . '_'.$f[2]] = $setting->value;
}else{
$user_fields[$field_id]['visibility']=$setting->value;
}
}
}
}else{
if(isset($form_settings[$setting->id])){
$form_settings[$setting->id] = 0; // use it for empty check
if($setting->id=='default_role'){
$save_settings[$setting->id]=$setting->value;
$user_args['role'] = $setting->value;
}
if($setting->id=='member_type'){
$save_settings[$setting->id]=$setting->value;
$member_type=$setting->value;
}
if($setting->id=='vibebp_user_bp_group'){
if(in_array($setting->value,$reg_form_settings['settings']['vibebp_user_bp_group']) || $reg_form_settings['settings']['vibebp_user_bp_group'] === array('enable_user_select_group')){
$save_settings[$setting->id]=$setting->value;
$vibebp_user_bp_group = $setting->value;
}else{
echo '<div class="message_wrap"><div class="message error">'._x('Invalid Group selection','error message when group is not valid','wplms').'<span></span></div></div>';
die();
}
}
}
}
}
}
}
if(!in_array('vibebp_user_bp_group', $settings2)){
if(!empty($reg_form_settings['settings']['vibebp_user_bp_group']) && is_array($reg_form_settings['settings']['vibebp_user_bp_group']) && $reg_form_settings['settings']['vibebp_user_bp_group'] !== array('enable_user_select_group') && count($reg_form_settings['settings']['vibebp_user_bp_group'])==1){
$vibebp_user_bp_group = $reg_form_settings['settings']['vibebp_user_bp_group'][0];
}
}
}
$user_args = apply_filters('vibebp_register_user_args',$user_args);
//hook for validations externally
do_action('vibebp_custom_registration_form_validations',$name,$settings,$all_form_settings,$user_args);
do_action('wplms_custom_registration_form_validations',$name,$settings,$all_form_settings,$user_args);
/*
RUN CONDITIONAL CHECKS
*/
$check_filter = filter_var($user_args['user_email'], FILTER_VALIDATE_EMAIL); // PHP 5.3
if(empty($user_args['user_email']) || empty($user_args['user_pass']) || empty($check_filter)){
echo '<div class="message_wrap"><div class="message error">'._x('Invalid Email/Password !','error message when registration form is empty','wplms').'<span></span></div></div>';
die();
}
//Check if user exists
if(!isset($user_args['user_email']) || email_exists($user_args['user_email'])){
echo '<div class="message_wrap"><div class="message error">'._x('Email already registered.','error message','wplms').'<span></span></div></div>';
die();
}
//Check if user exists
if(!isset($user_args['user_login'])){
$user_args['user_login'] = $user_args['user_email'];
if(email_exists($user_args['user_login'])){
echo '<div class="message_wrap"><div class="message error">'._x('Username already registered.','error message','wplms').'<span></span></div></div>';
die();
}
}elseif (username_exists($user_args['user_login'])){
echo '<div class="message_wrap"><div class="message error">'._x('Username already registered.','error message','wplms').'<span></span></div></div>';
die();
}
------------- CUT HERE -------------
/*
FORM SETTINGS
*/
if(empty($form_settings['hide_username'])){
$user_args['user_login'] = $user_args['user_email'];
}
$user_id = 0;
if(empty($form_settings['skip_mail'])){
$user_id = wp_insert_user($user_args);
------------- CUT HERE -------------
The condition of this vulnerability is quite similar to the Unauthenticated Privilege Escalation case in the WPLMS plugin. This function is called by the wp_ajax_nopriv_vibebp_register_user action and is used to process registration form submission. First, users are able to arbitrarily set the $settings object. Then, there is a $user_args object which is used to construct user data for registration. The code will assign $user_args[‘role’] with $setting->value which is simply a value from $settings[‘default_role’]. Lastly, the function will register the user using wp_insert_user($user_args). Since there is no proper check on the $user_args[‘role’] value, users can just supply arbitrary roles on the registration process and can escalate their privilege to any role including the Administrator role.
VibeBP: Unauthenticated SQL Injection
This vulnerability is assigned CVE-2024-56039. Originally, we found around 3 affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the get_avatar function, found in includes/buddypress/class-api-settings-controller.php:
function get_avatar($request){
$body = json_decode($request->get_body(),true);
$body = vibebp_recursive_sanitize_text_field($body);
$name = '';
$avatar= '';
$key='';
$type = '';
if(!empty($body['type'])){$type=$body['type'];}
switch($type){
case 'friends':
$key = 'user_'.$body['ids']['item_id'];
$avatar = bp_core_fetch_avatar(array(
'item_id' => (int)$body['ids']['item_id'],
'object' => 'user',
'type'=>'thumb',
'html' => false
));
$name = bp_core_get_user_displayname($body['ids']['item_id']);
break;
case 'group':
$key = 'group_'.$body['ids']['item_id'];
$avatar = bp_core_fetch_avatar(array(
'item_id' => (int)$body['ids']['item_id'],
'object' => 'group',
'type'=>'thumb',
'html' => false
));
global $wpdb,$bp;
$name = $wpdb->get_var("SELECT name from {$bp->groups->table_name} WHERE id=".$body['ids']['item_id']);
------------- CUT HERE -------------
This function handles the REST endpoint of /wp-json/vbp/v1/avatar. Let’s take a look at the REST endpoint registration process:
register_rest_route( $this->namespace, '/avatar', array(
array(
'methods' => 'POST',
'callback' => array( $this, 'get_avatar'),
'permission_callback' => array( $this, 'get_client_permissions' ),
),
));
The REST endpoint itself has a permission check using get_client_permissions:
function get_client_permissions($request){
$client_id = $request->get_param('client_id');
if($client_id == vibebp_get_setting('client_id')){
return true;
}
return $this->get_settings_permissions($request);
}
The vibebp_get_setting(‘client_id’) value itself can be fetched by unauthenticated users when they try to complete a checkout process in WooCommerce. Back to the get_avatar function, there is no proper escaping process on $body[‘ids’][‘item_id’] resulting in SQL Injection.
VibeBP: Subscriber+ SQL Injection
This vulnerability is assigned CVE-2024-56041. Originally, we found around 2 affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the remove_message_label function, found in includes/buddypress/class-api-messages-controller.php:
function remove_message_label($request){
$body = json_decode($request->get_body(),true);
$body = vibebp_recursive_sanitize_text_field($body);
$labels = get_user_meta($this->user->id,'vibebp_message_labels',true);
if(!empty($labels)){
$remove = 0;
foreach($labels as $k=>$l){
if($l['slug'] === $body['slug']){
$remove = $k;
break;
}
}
$label_key = 'vibebp_label_'.$this->user->id;
$slug = $body['slug'];
global $wpdb,$bp;
$labels_count = $wpdb->get_results("DELETE FROM {$bp->messages->table_name_meta} WHERE meta_key = '$label_key' AND meta_value = '$slug'");
unset($labels[$remove]);
update_user_meta($this->user->id,'vibebp_message_labels',$labels);
}
return new WP_REST_Response( array('status'=>1,'labels'=>$labels,'message'=>_x('Label removed.','message','vibebp')), 200 );
}
This function handles the REST endpoint of /wp-json/vbp/v1/messages/label/remove. The endpoint can be accessed by any authenticated users such as Subscriber role users. Since there is no proper escaping process on $slug, users are able to perform SQL Injection.
NOTE: Originally, we managed to report 18 different vulnerabilities to both WPLMS and VibeBP plugin. We only cover some of the critical vulnerabilities in this article.
The Patch
For the Arbitrary File Upload vulnerabilities, the vendor applies a patch to limit which file can be uploaded using a check on the file name and types. On some of the issues of Arbitrary File Upload, vendors also implement additional permission checks to the affected functions or remove the affected code.
For the Privilege Escalation vulnerabilities, the vendor applies a patch to limit which roles a user can register as. The patch implements a change where the user will be assigned a default role configured from the registration form setting. For Privilege Escalation via Arbitrary Option Update, the vendor implements an additional permission check on the function and applies a whitelist check on the option name that can be updated.
For SQL Injection vulnerabilities, the vendor applies a proper escaping to all of the reported variables and code.
Conclusion
The vulnerabilities discussed here highlight the importance of secure file upload, registration, and SQL query processes.
In the context of registration, care needs to be taken to ensure that users can only register with specifically acceptable roles. When implementing a custom registration portal, we recommend utilizing allowlists of only specifically allowed roles or using a default role on the registration process.
In the context of the file upload process, always implement both file name and file type checks and only a allow specific set of file types to be uploaded by the users. In this case, we recommend applying a whitelist check instead of a blacklist check.
In the context of the SQL query process, always make sure that the controlled user’s input is properly escaped when constructed into an SQL query. The best practice is using a prepared statement with a proper implementation.
Want to learn more about finding and fixing vulnerabilities?
Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.
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.