CVE-2023-49864,CVE-2023-49863,CVE-2023-49862
An information disclosure vulnerability exists in the aVideoEncoderReceiveImage.json.php image upload functionality of WWBN AVideo dev master commit 15fed957fb. A specially crafted HTTP request can lead to arbitrary file read.
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
6.5 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
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.
The objects/aVideoEncoderReceiveImage.json.php
file can be used to associate images and videos. This functionality does not need special configuration to be used. However, the user performing the request needs permission to upload videos.
A user can add several kinds of images: jpg
, jgpSpectrum
, gif
, webp
. All these fields are vulnerable. Let’s take the jpg
one to explain the issue.
...
[1] $obj->video_id = $_REQUEST['videos_id'];
$videoFileName = $video->getFilename();
$paths = Video::getPaths($videoFileName, true);
$destination_local = "{$paths['path']}{$videoFileName}";
make_path($destination_local);
...
$obj->jpgDest = "{$destination_local}.jpg";
[2] if (!file_exists($obj->jpgDest) || !fileIsAnValidImage($obj->jpgDest)) {
[3] if (isValidURL($_REQUEST['downloadURL_image'])) {
[4] $content = url_get_contents($_REQUEST['downloadURL_image']);
$obj->jpgDestSize = _file_put_contents($obj->jpgDest, $content);
_error_log("ReceiveImage: download {$_REQUEST['downloadURL_image']} to {$obj->jpgDest} " . humanFileSize($obj->jpgDestSize));
Based on the requested videos_id
, a destination image file path is built. If such image does not exist or is invalid [2], the downloadURL_image
is considered to fill the image contents. downloadURL_image
has to be a valid URL (checked using isValidURL()
[3]), and only if successfull is the image retrieved [4] and later associated with the video object.
function isValidURL($url)
{
if (empty($url) || !is_string($url)) {
return false;
}
if (preg_match("/^http.*/", $url) && filter_var($url, FILTER_VALIDATE_URL)) {
return true;
}
return false;
}
isValidURL
is rather simple, returning true when both these conditions are true:
FILTER_VALIDATE_URL
checkBasically, $url
needs to be a valid URL starting with “http”, which means schemas like file
won’t work.
As previously mentioned, url_get_contents()
is called after the checks above pass:
function url_get_contents($url, $ctx = "", $timeout = 0, $debug = false, $mantainSession = false)
{
global $global, $mysqlHost, $mysqlUser, $mysqlPass, $mysqlDatabase, $mysqlPort;
if (!isValidURLOrPath($url)) {
_error_log('url_get_contents Cannot download ' . $url);
return false;
}
if ($debug) {
_error_log("url_get_contents: Start $url, $ctx, $timeout " . getSelfURI() . " " . getRealIpAddr() . " " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
}
[5] $response = try_get_contents_from_local($url);
if (!empty($response)) {
return $response;
}
...
Recall that $url
is fully controlled by the attacker. It is eventually passed to try_get_contents_from_local()
.
function try_get_contents_from_local($url)
{
[6] if (substr($url, 0, 1) === '/') {
// it is not a URL
return file_get_contents($url);
}
global $global;
[7] $parts = explode('/videos/', $url);
if (!empty($parts[1])) {
if (preg_match('/cache\//', $parts[1])) {
$encoder = '';
} else {
$encoder = 'Encoder/';
}
[8] $tryFile = "{$global['systemRootPath']}{$encoder}videos/{$parts[1]}";
if (file_exists($tryFile)) {
[9] return file_get_contents($tryFile);
}
}
return false;
}
The check at [6] can’t be reached because our $url
has to start with http
.
However, if $url
contains /videos/
[7], then the path on the right of it is extracted and used to build a local file path inside the videos
directory. Note that anything on the left side of /videos/
(for example the hostname) is ignored.
If the file exists, it is read and its contents are returned [9] and eventually stored as an image associated with the video, which can be later retrieved with a simple HTTP request.
An attacker can exploit this issue to read any file in the system, by specifying an image URL such as http://123/videos/../../../../../../../etc/passwd
, possibly also leading to privilege escalation.
Moreover, this issue allows an attacker to read configuration.php
, which contains the salt
used for various encryptions made by AVideo. Knowledge of the salt can be used to achieve administrator privileges, as shown in TALOS-2023-1900. For this specific case, the image URL should contain cache/
, which allows for retrieving files directly in the videos
directory. In practice, the image URL to read configuration.php
would be http://123/videos/cache/../configuration.php
.
Via the downloadURL_gifimage
parameter, an attacker can specify an arbitrary file path to read from the webserver.
Via the downloadURL_webpimage
parameter, an attacker can specify an arbitrary file path to read from the webserver.
Via the downloadURL_image
parameter, an attacker can specify an arbitrary file path to read from the webserver.
2023-12-14 - Vendor Disclosure
2023-12-15 - Vendor Patch Release
2024-01-10 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.