Affected plugin | miniOrange |
Active installs | 20,000+ |
Vulnerable version | <= 5.5.82 |
Audited version | 5.5.82 |
Fully patched version | – |
Recommended remediation | Removal of the plugin |
Description
The plugin uses remote APIs in almost all authentication-related contexts. In addition, the plugin authenticates itself using information stored exclusively as plaintext in the database.
An attacker, armed with a read-only SQLi, can trivially gain access to the API credentials and use them to take over the site in many ways.
Proof of concept
The plugin uses the Mo2f_Api class to communicate with its remote authentication server.
It uses HTTP basic authentication using HTTP headers, which are provided by calling the Mo2f_Api::get_http_header_array method.
function get_http_header_array() {
$customerKey = get_option( 'mo2f_customerKey' );
$apiKey = get_option( 'mo2f_api_key' );
/* Current time in milliseconds since midnight, January 1, 1970 UTC. */
$currentTimeInMillis = Mo2f_Api::get_timestamp();
/* Creating the Hash using SHA-512 algorithm */
$stringToHash = $customerKey . $currentTimeInMillis . $apiKey;;
$hashValue = hash( "sha512", $stringToHash );
$headers = array(
"Content-Type" => "application/json",
"Customer-Key" => $customerKey,
"Timestamp" => $currentTimeInMillis,
"Authorization" => $hashValue
);
return $headers;
}
An attacker can obtain both “mo2fa_customerKey” and “mo2f_api_key”
by running the following SQL query through his read-only SQLi:
SELECT option_name, option_value FROM wp_options WHERE option_name = 'mo2f_customerKey' OR option_name = 'mo2f_api_key'
There are dozens of attacker vectors from there on, a couple of examples include:
- Creating new (miniOrange) users.
- Changing authentication methods for users.
- Updating emergency codes for users.
- Fetching all available user info that miniOranage has about users.
Proposed patch
1. Use API tokens for authentication and store the API tokens encrypted.
2. Implement a secure storage mechanism for retrieving the encryption key.
In WordPress, the options are, from most to least preferred:
- Reading the encryption key from a file whose name is passed as an environment variable. (Plays great with docker secrets).
- Reading the encryption key from an environment variable.
- Reading the encryption key from a constant defined in the wp-config.php file.
A sample implementation could look like this:
final class Secrets {
public static function get(string $secret_name) :string
{
$contents = @file_get_contents($secret_name);
if(is_string($contents)){
return $contents;
}
if(isset($_SERVER[$secret_name])) {
return $_SERVER[$secret_name];
}
$value = @constant($secret_name);
if(!$value) {
throw BrokenEnvironmentException::forMissingSecret($secret_name);
}
return $value;
}
}
}
Timeline
Vendor contacted | September 12, 2022 |
First Response | September 16, 2022 |
Fully patched at | – |
Publicly disclosed | April 24, 2023 |
Leave a Reply