Windows RPC 服务器安全防护指南与漏洞分析

本文深入探讨了Windows RPC服务器的安全机制,包括端点安全、接口安全和临时安全检查,并以PetitPotam攻击中的EFSRPC漏洞为例,分析了其可利用性及微软的修复措施。

如何保护 Windows RPC 服务器,以及如何不保护

PetitPotam 技术仍在人们的记忆中记忆犹新。虽然它本身不是一个直接的漏洞利用,但它是获取特权账户未经身份验证的 NTLM 以转发到 AD CS Web 注册服务等以危害 Windows 域的有用步骤。有趣的是,在微软最初对修复此问题不置可否后,他们发布了一个修复程序,尽管在撰写本文时似乎还不够完善。

虽然有很多关于如何滥用 EFSRPC 接口的细节,但关于为什么它一开始就可利用的信息却很少。我认为快速概述 Windows RPC 接口是如何保护的,进而为什么可以未经身份验证使用 EFSRPC 接口是很有用的。

注意: 毫无疑问,我可能遗漏了 RPC 中的其他安全检查,这些是我所知道的主要检查 :-)

RPC 服务器安全

RPC 的服务器安全似乎是随着时间的推移而逐步建立的。因此,有各种方法可以实现,有些方法比其他方法更好。基本上有三种方法,可以混合使用:

  • 保护端点
  • 保护接口
  • 临时安全

让我们依次来看每一种方法,以确定它们如何保护 RPC 服务器。

保护端点

您使用 RpcServerUseProtseqEp API 注册 RPC 服务器将监听的端点。此 API 接受端点类型,如 ncalrpc(ALPC)、ncacn_np(命名管道)或 ncacn_ip_tcp(TCP 套接字),并创建监听端点。例如,以下代码将创建一个名为 DEMO 的命名管道端点。

1
2
3
4
5
RpcServerUseProtseqEp(
    L"ncacn_np",
    RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
    L"\\pipe\\DEMO",
    nullptr);

最后一个参数是可选的,但表示您分配给端点的安全描述符(SD),以限制谁有权访问。这只能在 ALPC 和命名管道上强制执行,因为像 TCP 套接字这样的东西在连接时(技术上)没有访问检查。如果您不指定 SD,则会分配一个默认值。对于命名管道,默认 DACL 授予以下用户写访问权限:

  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • SELF

其中 SELF 是创建用户的 SID。这是一个相当宽松的 SD。关于 RPC 端点的一个有趣的事情是它们是复用的。您不会显式地将端点与要访问的 RPC 接口关联起来。相反,您可以连接到进程创建的任何端点。最终结果是,如果同一进程中存在安全性较低的端点,则可能使用最不安全的端点访问接口。总的来说,这使得依赖端点安全性具有风险,尤其是在运行多个服务(如 LSASS)的进程中。无论如何,如果您想使用 TCP 端点,则不能依赖端点安全性,因为它不存在。

保护接口

保护 RPC 服务器的下一种方法是保护接口本身。您使用以下 API 之一注册由 MIDL 生成的接口结构:

  • RpcServerRegisterIf
  • RpcServerRegisterIf2
  • RpcServerRegisterIfEx
  • RpcServerRegisterIf3
  • RpcServerInterfaceGroupCreate

每个 API 的参数数量不同,其中一些参数决定了接口的安全性。最新的 API 是 RpcServerRegisterIf3RpcServerInterfaceGroupCreate,它们在 Windows 8 中引入。后者只是一种在一次调用中注册多个接口的方法,因此我们将重点讨论前者。RpcServerRegisterIf3 有三个影响安全的参数:SecurityDescriptorIfCallbackFlags

SecurityDescriptor 参数最容易解释。它将一个 SD 分配给接口,当在该接口上进行调用时,将根据 SD 检查调用者的令牌,只有在检查通过时才授予访问权限。如果未指定 SD,则使用默认值,授予以下 SID 访问权限(假设是非 AppContainer 进程):

  • NT AUTHORITY\ANONYMOUS LOGON
  • Everyone
  • NT AUTHORITY\RESTRICTED
  • BUILTIN\Administrators
  • SELF

用于访问检查的令牌基于客户端的身份验证(我们稍后会讨论)或端点的身份验证。ALPC 和命名管道是经过身份验证的传输,而 TCP 不是。当使用未经身份验证的传输时,访问检查将针对匿名令牌进行。这意味着如果 SD 不包含对 ANONYMOUS LOGON 的允许 ACE,它将被阻止。

注意,由于访问检查过程的一个怪癖,RPC 运行时在调用者被授予任何访问权限(而不是特定访问权限)时授予访问权限。这意味着如果调用者被认为是所有者(通常设置为创建用户 SID),他们可能只被授予 READ_CONTROL,但这足以绕过检查。如果调用者具有 SeTakeOwnershipPrivilege 或类似权限,这也可能有用,因为它可以通用地绕过接口 SD 检查(尽管该权限本身就很危险)。

第二个参数 IfCallback 接受一个 RPC_IF_CALLBACK 函数指针。当对接口进行调用时,将调用此回调函数,尽管它将在 SD 检查之后调用。如果回调函数返回 RPC_S_OK,则允许调用,否则将拒绝调用。回调获取指向接口和绑定句柄的指针,并可以执行各种检查以确定是否允许调用者访问接口。

一个常见的检查是客户端的身份验证级别。客户端可以使用 RpcBindingSetAuthInfo API 指定连接服务器时使用的级别,但服务器不能直接指定它接受的最低身份验证级别。相反,回调可以使用 RpcBindingInqAuthClient API 来确定客户端使用了什么,并据此授予或拒绝访问。我们通常关心的身份验证级别如下:

  • RPC_C_AUTHN_LEVEL_NONE - 无身份验证
  • RPC_C_AUTHN_LEVEL_CONNECT - 在连接时进行身份验证,但不是每次调用都进行。
  • RPC_C_AUTHN_LEVEL_PKT_INTEGRITY - 在连接时进行身份验证,每次调用都有完整性保护。
  • RPC_C_AUTHN_LEVEL_PKT_PRIVACY - 在连接时进行身份验证,每次调用都经过加密并具有完整性保护。

身份验证使用定义的身份验证服务(如 NTLM 或 Kerberos)实现,尽管这对我们的目的来说并不重要。还要注意,这仅用于通过远程协议(如命名管道或 TCP)可用的 RPC 服务。如果 RPC 服务器在 ALPC 上监听,则假定始终为 RPC_C_AUTHN_LEVEL_PKT_PRIVACY。服务器可以做的其他检查是客户端使用的协议序列,这将允许拒绝通过 TCP 访问,但允许命名管道。

最后一个参数是标志。最明显与安全相关的标志是 RPC_IF_ALLOW_SECURE_ONLY(0x8)。如果当前身份验证级别是 RPC_C_AUTHN_LEVEL_NONE,则此标志阻止对接口的访问。这意味着调用者必须能够使用允许的身份验证服务之一向服务器进行身份验证。至少在任何现代版本的 Windows 上,使用 NULL 会话是不够的。当然,这并没有说明谁已经过身份验证,服务器可能仍然需要检查调用者的身份。

另一个重要的标志是 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH(0x10)。如果服务器指定了安全回调且未设置此标志,则任何未经身份验证的客户端都将被自动拒绝。

如果这还不够复杂,至少还有一个其他相关设置适用于整个系统,它将决定哪种类型的客户端可以访问哪些 RPC 服务器。即“限制未经身份验证的 RPC 客户端”组策略。默认情况下,如果 RPC 服务器在 Windows 的服务器 SKU 上运行,则设置为“无”,在客户端 SKU 上设置为“已身份验证”。

总的来说,此策略的作用是限制客户端在未单独身份验证到有效身份验证级别时是否可以使用未经身份验证的传输(如 TCP)。当设置为“无”时,RPC 服务器可以通过未经身份验证的传输访问,但受接口注册的任何其他限制。如果设置为“已身份验证”,则通过未经身份验证的传输的调用将被拒绝,除非为接口设置了 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH 标志或客户端已单独进行身份验证。还有第三个选项“无例外的已身份验证”,如果调用者未使用经过身份验证的传输,则在所有情况下都会阻止调用。

临时安全

最后一种类型的检查基本上是服务器为验证调用者而做的任何其他事情。一种常见的方法是在接口上的特定函数内执行检查。例如,服务器可能通常允许未经身份验证的客户端,但在调用读取重要秘密值的方法时除外。此时,它可以插入身份验证级别检查,以确保客户端已在 RPC_C_AUTHN_LEVEL_PKT_PRIVACY 进行身份验证,以便在返回给客户端时对秘密进行加密。

最终,您必须检查您感兴趣的每个函数,以确定存在哪些安全检查(如果有)。与所有临时检查一样,可能存在逻辑错误,可以利用这些错误来绕过安全限制。

深入探讨 EFSRPC

好了,这涵盖了 RPC 服务器如何保护的基础知识。让我们看看 PetitPotam 滥用的 EFSRPC 服务器的具体示例。奇怪的是,RPC 服务器有两个实现,一个在 efslsaext.dll 中,接口 UUID 为 c681d488-d850-11d0-8c52-00c04fd90f7e,另一个在 efssvc.dll 中,接口 UUID 为 df1941c5-fe89-4e79-bf10-463657acf44d。efslsaext.dll 中的那个是可以未经身份验证访问的,所以让我们从那里开始。我们将通过三种保护服务器的方法来确定它在做什么。

首先,服务器没有注册任何自己的协议序列,无论是否有 SD。这意味着谁可以调用 RPC 服务器取决于托管进程(在这种情况下是 LSASS)注册了哪些其他端点。

其次,检查对 RPC 服务器接口注册函数之一的调用,在 InitializeLsaExtension 中有一个对 RpcServerRegisterIfEx 的调用。这允许调用者指定安全回调,但不能指定 SD。然而,在这种情况下,它没有指定任何安全回调。InitializeLsaExtension 函数也没有指定两个安全标志中的任何一个(它设置了 RPC_IF_AUTOLISTEN,这没有任何安全影响)。这意味着通常允许任何经过身份验证的调用者。

最后,从临时安全的角度来看,所有主要函数(如 EfsRpcOpenFileRaw)都调用函数 EfsRpcpValidateClientCall,它看起来像下面这样(错误检查已移除)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void EfsRpcpValidateClientCall(RPC_BINDING_HANDLE Binding, 
                               PBOOL ValidClient) {
  unsigned int ClientLocalFlag;
  I_RpcBindingIsClientLocal(NULL, &ClientLocalFlag);
  if (!ClientLocalFlag) {
    RPC_WSTR StringBinding;
    RpcBindingToStringBindingW(Binding, &StringBinding);
    RpcStringBindingParseW(StringBinding, NULL, &Protseq, 
                           NULL, NULL, NULL);
    if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, 
         Protseq, -1, L"ncacn_np", -1) == CSTR_EQUAL)
        *ValidClient = TRUE;
    }
  }
}

基本上,只有当调用者使用命名管道传输并且管道不是本地打开的(即命名管道是通过 SMB 打开的)时,ValidClient 参数才会设置为 TRUE。这基本上就是所有被检查的安全措施。因此,唯一可以强制执行的安全措施受限于谁被允许连接到合适的命名管道端点。

至少,LSASS 注册了 \pipe\lsass 命名管道端点。当它在 lsasrv.dll 中设置时,为命名管道定义了一个 SD,授予以下用户访问权限:

  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • BUILTIN\Administrators

因此,理论上匿名用户有权访问管道,并且接口定义中没有其他安全检查。现在,通常默认情况下不允许通过 NULL 会话对命名管道进行匿名访问,但是域控制器通过配置的“网络访问:可以匿名访问的命名管道”安全选项对此策略有例外。对于 DC,这允许匿名访问 lsarpc、samr 和 netlogon 管道,这些都是 lsass 管道的别名。

现在您可以理解为什么 EFS RPC 服务器在 DC 上可以匿名访问了。另一个 EFS RPC 服务器如何阻止访问?在那种情况下,它指定了一个接口 SD,以限制仅对 Everyone 组和 BUILTIN\Administrators 的访问。默认情况下,匿名用户不是 Everyone 的成员(尽管可以这样配置),因此即使您通过 lsass 管道连接,也会阻止访问。

修复措施

微软做了什么来修复 PetitPotam?他们肯定没有做的一件事是更改接口注册或命名管道端点安全性。相反,他们在 EfsRpcOpenFileRaw 中添加了一个额外的临时检查。具体来说,他们添加了以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DWORD AllowOpenRawDL = 0;
RegGetValueW(
  HKEY_LOCAL_MACHINE,
  L"SYSTEM\\CurrentControlSet\\Services\\EFS",
  L"AllowOpenRawDL",
  RRF_RT_REG_DWORD | RRF_ZEROONFAILURE,
  NULL,
  &AllowOpenRawDL);
if (AllowOpenRawDL == 1 && 
     !EfsRpcpValidateClientCall(hBinding, &ValidClient) && ValidClient) {
  // 允许调用。
}

基本上,除非 AllowOpenRawDL 注册表值设置为 1,否则无论身份验证客户端如何,调用都会被完全阻止。这似乎是一个完全有效的修复,除了 EfsRpcOpenFileRaw 不是唯一可用于启动 NTLM 身份验证会话的函数。正如 Lee Christensen 指出的,您也可以通过 EfsRpcEncryptFileSrvEfsRpcQueryUsersOnFile 或其他函数来实现。因此,由于没有实施其他更改,这些其他函数也可以像原始函数一样未经身份验证访问。

真的不清楚微软为什么没有看到这一点,但我想他们可能被他们实际修复了某些东西所蒙蔽,而他们曾坚持认为这是系统管理员必须处理的配置问题。

更新 2021/08/17: 值得注意的是,虽然您可以未经身份验证访问其他函数,但任何网络访问似乎都是使用“已身份验证”的调用者(即 ANONYMOUS 用户)完成的,因此可能没有那么有用。本文的重点不是关于滥用 EFSRPC,而是为什么它可以被滥用 :-)

无论如何,我希望这解释了为什么 PetitPotam 可以未经身份验证工作(感谢 topotam77 的发现),并可能让您了解如何确定哪些 RPC 服务器将来可能可访问。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计