Talos Vulnerability Report

TALOS-2023-1886

WWBN AVideo getLanguageFromBrowser local file inclusion vulnerability

January 10, 2024
CVE Number

CVE-2023-47862

SUMMARY

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.

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

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

CWE

CWE-73 - External Control of File Name or Path

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’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.

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.