Security researcher Marc Montpas has discovered a vulnerability in UpdraftPlus, a WordPress plugin with over 3 million installations.

This vulnerability allows any logged-in user, including subscriber-level users, to download backups made with the plugin. Backups are a treasure trove of sensitive information, and frequently include configuration files that can be used to access the site database as well as the contents of the database itself.

The attack starts with the WordPress heartbeat function. The attacker needs to send a specially crafted heartbeat request containing a data[updraftplus] parameter. By supplying the appropriate subparameters, an attacker is able to obtain a backup log containing a backup nonce and timestamp which they can then use to download a backup.

Once the attacker has the backup nonce, they can trigger the maybe_download_backup_from_email function, but in order to do so successfully they’d need to fool a WordPress feature designed to determine the endpoint the request is being sent to:

Buy Me A Coffee
public function maybe_download_backup_from_email() {
    global $pagenow;
    if ((!defined('DOING_AJAX') || !DOING_AJAX) && UpdraftPlus_Options::admin_page() === $pagenow && isset($_REQUEST['page']) && 'updraftplus' === $_REQUEST['page'] && isset($_REQUEST['action']) && 'updraft_download_backup' === $_REQUEST['action']) {
        $findexes = empty($_REQUEST['findex']) ? array(0) : $_REQUEST['findex'];
        $timestamp = empty($_REQUEST['timestamp']) ? '' : $_REQUEST['timestamp'];
        $nonce = empty($_REQUEST['nonce']) ? '' : $_REQUEST['nonce'];
        $type = empty($_REQUEST['type']) ? '' : $_REQUEST['type'];
        if (empty($timestamp) || empty($nonce) || empty($type)) wp_die(__('The download link is broken, you may have clicked the link from untrusted source', 'updraftplus'), '', array('back_link' => true));
        $backup_history = UpdraftPlus_Backup_History::get_history();
        if (!isset($backup_history[$timestamp]['nonce']) || $backup_history[$timestamp]['nonce'] !== $nonce) wp_die(__("The download link is broken or the backup file is no longer available", 'updraftplus'), '', array('back_link' => true));
        $this->do_updraft_download_backup($findexes, $type, $timestamp, 2, false, '');
        exit; // we don't need anything else but an exit

The issue is the UpdraftPlus_Options::admin_page() === $pagenow check. This requires that the WordPress $pagenow global variable be set to options-general.php. Subscribers are typically not allowed to access this page. However, it is possible to spoof this variable on some server configurations, primarily Apache/modPHP. Similar to a previous vulnerability in WordPress < 5.5.1 also found by this researcher, it’s possible to send a request to e.g. wp-admin/admin-post.php/%0A/wp-admin/options-general.php?page=updraftplus.

While subscribers cannot access options-general.php, they are allowed to access admin-post.php. By sending the request to this endpoint they can fool the $pagenow check into thinking that the request is to options-general.php, while WordPress still sees the request as being to an allowed endpoint of admin-post.php.

Once this check has been passed, the attacker will need to provide the backup nonce as well as a type parameter. Finally, as all backups are indexed by timestamp, the attacker will need to add a timestamp that is either bruteforced or obtained from the backup log obtained earlier.

Ransomware Cripples London Hospitals, Cancels 800+ Surgeries in a Week

As such we urge all users running the UpdraftPlus plugin to update to the latest version of the plugin, which is version 1.22.3 as of this writing, as soon as possible, if you have not already done so, since the consequences of a successful exploit would be severe.