Remote Code Execution with ESXi - CVE-2021-21974 VMware ESXi Heap Overflow

Recently, an old vulnerability targeting VMware ESXi has been spotted in the wild. The vulnerability we are talking about is CVE-2021-21974 and exploits the OpenSLP service running on port 427. A malicious actor which has access to the control interface of the ESXi can exploit this vulnerability by sending crafted packets to the ESXi server, resulting in remote code execution on the ESXi server.

This blogpost is about the analysis we did on this vulnerability, in search of more details on how it works and where the vulnerability resides.

In the recent days, this vulnerability has been exploited as part of a ransomware campaign brought to attention by the French national government computer security incident response team (CERT-FR). The goal of the attack is the installation of a malware strain named “ESXiArgs” and, even though, no evidence of data exfiltration was found so far, the ransom note warns about releasing sensitive data if 2 Bitcoins are not paid within 3 days of compromise.

CVE-2021-21974 is a heap-overflow vulnerability in OpenSLP, used in ESXi 7.0 prior to ESXi70U1c-17325551, 6.7 prior to ESXi670-202102401-SG, 6.5 prior to ESXi650-202102101-SG. A malicious actor with access to port 427 (TCP and UDP) on the ESXi installation will be able to execute code in the context of the SLP daemon due to a lack of validation of the length of user-supplied data before copying it to a heap-based buffer.

We know slpd is the vulnerable service, but where in its code is the actual vulnerability? In order to find out the answer, we need a vulnerable setup and a PoC for CVE-2021-21974. We tested the vulnerability by using the public POC available here against ESXi-6.7.3-14320388-NEC-6.7-04.iso.

$ file /sbin/slpd
slpd: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.4.9, not stripped
$ ldd slpd
linux-gate.so.1 (0xf7ed8000)
libcrypto.so.1.0.2 => not found
libc.so.6 => /lib32/libc.so.6 (0xf7ccb000)
/lib/ld-linux.so.2 (0xf7eda000)

We extracted the binaries locally and managed to run it.

Running ltrace on the binary revealed a library call of select as shown below. This is of particular interest for us because this is usually how network services handle incoming data.

We use gef (GDB Enhanced Features) and rerun the service:

We let the program run some time and then we stopped in order to reveal the stack trace:

At this point we knew the address (0x804d8fa) from where the select function is called.

We decided to look at the OpenSLP code that can be found here, and search for the select function calls (as seen from the ltrace output), to have a better understanding of the execution flow and how that is affected by that call’s output:

(Snippet extracted from https://github.com/openslp-org/openslp/blob/5ff93b21201e12127be4b0d5a752a7290afa5d64/openslp/slpd/slpd_main.c#L697 )

We continued the analysis and went further in detail, starting with SLPDIncomingHandler, but we also tried a dynamic approach. Since in certain executions we got an error, we started there:

As the error states, the heap data was corrupted, so we used Valgrind to track the dynamic memory allocation. We managed to catch this issue:

The vulnerable part from the decompiled binary looks like the following:

We also know how the function is called:

(Snippet extracted from https://github.com/openslp-org/openslp/blob/df695199138ce400c7f107804251ccc57a6d5f38/openslp/slpd/slpd_knownda.c#L789 )

The calloc function is called pointing out that a memory region with the size param_1 + 0x1d is required. In other words, urllen+ 0x1d size is required.

The strstr function is called to find the pointer to the :/ string. The string starting from the beginning of the URL until :/ is then copied to __ptr+0x15. At this starting point there are at most (urllen + 0x1d – 0x15 = urllen + 8) bytes left.

The interesting part is regarding how the URL is created. If we look at the function which parses the DA Advert message, we can see that the URL size is extracted by reading 16 bytes from buffer and the pointer is obtained by the call to GetStrPtr.

(Snippet extracted from https://github.com/openslp-org/openslp/blob/df695199138ce400c7f107804251ccc57a6d5f38/openslp/common/slp_v2message.c#L565 )

The GetStrPtr function is of interest for us as it gets a pointer to the current location in the buffer and increments the pointer. No check is made for a null character (\0) to make the string valid from the point of view of the C programming language. Remember the structure of the DA Advert message. After the URL, the next field is the size of the scope list which is stored as a 16 bit Big Endian int. In case the scope list is greater than 0xFF then there will be no \0 character.

(Snippet extracted from https://github.com/openslp-org/openslp/blob/df695199138ce400c7f107804251ccc57a6d5f38/openslp/common/slp_message.c#L109 )

If we take a closer look at the message which generates overflow, the length of the scope-list is 0x298 and no null pointer is included in the length.

So, when we try to perform the strstr function call will find :/ outside of the URL and so the memcpy will generate an out of bounds write.

We won’t go much in depth about how heap exploitation works but there are two strategies in general.

The allocated memory regions returned by malloc/calloc/realloc have the structure from the image below (in the case of glibc, the default libc on linux) and are usually located next to each other. One way is to corrupt the other structures stored on heap, as they are stored nearby. The other way is to control the heap metadata stored by the allocator. In the case of glibc allocator the metadata is mainly composed of the size of the previous and of the next chunk and, in the case of allocated entries, also two pointers for a circular list in the case of freed entries: fd (forward) and bk (backward). In the end, by corrupting the heap, the attacker manages to get arbitrary read and arbitrary write primitives.

(Snippet extracted from http://phrack.org/issues/57/9.html )

Having arbitrary read and arbitrary write, the attacker generally wants to leak the address for libc in order to be able to call system function calls and get a shell. In fact, this is exactly what the POC for this exploit is doing:

It creates some payload to call system function with a string controlled by the attacker. There is one last aspect to be discussed and that is related to how do we gain control of the EIP (Instruction Pointer) register. In this exploit, the authors used the libc_free_hook . This is a pointer to a function which is called on every free() function call. By overwriting this pointer, the attacker can control the Instruction Pointer register to point anywhere he wants. In this case it points to the ROP (Return Oriented Programming) gadget controlled by the attacker.

This concludes the analysis of CVE-2021-21974, and we saw how a remote attacker who has access to port 427 of the ESXi can exploit the slpd service by levering the heap-based overflow vulnerability. Our analysis is based on the VMware ESXi version 6.7U3, however for other versions, the vulnerability should be similar. With all these details in mind we encourage you to upgrade the setups to a more recent version of ESXi or, if this is not possible, at least disable the vulnerable service by following the article from VMware. More information about the vulnerability can be found in the public advisory from VMware, here.

Leverage subscription service to stay ahead of attacks

Keysight's Application and Threat Intelligence (ATI) Subscription provides daily malware and bi-weekly updates of the latest application protocols and vulnerabilities for use with Keysight test platforms. The ATI Research Centre continuously checks threats as they appear in the wild.

limit
3