CVE-2023-47862
A local file inclusion vulnerability exists in the getLanguageFromBrowser functionality of WWBN AVideo dev master commit 15fed957fb. A specially crafted HTTP request can lead to arbitrary code execution. An attacker can send a series of HTTP requests 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
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-73 - External Control of File Name or Path
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’s user interface supports multiple languages. This functionality is implemented via setLanguage()
, which includes one of the locale files from the locale/
directory, depending on the language in use:
function setLanguage($lang) {
[2] $lang = strip_tags($lang);
if (empty($lang)) {
return false;
}
global $global;
$lang = flag2Lang($lang);
if (empty($lang) || $lang === '-') {
return false;
}
[1] $file = "{$global['systemRootPath']}locale/{$lang}.php";
_session_start();
if (file_exists($file)) {
$_SESSION['language'] = $lang;
[3] include_once $file;
return true;
...
At [1] the $file
path locale/{$lang}.php
is built based on the $lang
parameter. Note that only tags are stripped from $lang
[2]. At [3] $file
is executed.
Clearly, if an attacker is able to control $lang
arbitrarily, it would allow them to include arbitrary .php
files from the local filesystem.
Let’s see a straightforward path to reach this function, while simply loading the main page of AVideo:
/var/www/html/AVideo/view/index.php(7): require_once(\'...\')
/var/www/html/AVideo/videos/configuration.php(47): require_once(\'...\')
/var/www/html/AVideo/objects/include_config.php(146): require_once(\'...\')
/var/www/html/AVideo/objects/configuration.php(7): require_once(\'...\')
/var/www/html/AVideo/objects/user.php(14): require_once(\'...\')
/var/www/html/AVideo/plugin/Plugin.abstract.php(4): require_once(\'...\')
/var/www/html/AVideo/locale/function.php(9): includeLangFile()
/var/www/html/AVideo/locale/function.php(13): setSiteLang()
/var/www/html/AVideo/locale/function.php(159): User_Location::changeLang()
/var/www/html/AVideo/plugin/User_Location/User_Location.php(92): setLanguage()
The first interesting locale-related line is in plugin/Plugin.abstract.php
, which simply includes locale/function.php
:
require_once $global['systemRootPath'] . 'locale/function.php';
This in turn executes this inside locale/function.php
:
includeLangFile();
function includeLangFile() {
global $t, $global;
setSiteLang();
@include_once "{$global['systemRootPath']}locale/{$_SESSION['language']}.php";
}
...
function setSiteLang() {
global $config, $global;
if (empty($global['systemRootPath'])) {
if (function_exists('getLanguageFromBrowser')) {
setLanguage(getLanguageFromBrowser());
} else {
setLanguage('en_US');
}
} else {
require_once $global['systemRootPath'] . 'plugin/AVideoPlugin.php';
$userLocation = false;
$obj = AVideoPlugin::getDataObjectIfEnabled('User_Location');
$userLocation = !empty($obj) && !empty($obj->autoChangeLanguage);
if (!empty($_GET['lang'])) {
_session_start();
setLanguage($_GET['lang']);
} else if ($userLocation) {
[4] User_Location::changeLang();
}
If there’s no lang
specified via GET
, User_Location::changeLang()
is called:
static function changeLang($force = false) {
global $global;
_session_start();
if (!empty($force) || empty($_SESSION['language'])) {
$obj = AVideoPlugin::getDataObject('User_Location');
if ($obj->autoChangeLanguage) {
[5] $lang = self::getLanguage();
if (!empty($lang)) {
if (!empty($_REQUEST['debug'])) {
_error_log("changeLang line=" . __LINE__ . " " . json_encode(debug_backtrace()));
}
[6] setLanguage($lang);
...
If language
is not already set in session (this is true for any request that doesn’t send a sessid cookie), setLanguage()
is called [6] using $lang
retrieved from getLanguage()
:
static function getLanguage() {
global $global;
$global['User_Location_lang'] = false;
if (empty($global['User_Location_lang'])) {
$obj = AVideoPlugin::getDataObject('User_Location');
if ($obj->useLanguageFrom->value == 'browser') {
[7] $global['User_Location_lang'] = getLanguageFromBrowser();
} else {
$User_Location = self::getThisUserLocation();
$global['User_Location_lang'] = $User_Location['country_code'];
}
}
return $global['User_Location_lang'];
}
Eventually, getLanguageFromBrowser()
[7] is used to retrieve the language, since $obj->useLanguageFrom->value
has value “browser” by default (set by getEmptyDataObject()
):
function getLanguageFromBrowser()
{
if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return false;
}
[8] $parts = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
return str_replace('-', '_', $parts[0]);
}
getLanguageFromBrowser()
simply returns the language set by the Accept-Language
HTTP header [8], as set by the client making the request.
In summary, setLanguage()
can be called with an arbitrary $lang
parameter, which leads to executing arbitrary PHP files within the webserver’s filesystem.
This can be used by an attacker to include malicious PHP files. For example, using TALOS-2023-1885, it is possible to upload a malicious .php
file in /tmp
and execute it, leading to arbitrary code execution.
2023-12-14 - Vendor Disclosure
2023-12-15 - Vendor Patch Release
2024-01-10 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.