Hacker Already Targeting Plugin With Vulnerability Exposed by Wordfence Today Without Fix Being Available
Today, we have had two requests on our website checking if we were using a WordPress plugin by checking for the readme.txt file for it. The requests were for the path /wp-content/plugins/baiduseo/readme.txt. Those appeared to come from a hacker. Why would that be? Well the plugin, SEO合集(支持百度/Google/Bing/头条推送), was closed on the WordPress plugin directory yesterday:
No reason has been given for the closure.
Earlier today, the developer submitted an update with a lot of security related changes. One of those changes is connected to the likely explanation for the hacker(s)’s interest. Wordfence today told hackers about an arbitrary file upload vulnerability in the plugin. The claimed discoverer of the vulnerability appears to have sold the vulnerability to Wordfence instead of reporting it to the developer.
Considering how obvious it is that this would be exploited, it is unfathomable that there wasn’t a coordinated process to get this fixed and the update automatically pushed out to avoid exploitation. But Wordfence and WordPress didn’t do that. (We have for years offered to provide those fixes to WordPress if the developer isn’t providing one.)
What Went Wrong?
Since the cat is already out of the bag, let’s take a look at what went wrong here.
In the plugin’s main file, the function init() from the class baiduseo_common is called:
29 30 | $baiduseo_common = new baiduseo_common(); $baiduseo_common->init(); |
That means the code runs whenever the plugin is active.
That function in turn calls the function post() in the class baiduseo_youhua (which is located in the file /seo_title_baidu.php) if user input in the form of the POST input “data” is sent with a request:
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public function init(){ add_action('wp',[$this,'baiduseo_init_session']); $baiduseo_zz = get_option('baiduseo_zz'); //兼容所有文章类型的推送 if(isset($baiduseo_zz['status']) && strpos($baiduseo_zz['status'],'2') !== false){ $tuisong = explode(',',$baiduseo_zz['post_type']); foreach($tuisong as $k=>$v){ add_action('publish_'.$v,[$this,'baiduseo_articlepublish']); add_action('publish_future_'.$v,[$this,'baiduseo_articlepublish']); add_action('wp_trash_'.$v,[$this,'baiduseo_delete_post'],91); } if(is_array($tuisong) && !in_array('post',$tuisong)){ add_action('publish_post',[$this,'baiduseo_articlepublish']); add_action('publish_future_post',[$this,'baiduseo_articlepublish']); } }else{ add_action('publish_post',[$this,'baiduseo_articlepublish']); add_action('publish_future_post',[$this,'baiduseo_articlepublish']); } if(is_admin()){ add_action( 'admin_enqueue_scripts', [$this,'baiduseo_enqueue'] ); add_filter('plugin_action_links_'.BAIDUSEO_NAME, [$this,'baiduseo_plugin_action_links']); add_action('admin_menu', [$this,'baiduseo_addpages']); }else{ add_action( 'wp_head', [$this,'baiduseo_mainpage'],1); } //由于插件需求,有一些数据是其他服务器推送过来的,无法满足随机字符串验证 if(isset($_POST['data']) && is_string($_POST['data'])){ //过滤json数据 $BaiduSEO = baiduseo_seo::sanitizing_json($_POST['data']); if(is_array($BaiduSEO) && isset($BaiduSEO['BaiduSEO'])){ baiduseo_youhua::post($BaiduSEO); |
Code in that function, which is located in the file /inc/index/youhua.php, will pass some of that user input to the function download_remote_image_to_media_library():
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | }elseif($data['BaiduSEO']==49){ global $wp_rewrite,$wp_filesystem; if (null === $wp_rewrite) { $wp_rewrite = new WP_Rewrite(); } require_once(ABSPATH . 'wp-includes/pluggable.php'); global $wpdb; //查重 if($data['is_chachong']==1){ $article = $wpdb->get_results($wpdb->prepare('select ID from '.$wpdb->prefix . 'posts where post_title=%s and post_status="publish" and post_type="post"',$data['title']),ARRAY_A); if(!empty($article)){ exit; } } if($data['img']){ $attach_id = self::download_remote_image_to_media_library($data['img']); |
The final function in the chain will save the contents of a specified URL as a file on the website:
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 | public static function download_remote_image_to_media_library( $image_url ) { global $wp_rewrite,$wp_filesystem; if (null === $wp_rewrite) { $wp_rewrite = new WP_Rewrite(); } // 初始化 WP_Filesystem if ( empty( $wp_filesystem ) ) { require_once( ABSPATH . 'wp-admin/includes/file.php' ); WP_Filesystem(); } // 下载远程图片 $response = wp_remote_get( $image_url ); // 获取图片内容 $image_data = wp_remote_retrieve_body( $response ); // 计算 MD5 哈希值作为文件名 $md5_filename = md5( $image_data ); $file_extension = pathinfo( $image_url, PATHINFO_EXTENSION ); // 获取文件扩展名 $file_name = $md5_filename . '.' . $file_extension; // 确定上传目录 $upload_dir = wp_upload_dir(); $file_path = trailingslashit( $upload_dir['path'] ) . $file_name; // 使用 WP_Filesystem 保存图片 $wp_filesystem->put_contents( $file_path, $image_data, FS_CHMOD_FILE ); |
The file will be saved in the upload directory for the current month with the name based on the md5 hash of the contents of the URL.
The “Fix”
The code looks like it is probably missing multiple security checks. The one security check that was added was to restrict what files can be saved to files permitted by WordPress (as defined by wp_check_filetype()):
312 313 314 315 316 | if($data['img']){ $validate = wp_check_filetype($data['img']); if ( $validate['type'] == false ) { exit; } |
That would be enough to prevent say, uploading files with a .php extension, but would still allow a spammer to upload other types of files allowed by WordPress.
Free Warning
As the vulnerability is being targeted by a hacker, we are adding accurate data on it to the free data that comes with our Plugin Vulnerabilities plugin.