Talos Vulnerability Report

TALOS-2022-1542

WWBN AVideo cookie information disclosure vulnerability

August 16, 2022
CVE Number

CVE-2022-32777,CVE-2022-32778

SUMMARY

An information disclosure vulnerability exists in the cookie functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. The session cookie and the pass cookie miss the HttpOnly flag, making them accessible via JavaScript. The session cookie also misses the secure flag, which allows the session cookie to be leaked over non-HTTPS connections. This could allow an attacker to steal the session cookie via crafted HTTP requests.

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 11.6
WWBN AVideo dev master commit 3f7c0364

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-732 - Incorrect Permission Assignment for Critical Resource

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 uses cookies to keep track of users sessions. There are two separate but similar issues that are due to missing flags during cookie creation.

A session cookie is used to keep track of user sessions. The name of the session cookie is renamed depending on the installation path, as we can see in objects/include_config.php (which is included in every request):

$global['session_name'] = md5($global['systemRootPath']);
session_name($global['session_name']);

In our setup, the session name is 84b11d010cced71edffee7aa62c4eda0:

$ echo -en /var/www/html/AVideo/|md5sum
84b11d010cced71edffee7aa62c4eda0

In that same file, session_start() is called to start a session:

if (empty($doNotStartSessionbaseIncludeConfig)) {
    $config = new Configuration();
    session_write_close();

    // server should keep session data for AT LEAST 1 hour
    ini_set('session.gc_maxlifetime', $config->getSession_timeout());

    // each client should remember their session id for EXACTLY 1 hour
    session_set_cookie_params($config->getSession_timeout());

    //Fix “set SameSite cookie to none” warning
    if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
        setcookie('key', 'value', ['samesite' => 'None', 'secure' => true]);
    } else {
        header('Set-Cookie: cross-site-cookie=name; SameSite=None; Secure');
        setcookie('key', 'value', time() + $config->getSession_timeout(), '/; SameSite=None; Secure');
    }

    session_start();
}

We can see that some part of the configuration is changed at runtime via ini_set. However, two important parameter configurations are missing for the session cookie: session.cookie_secure and session.cookie_httponly. This means that the cookie will get the php.ini values, which, unless set manually, has a default of 0 in PHP.

We can see the secure and HttpOnly flags are missing by doing a simple request:

$ curl -kv https://192.168.1.200/user 2>&1 | grep 84b11d010cced71edffee7aa62c4eda0
< Set-Cookie: 84b11d010cced71edffee7aa62c4eda0=5s2kdm4sgpte26q0sh5uac3hgd; expires=Fri, 03-Jun-2022 17:44:40 GMT; Max-Age=3600; path=/

This issue allows the session cookie to be leaked on non-HTTPS requests (for cookie_secure), and it also allows the cookie to be read via Javascript (for cookie_httponly), for example as a secondary step during an XSS exploitation. This cookie can then be used, in the worst case, to login to the website as administrator.

The pass cookie is set in objects/user.php during login:

class User {
    ...
    public function login($noPass = false, $encodedPass = false, $ignoreEmailVerification = false) {
        ...
        _setcookie("pass", $user['password'], $expires);

_setcookie is defined in objects/functions.php:

function _setcookie($cookieName, $value, $expires = 0) {
    if (empty($expires)) {
        if (empty($config) || !is_object($config)) {
            $config = new Configuration();
        }
        $expires = time() + $config->getSession_timeout();
    }

    if (version_compare(phpversion(), '7.3', '>=')) {
        $cookie_options = [
            'expires' => $expires,
            'path' => '/',
            'domain' => getDomain(),
            'secure' => true,
            'httponly' => false,         // [1]
            'samesite' => 'None'
        ];
        return setcookie($cookieName, $value, $cookie_options);
    } else {
        return setcookie($cookieName, $value, (int) $expires, "/", getDomain());
    }
}

At [1] we can see the code explicitly sets ‘httponly’ to false, which will allow Javascript to access the pass value.

The content of the pass cookie is a hash of the password value:

// in objects/user.php
public function setPassword($password, $doNotEncrypt = false) {
    if (!empty($password)) {
        if ($doNotEncrypt) {
            $this->password = ($password);
        } else {
            $this->password = encryptPassword($password);
        }
    }
}

// in objects/functions.php
function encryptPassword($password, $noSalt = false) {
    global $advancedCustom, $global, $advancedCustomUser;
    if (!empty($advancedCustomUser->encryptPasswordsWithSalt) && !empty($global['salt']) && empty($noSalt)) {
        $password .= $global['salt']; // [2]
    }

    return md5(hash("whirlpool", sha1($password)));
}

The salt that is used at [2] is fixed for the whole installation, and is set in configuration.php. This means that the lifetime of a hashed password is as long as the password itself (unless the salt is changed and all passwords get updated).

Moreover, the pass value, even if hashed, can be used to login without possessing the password itself, because of the function used to verify the password, defined in objects/functions.php:

function encryptPasswordVerify($password, $hash, $encodedPass = false) {
    global $advancedCustom, $global;
    if (!$encodedPass || $encodedPass === 'false') {
        //_error_log("encryptPasswordVerify: encrypt");
        $passwordSalted = encryptPassword($password);
        // in case you enable the salt later
        $passwordUnSalted = encryptPassword($password, true);
    } else {
        //_error_log("encryptPasswordVerify: do not encrypt");
        $passwordSalted = $password;
        // in case you enable the salt later
        $passwordUnSalted = $password;
    }
    //_error_log("passwordSalted = $passwordSalted,  hash=$hash, passwordUnSalted=$passwordUnSalted");
    return $passwordSalted === $hash || $passwordUnSalted === $hash || $password === $hash;  // [3]
}

At [3] we can see that a valid password is the password itself, its salted hash and also its unsalted hash. So, having this cookie set without the HttpOnly flag allows any XSS attack to retrieve a hash which is equivalent to a password, and, in the worst case, can be used to login to the website as administrator.

We can see the secure flag is missing by doing a simple request:

$ curl -kv 'https://192.168.1.200/objects/login.json.php' \
  -X POST --data-raw 'user=user1&pass=user123&rememberme=false' 2>&1 | grep "Set-Cookie: pass"
< Set-Cookie: pass=9e0f453febd664afe850cddb516a8415; expires=Fri, 03-Jun-2022 21:21:01 GMT; Max-Age=3600; path=/; domain=192.168.1.200; secure; SameSite=None
VENDOR RESPONSE

Vendor confirms issues fixed on July 7th 2022

TIMELINE

2022-06-29 - Initial Vendor Contact
2022-07-05 - Vendor Disclosure
2022-07-07 - Vendor Patch Release
2022-08-16 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.