Talos Vulnerability Report

TALOS-2023-1896

WWBN AVideo userRecoverPass.php recoverPass generation insufficient entropy vulnerability

January 10, 2024
CVE Number

CVE-2023-49589

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

AVideo - https://github.com/WWBN/AVideo

CVSSv3 SCORE

8.8 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-640 - Weak Password Recovery Mechanism for Forgotten Password

DETAILS

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.

TIMELINE

2023-12-14 - Vendor Disclosure
2023-12-15 - Vendor Patch Release
2024-01-10 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.