14 May 2025

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.

Leave a Reply

Your email address will not be published.