Critical Vulnerabilities Patched in WordPress Automatic Plugin

Published 19 March 2024
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

This blog post is about the Automatic plugin vulnerabilities. If you're an Automatic user, please update the plugin to at least version 3.92.1.

All paid Patchstack users are protected from this vulnerability. Sign up for the free Community account first, to scan for vulnerabilities and apply protection for only $5 / site per month with Patchstack.

For plugin developers, we have security audit services and Enterprise API for hosting companies.

About the Automatic Plugin

The plugin Automatic (premium version), which is estimated to have over 40,000 active installations, is known as the more popular automatic content posts plugin in WordPress. This plugin is developed by ValvePress.

This premium WordPress plugin has some features, one of which is to create posts from almost any website to WordPress automatically. It can import from popular sites like YouTube and Twitter utilizing the APIs or from almost any website of our choice using scraping modules. The plugin also now can generate content using OpenAI GPT.

The security vulnerability

This plugin suffers from multiple critical vulnerabilities and could allow any unauthenticated user to read local files and gain a full-scale SQL query execution on the WordPress site.

The first vulnerability is Unauthenticated Arbitrary SQL Execution. This vulnerability allows any unauthenticated user to fully control an SQL query that will be executed on the WordPress site. The second vulnerability is Unauthenticated Arbitrary File Download and SSRF. This vulnerability allows any unauthenticated user to read arbitrary local files and perform a Server-Side Request Forgery (SSRF) attack on the WordPress site server. The described vulnerabilities were fixed in version 3.92.1 and assigned CVE-2024-27956 and CVE-2024-27954 respectively.

Unauthenticated Arbitrary SQL Execution

The underlying vulnerability exists on inc/csv.php file:

<?php
require_once('../../../../wp-load.php');
global $wpdb;

 

  global $current_user;
  wp_get_current_user();

     //   echo user_login . "'s email address is: " . $current_user->user_pass;
 

//get admin pass for integrity check 


// extract query
$q = stripslashes($_POST['q']);
$auth = stripslashes($_POST['auth']);
$integ=stripslashes($_POST['integ']);

if(wp_automatic_trim($auth == '')){
	
	  echo 'login required';
	exit;
}

if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
	  echo 'invalid login';
	exit;
}

if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
	  echo 'Tampered query';
	exit;
}
 

$rows=$wpdb->get_results( $q);
$date=date("F j, Y, g:i a s");
$fname=md5($date);
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=$fname.csv");
header("Pragma: no-cache");
header("Expires: 0");

  echo "DATE,ACTION,DATA,KEYWORD \n";
foreach($rows as $row){
	
	$action=$row->action;
	if (stristr($action , 'New Comment Posted on :')){
			$action = 'Posted Comment';
		}elseif(stristr($action , 'approved')){
			$action = 'Approved Comment';
	}
	
	//format date
	$date=date('Y-n-j H:i:s',strtotime ($row->date));

	$data=$row->data;
	$keyword='';
	//filter the data strip keyword
	if(stristr($data,';')){
		$datas=explode(';',$row->data);
		$data=$datas[0];
		$keyword=$datas[1];
	}
	  echo "$date,$action,$data,$keyword \n";

}

//  echo "record1,$q,record3\n";

?>

We can see that we are able to supply an arbitrary SQL query on $q variable and it will be executed with $wpdb->get_results( $q). However, there are checks implemented using wp_automatic_trim($current_user->user_pass) and then md5(wp_automatic_trim($q.$current_user->user_pass)).

Let's see the content of wp_automatic_trim function:

function wp_automatic_trim($str)
{
	if (is_null($str)) {
		return '';
	} else {
		return trim($str);
	}
}

The function just as stated in the function name, performs a basic trim to the supplied value. The first check involves $current_user->user_pass value. This value would be an empty string if the file is accessed by an unauthenticated user. So, we can just supply an empty string to the $auth variable. For the second check, we just need to supply only the MD5 value of our supplied SQL query to the $integ since $current_user->user_pass is an empty string. However, before the two checks, there is a check of if(wp_automatic_trim($auth == '')) making us can't just input an empty string to the $auth. To bypass this, we can just supply a single whitespace (" ") to the $auth and we are able to achieve an arbitrary SQL query execution.

Unauthenticated Arbitrary File Download and SSRF

The underlying vulnerability exists on downloader.php file:

function curl_exec_follow( &$ch){

	$max_redir = 3;

	for ($i=0;$i<$max_redir;$i++){

		$exec=curl_exec($ch);
		$info = curl_getinfo($ch);

		
		if($info['http_code'] == 301 ||  $info['http_code'] == 302  ||  $info['http_code'] == 307 ){
				
			curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($info['redirect_url']));
			$exec=curl_exec($ch);
				
		}else{
				
			//no redirect just return
			break;
				
		}


	}

	return $exec;

}

$link=$_GET['link'];//urldecode();
    $link=wp_automatic_str_replace('httpz','http',$link);
    //$link='http://ointmentdirectory.info/%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B9%81%E0%B8%AA%E0%B8%94%E0%B8%87%E0%B8%A0%E0%B8%B2%E0%B8%9E%E0%B8%99%E0%B8%B4%E0%B9%88%E0%B8%87-%E0%B8%97%E0%B8%AD%E0%B8%94%E0%B8%9B%E0%B8%A5%E0%B8%B2%E0%B9%80%E0%B8%9E';
    //  echo $link ;
    //exit ;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($link));
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
    curl_setopt($ch,CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_REFERER, 'http://bing.com');
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8');
    curl_setopt($ch,CURLOPT_MAXREDIRS, 5); // Good leeway for redirections.
    curl_setopt($ch,CURLOPT_FOLLOWLOCATION, 0); // Many login forms redirect at least once.
    
    $exec=curl_exec_follow($ch);

    
    
    $res=array();
    //get the link 
    $curlinfo=curl_getinfo($ch);
    
     
	$original_link=$curlinfo['url'];
	$original_link=wp_automatic_str_replace("?hop=zzzzz",'',$original_link);
	$res['link']=$original_link;
	
	//get the title
	preg_match("/<title>(.*?)<\/title>/i",$exec,$matches );
	$ret=$matches[1];

	$res['title']=$ret;
	$res['status']='success';

	$ret = array();
	
	/*** a new dom object ***/
	$dom = new domDocument;
	
	/*** get the HTML (suppress errors) ***/
	@$dom->loadHTML($exec);
	
	/*** remove silly white space ***/
	$dom->preserveWhiteSpace = false;
	
	/*** get the links from the HTML ***/
	$text = $dom->getElementsByTagName('p');
	
	/*** loop over the links ***/
	foreach ($text as $tag)
	{
		$textContent = $tag->textContent;
	
		if(wp_automatic_trim($textContent) == '' || strlen($textContent) < 25 || stristr($textContent, 'HTTP') || stristr($textContent, '$')) continue;
		$ret[] = $textContent;
		
	}
	
	$res['text']=$ret;
	
	  echo json_encode($res);

	exit;
    
    @unlink('files/temp.html');
    $cont=curl_exec($ch);
    //$cont=file_get_contents($link);
    if (curl_error($ch)){
    	  echo 'Curl Error:error:'.curl_error($ch);
    }
    file_put_contents('files/temp.html',$link.$cont);
?>

The mentioned file can accessed by setting the wp_automatic GET parameter (thru the to $wp->query_vars) to "download" as stated in the wp_automatic_parse_request function:

function wp_automatic_parse_request($wp) {

	//secret word 
	$wp_automatic_secret = wp_automatic_trim(get_option('wp_automatic_cron_secret'));
	if(wp_automatic_trim($wp_automatic_secret) == '') $wp_automatic_secret = 'cron';
	
	// only process requests with "my-plugin=ajax-handler"
	if (array_key_exists('wp_automatic', $wp->query_vars)) {
			
		if($wp->query_vars['wp_automatic'] == $wp_automatic_secret){
			require_once(dirname(__FILE__) . '/cron.php');
			exit;

		}elseif ($wp->query_vars['wp_automatic'] == 'download'){
			require_once 'downloader.php';
			exit;
		}elseif ($wp->query_vars['wp_automatic'] == 'test'){
			require_once 'test.php';
			exit;
		}elseif($wp->query_vars['wp_automatic'] == 'show_ip'){
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_HEADER,0);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
			curl_setopt($ch, CURLOPT_TIMEOUT,20);
			curl_setopt($ch, CURLOPT_REFERER, 'http://www.bing.com/');
			curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8');
			curl_setopt($ch, CURLOPT_MAXREDIRS, 5); // Good leeway for redirections.
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // Many login forms redirect at least once.
			
			//curl get
			$x='error';
			$url='https://www.showmyip.com/';
			
			curl_setopt($ch, CURLOPT_HTTPGET, 0);
			curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($url));
			
			$exec=curl_exec($ch);
			$x=curl_error($ch);
			
			//<h2 id="ipv4">41.176.183.83</h2>
			if(strpos($exec,'<h2 id="ipv4">')){
				preg_match('{<h2 id="ipv4">(.*?)</h2>}', $exec , $ip_matches);
				print_r($ip_matches[1]);
			}else{
			
				echo $exec.$x;
			}
			exit;
		 
		
		}
	}
}
add_action('parse_request', 'wp_automatic_parse_request');

Back to the downloader.php file, we are able to supply an arbitrary URL or even local files on $_GET['link'] parameter and it will be fetched using cURL.

The patch

For the Unauthenticated Arbitrary SQL Execution vulnerability, the vendor decided to remove entirely the inc/csv.php file. For the Unauthenticated Arbitrary File Download and SSRF, the vendor decided to apply a nonce check (whereas the nonce value could only be fetched from a privileged user) and apply a check on the $link variable.

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for SQL query execution and URL fetch. In the context of SQL query execution, we recommend developers to not provide a full-scale SQL query execution feature even for a high-privilege user such as an Administrator user. For the URL fetch process, we recommend applying permission and nonce check on the action and applying some checks and limitations to the supplied URL. For the best security practice, we recommend to use wp_safe_remote_* function to fetch the supplied URL by the user.

Timeline

25 February, 2024We found the vulnerability and reached out to the plugin vendor.
27 February, 2024Automatic version 3.92.1 released to patch the reported issues.
13 March, 2024Added the vulnerabilities to the Patchstack vulnerability database. Deployed vPatch rule to protect users.
19 March , 2024Security advisory article publicly released.

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.

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu