近日,Akamai研究人员在微软Windows RPC服务中发现了两个重要漏洞:严重程度分值为4.3的CVE-2022-38034,以及分值为8.8的CVE-2022-38045。这些漏洞可以利用设计上的瑕疵,通过缓存机制绕过MS-RPC安全回调。我们已经确认,所有未安装补丁的Windows 10和Windows 11计算机都会受到影响。
这些漏洞已经披露给微软,而微软也在10月的“周二补丁日”通过补丁修补了相关问题。漏洞的发现过程得到了Akamai研究人员开发的自动化工具和方法论支持,本文将介绍该漏洞的一些情况以及我们在研究过程中使用的工具(RPC工具包代码库)。
背景介绍
MS-RPC 是Windows操作系统的基石之一,自从二十世纪九十年代诞生以来,已经深深扎根在系统的很多功能和组件中。服务管理器?离不开RPC!Lsass?需要RPC!COM?也依赖RPC!甚至针对域控制器执行的一些域操作同样需要用到RPC。RPC已经如此普遍,很多人都觉得这项技术一定已经进行了大量检查、记录和研究。
其实并不然。虽然微软围绕RPC的使用提供了很多不错的文档,但有关本次漏洞相关主题的内容却并不多,研究人员对RPC,尤其是其安全性的关注也严重不足。这可能是因为RPC(不仅是MS-RPC,但微软无疑也牵涉其中)实在是太复杂,因此也就更加难以研究和理解。
但我们总是乐于接受挑战的,因此决定一头扎进MS-RPC的深海。不光是因为这是个有趣的研究课题,而且因为它会对安全性产生影响,毕竟哪怕到现在,也有很多常见的攻击技术用到了RPC(简单列举几个,例如通过MS-COM发起的T1021.003、通过MS-TSCH发起的T1053.005,以及通过MS-SCMR发起的T1543.003)。MS-RPC虽然内建了安全机制,但如果存在可被轻松绕过或滥用的漏洞呢?如果能滥用暴露的RPC服务以非用户希望的方式影响计算机呢?
实际上,我们就发现了一种可以通过缓存绕过某个安全机制的方法,并借此发现了可以滥用的服务,从而能在远程服务器上提升自己的特权,并且完全不需要满足其他必要条件(下文将详细分析)。目前我们可以分享有关这些潜在利用方式的两个例子:WksSvc和SrvSvc。当披露流程全部结束后,我们还将公布其他漏洞信息的详情。
本文将专注于RPC服务器的安全回调机制,通过缓存绕过该机制的方法,以及我们如何通过自动化研究方法将Windows服务标记为存在潜在漏洞的。我们的自动化工具以及原始输出结果可参阅我们的RPC工具包,这些信息已经发布到GitHub代码库中。这个代码库还提供了指向其他实用参考资源以及我们所依赖的其他研究成果的链接。
安全回调
在讨论漏洞本身之前,首先有必要对MS-RPC所实现的最基本的安全机制之一进行一个简单的说明:安全回调(Security callback)。安全回调可以让RPC服务器开发者对RPC接口的访问加以限制,从而应用开发者自己的逻辑来允许特定用户访问,强制实施身份验证或设置传输类型,以及阻止对特定Opnum(服务器所暴露的函数可以用Opnum来代表,即“操作编号”)的访问。
每当客户端调用服务器暴露出的函数时,RPC运行时都会发出这样的回调。
RPC安全回调
我们的研究主要专注于远程客户端-服务器交互。特别提到这一点是因为,RPC运行时在服务器端的代码实现与ALPC端点和远程端点(如命名管道)的代码实现有所差异。
缓存机制
为改善性能并提高利用率,RPC运行时会对安全回调的结果实现缓存。基本上,这意味着在每次调用安全回调之前,运行时会尝试着先使用缓存的项。让我们深入了解一下具体的实现吧。
在RPC_INTERFACE::DoSyncSecurityCallback调用安全回调前,首先会检查是否存在缓存项。为此需要调用OSF_SCALL::FindOrCreateCacheEntry。
OSF_SCALL::FindOrCreateCacheEntry会执行下列操作:
- 从SCALL(一种代表客户端调用的对象)获取客户端的安全上下文。
- 从客户端的安全上下文中获取缓存字典。
- 使用接口指针作为字典的键,并使用缓存项作为值。
- 如果不存在缓存项,则会创建一个。
RPC运行时内部的缓存过程
每个缓存项都有三个重要字段:接口中的过程(Procedure)数量、一个位图,以及接口代系(Generation)。
在RPC服务器生命周期内,接口可能会被改变,例如服务器针对现有接口调用RpcServerRegisterIf3后。这又会导致随后调用RPC_INTERFACE::UpdateRpcInterfaceInformation,从而更新接口并增大接口的代系。这样,缓存就知道自己需要“重置”,因为缓存项可能来自原先的接口。
缓存机制可以工作在两种模式下:基于接口(这是默认行为)的模式,以及基于调用的模式。
- 基于接口的缓存
在该模式下,缓存以接口为基础运行。这意味着从缓存的视角来看,只要位于同一个接口上,对两个不同函数的两个调用就没有任何区别。
为了知道是否可以使用缓存项还是需要调用安全回调,RPC运行时会将缓存项中保存的接口代系与实际的接口代系进行对比。由于缓存项的初始化过程会让接口代系归零,因此在首次进行对比时,接口代系必然是不同的,因而总是会调用安全回调。如果回调成功返回,RPC运行时就会更新缓存项的接口代系(从而将其“标记”为一个成功的缓存项,随后无需再次调用安全回调就可以访问接口了)。客户端下次调用相同接口上的函数时将使用缓存项。
- 基于调用的缓存
当RPC接口使用RPC_IF_SEC_CACHE_PER_PROC标志注册时将使用此模式。在该模式下,缓存会通过一个位图来追踪安全回调可以访问哪些过程。因此,如果客户端调用Foo函数并且安全回调成功返回,我们就针对Foo有了一个缓存项;如果客户端调用Bar函数,则将再次调用安全回调。
基于调用的缓存和基于接口的缓存
缓存的相关要求
那么需要满足哪些条件才能让缓存正常工作?首先需要澄清一些术语。MS-RPC代表客户端和服务器之间使用绑定句柄(Binding handle)建立的逻辑连接,客户端和服务器都可以使用指定的函数来操作绑定数据。
绑定可以进行身份验证。当服务器(通过调用RpcServerRegisterAuthInfo)注册身份验证信息时就会发生这种操作,随后客户端可以在绑定上设置身份验证信息。借此,服务器可以获得有关客户端身份的信息。该身份验证过程将输出一个为客户端创建的安全上下文对象。
整个缓存机制均基于这个安全上下文。这意味着如果绑定未经身份验证,将无法为客户端创建安全上下文,因而将无法使用缓存。为了让缓存正常工作,服务器和客户端都需要注册并设置身份验证信息。
但如果服务器不注册身份验证信息会怎样?能否依然使用缓存?这就涉及到多路复用(Multiplexing)。
多路复用
在Windows 10的1703版之前,一个服务可以与其他服务共用同一个Svchost进程。这种行为对MS-RPC的安全性产生了一定影响,因为一些RPC运行时对象是在所有实例之间共享的。例如,在注册一个端点(如TCP端口7777)后,该端点将可用于访问同一个进程中运行的所有接口。因此其他本应只进行本地访问的服务,现在也将可以远程访问。微软也在这里描述了这种行为。
端点能被复用,虽然这一事实已被很多人所了解并有了相关文档记载,但我们想说的是另一种非常类似的行为:SSPI多路复用。作为身份验证信息注册过程的一部分,服务器必须指定要使用的身份验证服务。身份验证服务是一种Security Support Provider(SSP),作为一个软件包,它可以处理从客户端收到的身份验证信息。大部分情况下将会使用NTLM SSP、Kerberos SSP或Microsoft Negotiate SSP,从而在Kerberos和NTLM之间选择最佳选项。
RPC运行时会将身份验证信息以全局的方式保存。这意味着,如果两个RPC服务器共用同一个进程,并且其中一个服务器注册了身份验证信息,那么另一个服务器也将获得这些身份验证信息。这样,客户端在访问任何一个服务器时,就可以对绑定进行身份验证。从安全的角度来看,如果一个服务器没有注册身份验证信息,此时就不会期待客户端对绑定进行身份验证,也不应该进行缓存。然而事实并非如此。
CVE-2022-38045 — Srvsvc
在了解了有关RPC安全回调和缓存的基础知识后,我们可以看看是否能在真实世界中滥用这一机制。我们选择了Srvsvc,过去我们已经在其中发现了一个被逐步击破的漏洞。
Srvsvc暴露了MS-SRVS接口。Server服务(也叫做LanmanServer)是Windows中一项负责管理SMB共享的服务。共享也是一种资源(文件、打印机、目录树),可通过Common Internet File System(CIFS)服务器进行网络访问。本质上来看,网络共享可以让用户利用网络上的其他设备执行日常任务。
在研究Srvsvc的安全回调时,我们注意到除了已经发现的漏洞外,该函数可能包含其他漏洞。一起看看安全回调的逻辑:
Srvsvc的安全回调逻辑
如上所示,Srvsvc的安全回调包含下列逻辑:
- 如果远程客户端试图访问介于64-73(含)范围内的函数——拒绝访问。
- 如果非集群账户的远程客户端试图访问介于58-63(含)范围内的函数——拒绝访问。
因此从本质上来看,远程客户端被禁止访问接口上的特定函数。从这个范围检查可知,受限制的函数都是敏感函数,只能被预期的(本地)进程所调用。
尽管这个检查试图阻止对这些函数的远程访问,但远程攻击者只要滥用缓存即可绕过这一限制。首先,远程攻击者可以调用一个不在该范围内的函数,即可以远程使用的函数。由于安全回调函数会返回RPC_S_OK,RPC运行时即可将结果成功缓存。又因为该接口并未使用RPC_IF_SEC_CACHE_PER_PROC标记注册,因此将使用基于接口的缓存。所以,攻击者下一次调用相同接口上的任意函数时,将直接使用缓存项,进而允许访问。这意味着攻击者现在将可以调用自己本不应能访问的本地函数,这一过程中完全不会调用安全回调。
RPC安全回调的缓存绕路过程
Srvsvc并不注册身份验证信息,因此正常情况下,客户端将无法对绑定进行身份验证,进而无法启用缓存。但事实证明,当服务器计算机内存数少于3.5GB时,Srvsvc将与其他服务共用同一个Svchost进程。AD Harvest Sites and Subnets Service和Remote Desktop Configuration Service这两个服务会注册身份验证信息,因此Srvsvc此时就容易受到缓存攻击了。
在这种特定情况下,攻击者可以使用Opnum 58–74访问受限制的函数,而攻击者利用这些函数的方式之一就是胁迫远程计算机进行身份验证。
开始寻宝吧
在了解到滥用安全回调的缓存机制会产生实际的漏洞后,我们决定找出其他可能受到缓存攻击影响的接口。但如果要手工查找所有接口,这将是一项漫长而艰巨的任务,于是我们打算用自动化的方式来完成。
这种情况下,可以通过两种方式来查找RPC接口:通过当前正在运行的进程,或通过文件系统。
对于正在运行的进程,我们可以查看已经载入内存的RPC服务器,为此或者在远程服务器上查询远程端点映射器(例如使用Impacket的rpcmap或rpcdump工具),或者在本地进行(使用RpcView或RpcEnum等工具)。不过这种方式会遇到一个问题:会漏掉所有当前尚未加载的端口,并起我们无法查看客户端端口,因为它们还没注册。
或者也可以搜索Windows文件系统,在其中查找文件中编译的RPC接口。对于每个接口,我们通过分析传递给RpcServerRegisterIf的参数来找出注册信息。这有些类似RpcEnum的做法,但我们查找的是文件系统而非内存。
为了涵盖并未载入内存的接口,我们的研究最终选择了文件系统的方法。我们编写了多个脚本和工具将这一过程自动实现,相关内容均已发布至RPC工具包代码库。
为了找到启用缓存的接口,我们其实并不需要解析RPC接口本身,所需的全部信息都能从RPC服务器的注册调用中提取。注册函数可接受RPC接口结构、注册标记以及安全回调指针。尽管如此,解析RPC接口结构也能提供很多实用信息,例如接口暴露的函数,接口被客户端还是服务器使用等。虽然我们最关注的是RPC服务器(其中可能存在漏洞),但RPC客户端也对服务器的调用提供了可供参考的宝贵见解。
RPC服务器接口结构请参阅该文档,借此我们就不必猜测各种字段了。另外,大小字段和传输语法是不变的(实际上可能的传输语法有两种:DCE NDR和NDR64,但我们只是意外发现了DCE NDR)。
PE文件中保存的RPC接口结构代码片段截图,框出的内容为大小和传输语法
通过(使用Yara或正则表达式)寻找这两个常量,我们可以很简单地找到所有RPC接口结构。一旦找到后,即可借助解释器信息字段来了解服务器到底实现了哪些功能。
清理后的输出内容范例
但我们还是缺乏有关接口安全回调的信息(如果存在这些信息的话),同时也不知道接口是否会被缓存。为此,我们必须求助可信赖的朋友:反汇编器。每个称职的反汇编器都会提供xref功能,借此可以在RPC服务器中轻松找到所有接口注册函数调用。这样,我们就只需要解析函数调用参数并借此提取接口结构地址(以便将其与我们收集到的RPC服务器数据交叉引用),以及安全回调地址(如果存在)和RPC接口标记。
RPC服务注册反汇编
我们所公布的清理脚本可以实现上述这一系列操作。您可以在RPC工具包中获取该脚本,以及在Windows Server 2012和Server 2022上的输出结果。
到底能有效果吗?
这些方法和理论听起来都挺不错,但真的能获得结果吗?
答案是:能。共有超过120个接口同时具备安全回调和缓存,很多都缺乏文档记载。这本身并不值得恐慌,因为大部分时候,安全回调并不会受到缓存机制如此大的影响。通常来说,安全回调所执行的检查都是针对不可缓存的值进行的,例如传输协议序列(如TCP)或身份验证级别。任何变更都需要一个新的安全上下文,因为此时需要建立新的连接,这就重置了缓存,并且让任何可能的缓存绕路措施失效。
但我们通过这种研究方法也发现了一些漏洞。目前只能讨论其中一个,因为其他漏洞还没有走完披露过程。
WksSvc
- CVE-2022-38034 CVSS评分:4.3
WksSvc暴露了MS-WKST接口。该服务负责管理域成员关系、计算机名称以及到SMB网络重定向器(如SMB打印服务器)的连接。查看该接口的安全回调我们发现,少数函数的处理方式与其他函数有很大差异。
WksSvc安全回调的一部分,展示了不同函数和不同Opnum之间的差异
当通过本地客户端调用Opnum介于8-11之间的函数时,也需要进行检查,这意味着不允许对它们进行远程调用。但因为使用了缓存,如果首先调用另一个允许远程调用的函数,然后调用这个受限制的函数,又会发生什么?
您猜对了:由于第一个调用所创建的缓存项,我们将可以用远程方式调用这个原本受到限制只能本地调用的函数。那么现在又产生了新问题:这些函数是否重要到需要保证它们受到限制,只能通过本地客户端调用?
暴露的函数包括:NetrUseAdd、NetrUseGetInfo、NetrUseDel以及NetrUseEnum。也许您觉得熟悉,因为它们都能通过netapi32.dll访问(例如可参阅NetUseAdd)。
这很好,因为我们从中获得了一条线索,从而可以确定能通过这种攻击做些什么。也就是说,我们可以将远程服务器连接到我们自己指定的网络共享文件夹,甚至将其映射到我们选择的逻辑驱动器盘符,这与net use的作用极为类似。(巧合?也许未必!)
这样我们就可以指定两种攻击方案:
1.可以要求对我们的共享文件夹进行身份验证,随后将其转发至不同服务器以进行NTML重播攻击,或将令牌存储起来以便脱机破解密码。
要求对我们的恶意文件服务器进行身份验证,随后即可在验证过程中窃取NT哈希
2.或者我们可以用一些有趣或实用的文件伪装称成现有的文件服务器(或假装是全新文件服务器),由于这些文件都在我们控制之下,因此可以用我们认为适合的方式将其变为武器,进而感染目标用户。
将恶意Web服务器充当中间人攻击手段或钓鱼服务器,向不够警惕的用户发送武器化的文档或恶意软件
上述两个场景,以及以远程方式调用受到限制只能本地调用的函数的能力,足以让微软将这个漏洞归类为EoP分类,并给出4.3分的评分。
但这还不是故事的全部,我们还有一些问题需要注意。
安全上下文
WksSvc下的RPC服务器本身并不进行任何身份验证注册。如果该服务独立运行,将无法进行任何客户端身份验证(这样做会导致RPC_S_UNKNOWN_AUTHN_SERVICE错误)。因此我们需要让该服务与其他服务一起运行,以便同时滥用SSPI多路复用机制。这也使得受影响的Windows版本仅限Windows 10 1703之前的版本,或在内存不足3.5GB的情况下运行的更新版本的Windows。
登录会话
另一个问题则和网络映射文件夹的工作方式有关,这类文件夹始终会被限制在创建映射文件夹的用户的登录会话中。因为我们最开始需要登录才能获得安全绑定和缓存,这意味着我们在目标计算机上将始终创建不同于现有(交互式)会话的另一个登录会话。就所有意图和目的而言,这意味着我们的漏洞并不会产生影响。我们所创建的网络映射只存在于我们那短暂的登录会话下,并不像普通用户登录计算机时创建的网络映射,我们创建的映射根本不可见。
WinObj截图,展示了所创建的逻辑盘符只存在于发起攻击的登录会话上下文中,并不会出现在交互式会话中
为了克服这个问题,我们又深入挖掘了NetrUseAdd的代码。结果发现,我们可以向NetrUseAdd传递一些标记,借此让它在Global命名空间中创建映射,这将影响所有用户。这些标记甚至能在可用的头文件LMUse.h中找到:
在LMUse.h中看到的全局映射标记
借助这些标记,我们的代码已经可以成功创建能影响交互式会话的全局映射,随后就可以利用了。
WinObj和资源管理器的截图片段,展示了对WksSvc的成功利用:在远程服务器上创建了一个全局驱动器映射,并使其在资源管理器中对已登录用户可见
总结
MS-RPC是一个庞大而复杂的协议,它也承担了Windows的一些核心功能。虽然开发者可以利用一些安全功能保护自己的RPC服务器,但对安全研究人员来说,这依然是一个有趣的话题,这恰恰是因为它包含了一个可能产生安全影响的漏洞。
尽管如此,有关该话题的公开研究也并不多。本文我们探讨了MS-RPC中的一个大型安全机制(安全回调),并发现了以回调结果缓存形式存在的绕过机制。我们还介绍了自己发现有漏洞RPC服务器所采用的研究方法,并通过漏洞的利用演示了我们的发现。
希望本文提供的内容,以及我们的RPC工具包代码库能对其他人的研究工作起到一些帮助。