Blog: We Wanted to Talk About Cyberattacks During the Olympics, but We Have Nothing to Say
Blog: From a warning to a CSRF impacting over 3 million sites
Author: Nicolas D.
Published on
WordPress is a fascinating piece of technology. It's first version was released in 2003, more than 20 years ago, and it has made its way up to being the most used Content Management System (CMS). It is used by no more than around 43% of all websites in the world.
At Patrowl, we are doing automated pentesting at large scale with almost 18.000 assets being pentested and more than 200.000 assets in passive recon and what we see confirms these statistics: WordPress is used everywhere! With corporate websites, blogs or e-commerce platforms, WordPress is known for being usable in almost any circumstance. And this is only possible thanks to its famous plugin system.
WordPress is made so that any plugin can interact with absolutely everything using components such as actions, filters or blocks. Because of these, a plugin can change the way WordPress looks, how it behaves and, by the way, it can actually significantly reduce its security level.
Looking for targets
As pentesters, we love to look at WordPress sites and check the security of plugins installed. The list of installed plugins is easy to find thanks to our unique solution that scans all assets and returns us with vulnerabilities but also information about technologies being used.
Recently, we received an alert regarding a PHP warning on a WordPress site:
Having such warning is a good hint that there might be interesting things to find. So we have a target: UpdraftPlus with 3+ million active installations.
CVE-2023-5982 - UpdraftPlus <= 1.23.10 - Cross-Site Request Forgery to Google Drive Storage Update
Description
The UpdraftPlus plugin helps users to manage backups for WordPress. It is a widely used plugin with more than 3 million installations. To manage backups, it is possible to link the plugin with a Google account to automatically send backups to a Drive directory.
While reviewing the security of this plugin, Patrowl found that it is vulnerable to a CSRF attack that could enable an attacker to setup its own Google Drive account as remote storage. Any future WordPress backup would then be sent in the attacker's Google Drive directory, leading to a leak of sensitive data and a possible complete takeover of the WordPress instance. Indeed, a backup can contain all files of the WordPress installation, all settings and the database content.
Code source analysis
During the plugin initialisation, the handle_url_actions
function is associated with the init
action:
// class-updraftplus.php L.126
add_action('init', array($this, 'handle_url_actions'));
This function gets the value of the action
query string and matches it against a regex to ensure it follows this schema:
updraftmethod-[method_name]-[call_method]
It ensures that the [method_name]
method is defined and that the user is authorized to manage the UpdraftPlus plugin.
This last check efficiently prevents an anonymous user from executing the next instructions of the function.
However, no protection against Cross-Site Request Forgery (CSRF) attacks is implemented in the handle_url_actions
function.
This kind of attack involves tricking a privileged authenticated user into visiting a malicious website. This website will send a request on behalf the user.
Well, being possible to exploit a CSRF attack on this endpoint is interesting but let's dive deeper and see what can be done with this endpoint.
After a few checks, the plugin will try to call the action_[call_method]
method of the class located in /methods/[method_name].php
.
There are quite a few methods available, but one of them caught our attention: /methods/googledrive.php
.
It contains the following action_auth
function:
// /methods/googledrive.php L.31
public function action_auth() {
if (isset($_GET['state'])) {
$parts = explode(':', $_GET['state']);
$state = $parts[0];
if ('success' == $state) {
if (isset($_GET['user_id']) && isset($_GET['access_token'])) {
$code = array(
'user_id' => $_GET['user_id'],
'access_token' => $_GET['access_token']
);
} else {
$code = array();
}
$this->do_complete_authentication($state, $code);
} elseif ('token' == $state) {
$this->gdrive_auth_token();
} elseif ('revoke' == $state) {
$this->gdrive_auth_revoke();
}
} elseif (isset($_GET['updraftplus_googledriveauth'])) {
if ('doit' == $_GET['updraftplus_googledriveauth']) {
$this->action_authenticate_storage();
} elseif ('deauth' == $_GET['updraftplus_googledriveauth']) {
$this->action_deauthenticate_storage();
}
}
}
If the states
, user_id
and access_token
GET parameters are defined, the do_complete_authentication
method will be called with the following value:
state = "success:[instance_id]"
code = array('user_id': [user_id], 'access_token': [access_token])
Here is the code of this function:
// methods/googledrive.php L.448
public function do_complete_authentication($state, $code, $return_instead_of_echo = false) {
// If these are set then this is a request from our master app and the auth server has returned these to be saved.
if (isset($code['user_id']) && isset($code['access_token'])) {
$opts = $this->get_options();
$opts['user_id'] = base64_decode($code['user_id']);
$opts['tmp_access_token'] = base64_decode($code['access_token']);
// Unset this value if it is set as this is a fresh auth we will set this value in the next step
if (isset($opts['expires_in'])) unset($opts['expires_in']);
// remove our flag so we know this authentication is complete
if (isset($opts['auth_in_progress'])) unset($opts['auth_in_progress']);
$this->set_options($opts, true);
}
if ($return_instead_of_echo) {
return $this->show_authed_admin_success($return_instead_of_echo);
} else {
add_action('all_admin_notices', array($this, 'show_authed_admin_success'));
}
}
This function simply saves the user_id
and access_token
provided by the user as options for the plugin.
These values will then be used each time a backup is performed to save data in the Drive associated with the provided credentials.
An important flaw in this function is that its first argument, the $state
variable, is never used and no verification is performed on its value.
Indeed, the state variable is expected to have the following format: success:[instance_id]
where instance_id
is a random value identifying the plugin instance.
This value is not known by an anonymous attacker and if it were checked, an attacker wouldn't be able to perform a CSRF attack.
Exploitation
Well, being able to manipulate some settings of the plugin using a CSRF attack seems interesting, but how is it really exploitable and what can we do using this attack?
After the options are set by the do_complete_authentication
function, the plugin uses this information to authenticate on the Google account and, if the plugin is configured to use Google Drive as a remote storage for the backups, the next backups will be saved on the associated Google Drive directory.
Therefore, to exploit this vulnerability, an attacker should first generate a valid Google Drive access token before sending it to the victim.
The easiest way to generate a valid token is to deploy the UpraftPlus plugin on a local instance of WordPress, go to the settings of the plugin and activate the Google Drive remote storage, save settings and perform the authentication with a valid Google account.
At the end of the process, a redirection is performed on auth.updraftplus.com
containing a link to the local instance of WordPress:
https://[domain]/wp-admin/options-general.php?action=updraftmethod-googledrive-auth&state=success%3A[instance_id]&access_token=[access_token]&user_id=[user_id]
To exploit the CSRF vulnerability, just change the domain name in this link and send it to an authenticated administrator on the targeted WordPress site.
If the attack is successful, the attacker will receive backups in its own Google Drive account, potentially including database and settings backups, depending on the plugin configuration.
Remediation
Here is the fix implemented in the handle_url_actions
function by the vendor:
// If we don't have an instance_id but the state is set then we are coming back to finish the auth and should extract the instance_id from the state
if ('' == $instance_id && isset($state) && false !== strpos($state, ':')) {
$parts = explode(':', $state);
$instance_id = $parts[1];
}
if (!preg_match('/^[-A-Z0-9]+$/i', $instance_id)) die('Invalid input.');
if (empty($storage_objects_and_ids[$method]['instance_settings'][$instance_id])) {
error_log("UpdraftPlus::handle_url_actions(): no such instance ID found in settings.");
return;
}
If a state
variable is provided by the user, the instance_id
it contains is compared to the current plugin instance id.
Because the instance_id
value is not known by an attacker, it effectively prevent CSRF attacks.
Conclusion
Is the vulnerability easily exploitable? Not really, as for almost any CSRF attack, even if an attack on a plugin with 3+ million active installations may be doable. But this finding shows that developing a secured WordPress plugin is difficult. Even if permissions are checked, developers should ensure no CSRF attack is possible. And if such protection is implemented, maybe an attacker could leak the CSRF token through a verbose entry point?
At Patrowl, we love automated and continuous pentest. Our unique scanning and testing automation guides us into manual searching to ensure we only return real vulnerabilities to our customers. We will continue to look for new vulnerabilities based on findings we get on our clients' assets, so stay tuned for future posts!
Patrowl Raises €11m in Series A Funding: Continuous Protection of Internet Exposed Assets
Blog: RegreSSHion, critical vulnerability on OpenSSH CVE-2024-6387