CVE-2023-49589
An insufficient entropy vulnerability exists in the userRecoverPass.php recoverPass generation functionality of WWBN AVideo dev master commit 15fed957fb. A specially crafted HTTP request can lead to an arbitrary user password recovery. An attacker can send an HTTP request to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
WWBN AVideo dev master commit 15fed957fb
AVideo - https://github.com/WWBN/AVideo
8.8 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE-640 - Weak Password Recovery Mechanism for Forgotten Password
AVideo is a web application, mostly written in PHP, that can be used to create an audio/video sharing website. It allows users to import videos from various sources, encode and share them in various ways. Users can sign up to the website in order to share videos, while viewers have anonymous access to the publicly-available contents. The platform provides plugins for features like live streaming, skins, YouTube uploads and more.
AVideo allows users to recover their account access when they forget their password. This functionality is implemented by objects/userRecoverPass.php
:
...
$user = new User(0, $_REQUEST['user'], false);
[1] if (!(!empty($_REQUEST['user']) && !empty($_REQUEST['recoverpass']))) {
$obj = new stdClass();
$obj->user = $_REQUEST['user'];
[2] $obj->captcha = $_REQUEST['captcha'];
[2] $obj->reloadCaptcha = false;
$obj->session_id = session_id();
header('Content-Type: application/json');
[3] if(empty($user->getStatus())){
$obj->error = __("User not found");
die(json_encode($obj));
}
[3] if($user->getStatus() !== 'a'){
$obj->error = __("The user is not active");
die(json_encode($obj));
}
if (!empty($user->getEmail())) {
[4] $recoverPass = $user->setRecoverPass();
if (empty($_REQUEST['captcha'])) {
$obj->error = __("Captcha is empty");
} else {
if ($user->save()) {
require_once 'captcha.php';
[5] $valid = Captcha::validation($_REQUEST['captcha']);
if ($valid) {
//Create a new PHPMailer instance
$mail = new \PHPMailer\PHPMailer\PHPMailer();
setSiteSendMessage($mail);
//Set who the message is to be sent from
$mail->setFrom($config->getContactEmail(), $config->getWebSiteTitle());
//Set who the message is to be sent to
$mail->addAddress($user->getEmail());
//Set the subject line
$mail->Subject = __('Recover Pass from') .' '. $config->getWebSiteTitle();
$msg = __("You asked for a recover link, click on the provided link") . " <a href='{$global['webSiteRootURL']}recoverPass?user={$_REQUEST['user']}&recoverpass={$recoverPass}'>" . __("Reset password") . "</a>";
$mail->msgHTML($msg);
//send the message, check for errors
[6] if (!$mail->send()) {
$obj->error = __("Message could not be sent") . " " . $mail->ErrorInfo;
} else {
...
The purpose of the code block above is to create a recovery code and send it as a link to the user’s email address. Once the user receives the link, they can click on it so they’ll reach an interface where they can set a new password for their account.
The code block can be entered when user
is set but recoverpass
is not [1]. Then, user
and captcha
parameters are retrieved from the request [2].
If the user is valid [3], a recovery code ($recoveryPass
) is generated via setRecoverPass()
[4]. Finally, the captcha is checked [5] and an email containing the recovery link is sent to the user [6].
Let’s see how setRecoverPass()
is implemented:
public function setRecoverPass($forceChange = false)
{
// let the same recover pass if it was 10 minutes ago
[7] if (!$this->isRecoverPassExpired($this->recoverPass) && empty($forceChange) && !empty($this->recoverPass) && !empty($recoverPass) && !empty($this->modified) && strtotime($this->modified) > strtotime("-10 minutes")) {
return $this->recoverPass;
}
[8] $this->recoverPass = $this->createRecoverPass();
return $this->recoverPass;
}
private function createRecoverPass($secondsValid = 600)
{
$json = new stdClass();
[9] $json->valid = strtotime("+{$secondsValid} seconds");
[10] return encryptString(json_encode($json));
}
At [7] the function makes sure that codes are not regenerated in intervals shorter than 10 minutes. This is, however, irrelevant to the current issue.
At [8] the recoverPass
field is set via createRecoverPass()
, which calls encryptString()
[10] with a JSON object containing the current time plus 600 seconds [9]. encryptString()
is a generic function that encrypts strings and does not contain any random components except for the salt
, which is created at setup time and is unknown to an attacker, hence the encrypted strings can’t be decrypted without it.
However, even without being able to decrypt strings, an attacker can have AVideo generate two codes in parallel for two different users. Since the plaintext content of the recovery code is simply the current time plus 600, two requests happening within the same second will have an identical encrypted string as output.
An attacker with basic user privileges can exploit this issue by requesting a recovery code for their own user and for the admin user at the same time. In the email, the attacker will receive a link like:
You asked for a recover link, click on the provided link
<a href='https://localhost/recoverPass?user=attacker&recoverpass=123456'>Reset password</a>
The attacker can simply follow the link after replacing user=attacker
with user=admin
, and they’ll be presented with a page to reset the admin’s password.
Of course, the admin will receive a recovery link as well. However, TALOS-2023-1897 can be used to prevent an email from being sent to the admin, so exploitation becomes less evident.
2023-12-14 - Vendor Disclosure
2023-12-15 - Vendor Patch Release
2024-01-10 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.