Talos Vulnerability Report

TALOS-2023-1801

VMware DCERPC call request uninitialized memory heap overflow vulnerability

July 13, 2023
CVE Number

CVE-2023-20892

SUMMARY

A heap overflow vulnerability exists in the request processing functionality of DCERPC library as used in VMware vCenter Server 7.0.3.01000 that can lead to use of uninitialized memory. A specially crafted network packet can cause use of uninitialized memory, which can lead to heap overflow and arbitrary code execution. Remote attacker can send a network request 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

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

CWE

CWE-457 - Use of Uninitialized Variable

DETAILS

DCERPC is a remote procedure call protocol that is the basis for RPC functionality on Windows. DCERPC is used as a library in VMWare vCenter to implement this protocol and enable interoperability of Windows network services inside vCenter.

A version of the DCERPC codebase is used by VMWare vCenter server. 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.

There exists a vulnerability in a way that DCERPC framework processes request packets with MAYBE semantics. In DCERPC specification, a packet coming from a client that is marked with a MAYBE flag signifies that a client doesn’t expect a reply. In DCERPC framework implementation, this changes the codepath taken when sending out a call fault, which can result in access of an otherwise uninitialized memory. To reach the vulnerable codepath, a bind request packet is sent first, to get to the proper state of the state machine. Regardless of the validity of the bind request, a subsequent rpc call request can trigger the vulnerable code path. When responding to a call request, function rpc__cn_call_end is called and the following code is reached:

    /*
     * If there are iovector elements in the call_rep,
     * free them all.  There would not be any in the case
     * of a maybe call.
     */
    if (RPC_CN_CREP_IOVLEN (call_rep) > 0)                        [1]
    {
        for (cur_iov_index = 0;
             cur_iov_index < RPC_CN_CREP_IOVLEN (call_rep);
             cur_iov_index++)
        {
            if (RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_dealloc !=         [2]
                NULL)
            {
                (RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_dealloc)  
                (RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_addr);
            }
            RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_addr = NULL;
        }
    }

The above code is responsible for freeing chunks of fragmented packets, and as the comment specifies, it expects that a maybe call wouldn’t have any. A check at [1] simply translates to the following:

 #define RPC_CN_CREP_IOVLEN(cp)             (((cp)->buffered_output).iov.num_elt)

If num_elt of IO vector isn’t 0 (that is, there are additional buffers allocated), it proceeds to iterate over them to perform cleanup. Conveniently, each IO vector element can contain a pointer to a function that is to be called to deallocate it. Otherwise, its associated buffer address is simply set to NULL. From this, we can conclude that if num_elt is somehow bigger than the actual number of elements, a heap overflow would result during dereference at [2].

If we consider the codepaths that lead us to here, one of them goes through function handle_first_frag_action_rtn in which we can find the following code:

  /*
     * Copy the opnum field into the local call rep.
     */
    call_rep->opnum = RPC_CN_PKT_OPNUM (request_header_p);
    if (!(RPC_CN_PKT_FLAGS (request_header_p) & RPC_C_CN_FLAGS_MAYBE)) [3]
    {

/*
     * Fill in the fields of the response header if this is not
     * a maybe call.
     */
    RPC_CN_CREP_SIZEOF_HDR (call_rep) = RPC_CN_PKT_SIZEOF_RESP_HDR;
    response_header_p = (rpc_cn_packet_p_t) RPC_CN_CREP_SEND_HDR (call_rep);
    ...
    ...
    ...
    /*
     * Initialize the iovector in the call_rep to contain only
     * one initial element, pointing to the protocol header.
     * Also, update pointers to show that we can copy data into
     * the stub data area.
     */
    RPC_CN_CREP_IOVLEN (call_rep) = 1;                       [4]
    RPC_CN_CREP_CUR_IOV_INDX (call_rep) = 0;
    RPC_CN_CREP_FREE_BYTES (call_rep) =
        RPC_C_CN_SMALL_FRAG_SIZE - RPC_CN_PKT_SIZEOF_RESP_HDR;
    RPC_CN_CREP_ACC_BYTCNT (call_rep) = RPC_CN_PKT_SIZEOF_RESP_HDR;
    RPC_CN_CREP_FREE_BYTE_PTR(call_rep) =
        RPC_CN_PKT_RESP_STUB_DATA (response_header_p);
    (RPC_CN_CREP_IOV (call_rep)[0]).data_len = RPC_CN_PKT_SIZEOF_RESP_HDR;

At [3], true branch is taken only if the packet is not a MAYBE call, and at [4] num_elt is properly initialized to 1. No initialization happens if the request has a RPC_C_CN_FLAGS_MAYBE flag set, in which case num_elt remains uninitialized. If the uninitialized value happens to be non-zero, this can lead to a heap buffer overflow at [2].

With multiple simultaneous connections and calculated and timed allocations, enough control over memory layout could be achieved to precisely control the contents of the uninitialized memory, which could result in arbitrary code execution.

Note that the original DCERPC codebase was independently modified and reused by both VMWare in vCenter and Apple in macOS. Hence, this analysis is largely the same as the one described with more context in TALOS-2022-1677.

Crash Information

Crash happens as an invalid memory is used as a function pointer, thus hijacking control flow:

#0  0x0000011000000001 in ?? ()
#1  0x00007ffff6e82cac in rpc__cn_call_end (call_r=0x7fffea38bac0, st=0x7fffea38ba40) at ../../../dcerpc/ncklib/cncall.c:1794
#2  0x00007ffff6e91ca9 in rpc__cn_call_executor (arg=0x62f000000400, call_was_queued=0x0) at ../../../dcerpc/ncklib/cncthd.c:284
#3  0x00007ffff6e1c4e9 in cthread_call_executor (cthread=0x619000020228) at ../../../dcerpc/ncklib/comcthd.c:574
#4  0x00007ffff6cd73ee in proxy_start (arg=0x6030000562f0) at ../../../dcerpc/libdcethread/dcethread_create.c:100
#5  0x00007ffff6335f87 in start_thread () from /lib/libpthread.so.0
#6  0x00007ffff622662f in clone () from /lib/libc.so.6 
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 Aleksandar Nikolic of Cisco Talos.