自 20 世纪 90 年代初以来,组件对象模型 (COM) 一直是 Microsoft Windows 开发的核心基石,至今仍在现代 Windows 操作系统和应用程序中广泛应用。多年来对 COM 组件的依赖以及大量功能开发,导致其形成了庞大的攻击面。2025 年 2 月,Google Project Zero 的 James Forshaw (@tiraniddo) 发布了一篇博客文章,详细介绍了一种滥用分布式 COM(DCOM)远程技术的新方法——通过捕获的 COM 对象,可在服务器端 DCOM 进程的上下文环境中执行 .NET 托管代码。Forshaw 重点介绍了权限提升与受保护进程轻量版 (PPL) 绕过的多个用例。
基于 Forshaw 的研究,Mohamed Fakroud (@T3nb3w) 于 2025 年 3 月初发布了该技术的实现方案,用于绕过 PPL 保护机制。Jimmy Bayne (@bohops) 与我于 2025 年 2 月开展了类似研究,最终开发出一种概念验证级无文件横向移动技术,其核心是滥用捕获的 COM 对象。
COM 是一种二进制接口标准及中间件服务层,支持不同的模块化组件彼此交互、与应用程序对接,且不受底层编程语言限制。例如,使用 C++ 开发的 COM 对象可轻松与 .NET 应用程序对接,帮助开发人员高效整合各类软件模块。DCOM 是一种远程技术,支持 COM 客户端通过进程间通信 (IPC) 或远程过程调用 (RPC) 与 COM 服务器通信。许多 Windows 服务都实现了可本地或远程访问的 DCOM 组件。
COM 类通常会注册并存储在 Windows 注册表中。客户端程序通过创建 COM 类的实例(即 COM 对象)与 COM 服务器进行交互。该对象会提供一个指向标准化接口的指针。客户端通过该指针访问对象的方法和属性,为客户端与服务器之间的通信及功能实现提供支持。
COM 对象通常是评估漏洞暴露面、发现可滥用功能的研究目标。捕获的 COM 对象是一类漏洞场景:COM 客户端在进程外 DCOM 服务器中实例化 COM 类,通过引用封送对象指针控制该 COM 对象。根据具体场景,该控制路径可能存在与安全相关的逻辑漏洞。
Forshaw 的博客描述了一个 PPL 绕过用例:通过操纵 WaaSRemediation COM 类中公开的 IDispatch 接口,实现对捕获 COM 对象的滥用及 .NET 代码执行。WaaSRRemediation 在 WaaSMedicSvc 服务中实现,该服务以 NT AUTHORITY\SYSTEM 权限,作为受保护的 svchost.exe 进程运行。Forshaw 详尽的技术详解,为我们开发概念验证级无文件横向移动技术提供了应用研究基础。
我们的研究从深入了解支持 IDispatch 接口的 WaaSRemediation COM 类起步。该接口支持客户端执行延迟绑定操作。通常,COM 客户端会在编译阶段定义其使用对象的接口及类型信息。而延迟绑定允许客户端在运行时发现并调用对象的方法。IDispatch 包含 GetTypeInfo 方法,该方法会返回 ITypeInfo 接口。ITypeInfo 提供的方法可用于获取实现该接口的对象的类型信息。
如果 COM 类使用类型库,客户端可通过 ITypeLib(通过 ITypeInfo->GetContainingTypeLib 获取)查询该类型库以获取类型信息。此外,类型库还可能引用其他类型库以补充类型信息。
根据 Forshaw 的博客内容,WaaSRemediation 引用了 WaaSRemediationLib 类型库,而该类型库又引用了 stdole(OLE 自动化)。WaaSRemediationLib 采用了该库中的两个 COM 类:StdFont 和 StdPicture。通过修改 StdFont 对象的 TreatAs 注册表项对其执行 COM 劫持,可使该类指向我们选定的另一个 COM 类(例如 .NET Framework 中的 System.Object)。值得注意的是,Forshaw 指出 StdPicture 不可行,因为该对象会对进程外实例化进行校验,因此我们将研究重点放在了 StdFont 的应用上。
我们之所以关注 .NET 对象,核心原因是 System.Object 具备 GetType 方法。通过 GetType 方法,我们可执行 .NET 反射,最终调用 Assembly.Load。尽管我们选择了 System.Object,但该类型恰好是 .NET 类型层次结构的根类型。因此,任何 .NET COM 对象都可用于此类场景。
完成初始配置后,要将我们设想的用例落地,还需依赖 HKLM\Software\Microsoft.NetFramework 项下的另外两个 DWORD 值:
初始测试中确认可加载最新版本的 CLR 和 .NET 后,我们确认方向正确。
我们将注意力转向远程编程层面,首先通过远程注册表工具操作 .NetFramework 注册表项值,并劫持目标机器上的 StdFont 对象。随后,我们将 CoCreateInstance 替换为 CoCreateInstanceEx,在远程目标上实例化 WaaSRemediation COM 对象,并获取指向 IDispatch 接口的指针。
获得 IDispatch 接口指针后,我们调用其 GetTypeInfo 成员方法,获取指向 ITypeInfo 接口的指针 —— 该接口会驻留在服务器端。此后调用的所有成员方法均在服务器端执行。在识别出目标类型库引用 (stdole) 并推导得出目标类对象引用 (StdFont) 后,我们最终通过 ITypeInfo 接口的“可远程调用”CreateInstance 方法,重定向 StdFont 对象的链接流程(通过此前的 TreatAs 操作),从而实例化 System.Object。
由于 AllowDCOMReflection 已正确配置,我们可通过 DCOM 执行 .NET 反射,调用 Assembly.Load 将 .NET 程序集加载到 COM 服务器中。鉴于我们是通过 DCOM 调用 Assembly.Load,该横向移动技术完全属于无文件攻击,程序集字节传输由 DCOM 远程机制自动处理。要深入了解从对象实例化到反射的完整技术流程,请参阅下图:
我们面临的首要问题是通过 IDispatch->Invoke 调用 Assembly.Load_3。Invoke 会将参数的对象数组传递给目标函数,而 Load_3 是 Assembly.Load 的重载方法,仅接受单个字节数组作为参数。因此,我们需要将字节类型的 SAFEARRAY 包装在变体类型 (VARIANT) 的 SAFEARRAY 中 —— 最初我们一直尝试直接传递单个字节类型的 SAFEARRAY,导致调用失败。
另一个问题是找到正确的 Assembly.Load 重载方法。辅助函数取自 Forshaw 的 CVE-2014-0257 相关代码,其中包含 GetStaticMethod 函数。该函数通过 DCOM 执行 .NET 反射,根据类型指针、方法名称及其参数数量查找静态方法。Assembly.Load 有两个仅接受单个参数的静态重载方法,因此我们最终采用了一种临时解决方案。我们发现,第三个仅接受单个参数的 Load 方法实例正是我们需要的。
我们观察到该技术的最大缺陷之一是:生成的信标生命周期与 COM 客户端绑定——在本测试场景中,即我们的武器化二进制程序 “ForsHops.exe” 的运行周期。(当然,这个命名相当巧妙)。因此,如果 ForsHops.exe 清理自身的 COM 引用或退出运行,远程机器 svchost.exe 进程下运行的信标也会随之终止。我们尝试了多种解决方案,例如让 .NET 程序集无限期挂起主线程、在另一个线程中执行 shellcode、让 ForsHops.exe 保持漏洞利用线程挂起状态,但均未达到理想效果。
目前版本中,ForsHops.exe 会持续运行直至信标退出,届时会清理其执行的注册表操作。该技术仍有改进空间,相关优化工作就留给读者自行探索。
检测指南由 Samir Bousseaden (@SBousseaden) 在 Mohamed Fakroud 发布实现方案后提出,同样适用于该横向移动技术:
此外,我们建议采取以下额外控制措施:
此外,可利用以下概念验证级 YARA 规则检测标准的 ForsHops.exe 可执行文件:
规则 Detect_Standard_ForsHops_PE_By_Hash
我们的实现方案对 Forshaw 博客中阐述的 COM 滥用方式做了小幅扩展,即利用捕获的 COM 对象实现横向移动,而非通过本地执行来实现 PPL 绕过。因此,该方案仍会受到与本地执行实现方案相同的检测机制的监测。
您可以在此处获取 ForsHops.exe 横向移动的概念验证代码。
特别感谢 Dwight Hohnstein (@djhohnstein) 和 Sanjiv Kawa (@sanjivkawa) 为本研究提供反馈,并对博客文章内容进行审核。