Talos Vulnerability Report

TALOS-2022-1538

WWBN AVideo footer alerts cross-site scripting (XSS) vulnerability

August 16, 2022
CVE Number

CVE-2022-32770,CVE-2022-32772,CVE-2022-32771

SUMMARY

A cross-site scripting (xss) vulnerability exists in the footer alerts functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted 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

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

CWE

CWE-79 - Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)

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 view logic in AVideo normally includes a generic view/include/footer.php file to handle the pages footer.

The code in footer.php does not sanitize variables correctly, and is thus vulnerable to multiple reflected cross-site scripting (XSS) issues. These issues can be used by an attacker, in the worst case, to take over an administrator account, for example by tricking an administrator into clicking on a link that triggers the XSS.

The issue in footer.php starts with these lines:

<script>
    $(function () {
<?php
showAlertMessage();
?>
    });
</script>

This defines a Javascript function that shows an alert message. For example, with the request https://192.168.1.200/view/charts.php?msg=test, we end up with the following code:

<script>
    $(function () {
/** showAlertMessage **/
avideoAlertInfo("test");window.history.pushState({}, document.title, "https://192.168.1.200/view/charts.php?msg=test");
/** showAlertMessage END **/    });
</script>

The function showAlertMessage is defined in objects/functions.php:

function showAlertMessage() {
    ...

    // [1]
    $joinString = ['error', 'msg', 'success'];
    foreach ($joinString as $value) {
        if (!empty($_GET[$value]) && is_array($_GET[$value])) {
            $_GET[$value] = array_unique($_GET[$value]);
            $newStr = [];
            foreach ($_GET[$value] as $value2) {
                if (!empty($value2)) {
                    $newStr[] = $value2;
                }
            }
            $_GET[$value] = implode("<br>", $newStr);
        }
    }

    // [2]
    $check = ['error', 'msg', 'success', 'toast'];
    foreach ($check as $value) {
        if (!empty($_GET[$value])) {
            if (is_array($_GET[$value])) {
                $newStr = [];
                foreach ($_GET[$value] as $key => $value2) {
                    $value2 = str_replace('"', "''", $value2);
                    if (!empty($value2)) {
                        $newStr[] = $value2;
                    }
                }
                $_GET[$value] = $newStr;
            } else {
                $_GET[$value] = str_replace('"', "''", $_GET[$value]);
            }
        }
    }

    // [3]
    echo "/** showAlertMessage **/", PHP_EOL;
    if (!empty($_GET['error'])) {
        echo 'avideoAlertError("' . $_GET['error'] . '");';
        echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
    }
    if (!empty($_GET['msg'])) {
        echo 'avideoAlertInfo("' . $_GET['msg'] . '");';
        echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
    }
    if (!empty($_GET['success'])) {
        echo 'avideoAlertSuccess("' . $_GET['success'] . '");';
        echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
    }
    if (!empty($_GET['toast'])) {
        if (!is_array($_GET['toast'])) {
            $_GET['toast'] = [$_GET['toast']];
        } else {
            $_GET['toast'] = array_unique($_GET['toast']);
        }
        foreach ($_GET['toast'] as $key => $value) {
            $hideAfter = strlen(strip_tags($value)) * 150;

            if ($hideAfter < 3000) {
                $hideAfter = 3000;
            }
            if ($hideAfter > 15000) {
                $hideAfter = 15000;
            }

            echo '$.toast({
                    text: "' . $value . '",
                    hideAfter: ' . $hideAfter . '   // in milli seconds
                });console.log("Toast Hide after ' . $hideAfter . '");';
        }
        echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
    }
    echo PHP_EOL, "/** showAlertMessage END **/";
}

At [1], the error, msg and success variables are concatenated if they’re passed as arrays.
At [2], double quotes are replaced with two single quotes for error, msg, success and toast parameters.
At [3], for each of the 4 parameters, some Javascript is output. Of these 4 parameters, only error is output after proper sanitization.

Sanitization happens in objects/security.php, which is included in every request:

...
// filter some security here
$securityFilter = ['error', 'catName', 'type', 'channelName', 'captcha', 'showOnly', 'key', 'link', 'email', 'country', 'region', 'videoName'];
$securityFilterInt = ['isAdmin', 'priority', 'totalClips', 'rowCount'];
$securityRemoveSingleQuotes = ['search', 'searchPhrase', 'videoName', 'databaseName', 'sort', 'user', 'pass', 'encodedPass', 'isAdmin', 'videoLink', 'video_password'];
$securityRemoveNonChars = ['resolution', 'format', 'videoDirectory'];
$filterURL = ['videoURL', 'siteURL', 'redirectUri', 'encoderURL'];
...
$scanVars = ['_GET', '_POST', '_REQUEST'];

foreach ($scanVars as $value) {
    $scanThis = &$$value;
    ...
    foreach ($securityFilter as $value) {
        if (!empty($scanThis[$value])) {
            // [4]
            $scanThis[$value] = str_replace(['\\', "--", "'", '"', "&quot;", "&#039;", "%23", "%5c", "#"], ['', '', '', '', '', '', '', '', ''], xss_esc($scanThis[$value]));
        }
    }
    ...
}

error is present in the $securityFilter array, so any time a request with an error parameter is sent, the sanitization at [4] is applied.

There are no entries for msg, success or toast. All those parameters are only subject to the sanitization at [2] (double quotes replacement), which can be bypassed, thus making the three parameters vulnerable to XSS at [3].

CVE-2022-32770 - “toast” parameter

The toast parameter is inserted into the document with insufficient sanitization:

echo '$.toast({
    text: "' . $value . '",
    hideAfter: ' . $hideAfter . '   // in milli seconds
});console.log("Toast Hide after ' . $hideAfter . '");';

While an attacker can’t use double quotes to escape the string definition, one can simply inject </script><script> to start a new script and inject any Javascript.

CVE-2022-32771 - “success” parameter

The success parameter is inserted into the document with insufficient sanitization:

if (!empty($_GET['success'])) {
    echo 'avideoAlertSuccess("' . $_GET['success'] . '");';
    echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
}

While an attacker can’t use double quotes to escape the string definition, one can simply inject </script><script> to start a new script and inject any Javascript.

CVE-2022-32772 - “msg” parameter

The msg parameter is inserted into the document with insufficient sanitization:

if (!empty($_GET['error'])) {
    echo 'avideoAlertError("' . $_GET['error'] . '");';
    echo 'window.history.pushState({}, document.title, "' . getSelfURI() . '");';
}

While an attacker can’t use double quotes to escape the string definition, one can simply inject </script><script> to start a new script and inject any Javascript.

Exploit Proof of Concept

This is a proof-of-concept for the msg parameter:

curl https://192.168.1.200/index.php?msg=</script><script>alert(document.cookie);</script>

One can also exploit the array concatenation happening at [1] to trigger the same XSS (note this is not possible for the toast parameter):

curl https://192.168.1.200/view/charts.php?msg[0]=</script><script>/*&msg[1]=*/alert(1)//&msg[2]=</script>
VENDOR RESPONSE

Vendor confirms issues fixed on July 7th 2022

TIMELINE

2022-07-05 - Vendor Disclosure
2022-07-07 - Vendor Patch Release
2022-08-16 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.