防止客户端模拟消息欺骗服务器,PrintSpoofer:滥用 Windows 10 和 Server 2019 上的模拟特权...

概述

在过去的几年中,类似于 RottenPotato、RottenPotatoNG 或 Juicy Potato 这样的一些 Windows 特权模拟工具,已经在攻防安全社区中非常流行。但是,随着操作系统不断的升级,其中也有意或无意地降低了在 Windows 10 和 Windows Service 2016/2019 上使用这些工具的效果。而我们本次分析的是一个全新的工具,将有助于渗透测试人员再次轻松地利用这些特权。

需要请大家注意的是,在这里我们介绍的是一种新的工具,而不是新的技术。实际上,我将配合该工具,讨论两种可以结合在一起的知名技术,从而实现从本地服务 / 网络服务到系统的特权提升。目前,可能并没有其他研究人员公开讨论过这种使用的特殊技巧。

关于模拟特权

要介绍模拟特权,我想首先引用 @decoder_it 的一句话:" 如果你具有了 SeAssignPrimaryToken 或 SeImpersonate 特权,那么你就具有了 SYSTEM 的权限 "。显然,这句话说得太过简单,但事实也并非遥不可及。

这两个特权确实非常强大,通过这两个特权,我们可以在其他用户的上下文中运行代码,甚至创建新的进程。如果我们拥有 SeImpersonatePrivilege 特权,就可以调用 CreateProcessWithToken ( ) ;如果我们拥有 SeAssignPrimaryTokenPrivilege 特权,就可以调用 CreateProcessAsUser ( ) 。

在讨论这两个特定的函数,我们首先来看看标准的 CreateProcess ( ) 函数是什么样的:

前两个参数可以让我们指定要执行的应用程序或命令行。然后,可以调整许多设置,以自定义环境和子进程的安全上下文。最后一个参数是对 PROCESS_INFORMATION 结构的引用,该函数将在成功执行后返回。其中,包含目标进程和线程的句柄。

现在,让我们来看一下 CreateProcessWithToken ( ) 和 CreateProcessAsUser ( ) :

我们看到,这两个函数与标准的 CreateProcess ( ) 函数并没有太大的区别。但是,它们都需要令牌的句柄。根据文档,hToken 必须是 " 代表用户的主要令牌的句柄 "。在文档中还写着," 要获取代表指定用户的主令牌, [ ... ] 我们可疑调用 DuplicateTokenEx 函数将模拟令牌转换为主令牌。这将允许模拟客户端的服务器应用程序创建具有客户端安全上下文的进程。"

当然,在官方文档之中,并没有告诉我们首先要如何获取这个令牌,因为获取令牌并非是这两个函数的功能。但是,文档告诉了我们应该在什么类型的场景中使用它们。这些函数允许服务器应用程序在客户端的安全上下文中创建进程。比如,对于公开 RPC/COM 接口的 Windows 服务,这确实是非常普遍的一种实现方式。当我们调用由高特权帐户运行的服务公开的 RPC 函数时,该服务就有可能调用 RpcImpersonateClient ( ) ,以在我们的安全上下文中运行某些代码,从而降低了特权提升漏洞的风险。

总而言之,只要我们拥有 SeImpersonatePrivilege 或 SeAssignPrimaryTokenPrivilege 特权,就可以在另一个用户的安全上下文中创建一个进程。不过,我们需要的是该用户的令牌。但问题在于——如何使用自定义服务器应用程序来捕获到此类令牌呢?

使用命名管道模拟用户

Potato 家族的漏洞利用工具都是基于相同的思想——将网络身份验证从回环 TCP 终端中继到 NTLM 协商程序。为了完成这一任务,工具利用 IStorage COM 接口的某些特殊功能,使 NT AUTHORITYSYSTEM 帐户连接,并对其控制的 RPC 服务器进行身份验证。

在身份验证过程中,所有消息都会在客户端(这里是 SYSTEM 帐户)与本地 NTLM 协商程序之间中继。这个协商器只是几个 Windows API 调用(例如:AcquireCredentialsHandle ( ) 和 AcceptSecurityContext ( ) )的组合,它们通过 ALPC 与 lsass 进程进行交互。最后,如果一切顺利,我们将获得原本需要的 SYSTEM 令牌。

遗憾的是,由于某些核心的更改,该技术目前不再适用于 Windows 10 操作系统,因为现在仅在 TCP/135 端口上允许从目标服务到 "Storage" 的基础 COM 连接。

我们前面提到过的 @decoder_it 曾发表过一篇文章,在文章中表示,实际上可以绕过该限制,但是得到的令牌不能用于模拟。

现在,我们来盘点一下有哪些替代方案?RPC 并不是在这种中继方案中唯一可以使用的协议,但是我们将不做过多的展开。相反,我们想讨论一种涉及管道的古老技术。正如我在前言中所述,我希望能按照自己的方式来介绍事务,即使大多数人都觉得这些点已经掌握了,但实际上还是能从中掌握到一些基本知识。

根据官方文档记载," 管道是用于通信的进程的共享内存中的一部分。管道服务器是创建管道的进程,而管道客户端则是连接到管道的进程。一个进程负责将信息写入到管道,然后另一个进程从管道读取信息。"

换句话说,管道是在 Windows 上实现进程间通信(IPC)的众多方式之一,其他方式还有 RPC、COM 或者套接字。

管道可以是两种类型之一:

1、匿名管道:匿名管道通常在父进程和子进程之间传输数据。它们通常用于在子进程与其父进程之间重定向标准的输入和输出。

2、命名管道:命名管道可以在不相关的进程之间传输数据,前提是管道的权限允许对客户端进程具有适当的访问权限。

在第一部分中,我提到了 RpcImpersonateClient ( ) 函数。RPC 服务器可以使用它来模拟 RPC 客户端。事实证明,命名管道与 ImpersonateNamedPipeClient ( ) 函数具有相同的功能。所以接下来,我们就首先对命名管道进行一些模拟尝试。

也许上面所解释的内容过于理论化了,因此我们需要一个具体的示例。我们以如下代码为例:

HANDLE hPipe = INVALID_HANDLE_VALUE; LPWSTR pwszPipeName = argv [ 1 ] ; SECURITY_DESCRIPTOR sd = { 0 }; SECURITY_ATTRIBUTES sa = { 0 }; HANDLE hToken = INVALID_HANDLE_VALUE; if ( !InitializeSecurityDescriptor ( &sd, SECURITY_DESCRIPTOR_REVISION ) ) { wprintf ( L"InitializeSecurityDescriptor ( ) failed. Error: %d - ", GetLastError ( ) ) ; PrintLastErrorAsText ( GetLastError ( ) ) ; return -1; } if ( !ConvertStringSecurityDescriptorToSecurityDescriptor ( L"D: ( A;OICI;GA;;;WD ) ", SDDL_REVISION_1, & ( ( &sa ) ->lpSecurityDescriptor ) , NULL ) ) { wprintf ( L"ConvertStringSecurityDescriptorToSecurityDescriptor ( ) failed. Error: %d - ", GetLastError ( ) ) ; PrintLastErrorAsText ( GetLastError ( ) ) ; return -1; } if ( ( hPipe = CreateNamedPipe ( pwszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa ) ) != INVALID_HANDLE_VALUE ) { wprintf ( L" [ * ] Named pipe '%ls' listening...n", pwszPipeName ) ; ConnectNamedPipe ( hPipe, NULL ) ; wprintf ( L" [ + ] A client connected!n" ) ; if ( ImpersonateNamedPipeClient ( hPipe ) ) { if ( OpenThreadToken ( GetCurrentThread ( ) , TOKEN_ALL_ACCESS, FALSE, &hToken ) ) { PrintTokenUserSidAndName ( hToken ) ; PrintTokenImpersonationLevel ( hToken ) ; PrintTokenType ( hToken ) ; DoSomethingAsImpersonatedUser ( ) ; CloseHandle ( hToken ) ; } else { wprintf ( L"OpenThreadToken ( ) failed. Error = %d - ", GetLastError ( ) ) ; PrintLastErrorAsText ( GetLastError ( ) ) ; } } else { wprintf ( L"ImpersonateNamedPipeClient ( ) failed. Error = %d - ", GetLastError ( ) ) ; PrintLastErrorAsText ( GetLastError ( ) ) ; } CloseHandle ( hPipe ) ; } else { wprintf ( L"CreateNamedPipe ( ) failed. Error: %d - ", GetLastError ( ) ) ; PrintLastErrorAsText ( GetLastError ( ) ) ; }

前两个函数调用的作用是创建自定义安全描述符,这些描述符将应用于管道之中。这些函数并不是特定于管道而言的,它们在模拟中不会起作用,但我们必须要先提到它们。如同文件或注册表项一样,管道是可保护的对象。这意味着,如果我们没有在创建的命名管道上设置适当的权限,那么使用其他身份运行的客户端可能将根本无法访问它。在这里,我通过授予 Everyone 对管道的通用访问权限的方式,来得到一种简单的方法。

下面是通过命名管道模拟客户端的过程中所需要的函数:

1、CreateNamedPipe ( ) :这个函数名称就足以说明一切。作为服务器应用程序,该函数允许我们创建名称格式为 \.pipePIPE_NAME 的命名管道。

2、ConnectNamedPipe ( ) :创建管道后,该函数用于接受连接。除非指定了另外的其他参数,否则默认情况下该调用是同步的,因此线程将保持暂停,直至客户端连接为止。

3、ImpersonateNamedPipeClient ( ) :这里,就是发生奇迹的地方!

当然,某些规则适用于使用最后一个函数的场景。根据文档中的说明,下面是其中两种可以允许冒充的情况:

1、经过身份验证后,其身份与调用方相同。换句话说,我们自己可以冒充自己。但出乎意料的是,在某些漏洞利用场景中,这种情况实际上是有用的。

2、调用方拥有 SeImpersonatePrivilege 特权。而这一种,就是我们目前的情况。

在执行代码之前,我还做了最后一件事。我实现了一些函数,这些函数可以打印有关客户端令牌的一些信息,并且我还实现了一个名为 DoSomethingAsImpersonatedUser ( ) 的函数,该函数的目的是检查我们是否可以在客户端上下文中实际执行代码。在这篇文章的最后,我们将涉及这一部分内容。

PrintTokenUserSidAndName ( hToken ) ; PrintTokenImpersonationLevel ( hToken ) ; PrintTokenType ( hToken ) ; DoSomethingAsImpersonatedUser ( ) ;

接下来,我们可以行动了。由于默认情况下管理员都具有 SeImpersonatePrivilege 特权,因此以本地管理员身份启动服务器应用程序后,我就使用普通帐户,尝试写入命名管道。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值