CVE-2022-32777,CVE-2022-32778
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.
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
AVideo - https://github.com/WWBN/AVideo
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CWE-732 - Incorrect Permission Assignment for Critical Resource
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 confirms issues fixed on July 7th 2022
2022-06-29 - Initial Vendor Contact
2022-07-05 - Vendor Disclosure
2022-07-07 - Vendor Patch Release
2022-08-16 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.