PrintSpoofer:滥用Windows 10和Server 2019上的模拟特权
2020-05-31 11:20:00 Author: www.4hou.com(查看原文) 阅读量:423 收藏

概述

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

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

关于模拟特权

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

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

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

1.png

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

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

2.png

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

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

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

使用命名管道模拟用户

Potato家族的漏洞利用工具都是基于相同的思想——将网络身份验证从回环TCP终端中继到NTLM协商程序。为了完成这一任务,工具利用IStorage COM接口的某些特殊功能,使NT AUTHORITY\SYSTEM帐户连接,并对其控制的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():这个函数名称就足以说明一切。作为服务器应用程序,该函数允许我们创建名称格式为\\.\pipe\PIPE_NAME的命名管道。

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

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

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

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

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

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

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

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

3.gif

在客户端连接之后,我们将得到模拟级别为2的模拟令牌,即SecurityImpersonation。另外,DoSomethingAsImpersonatedUser()也成功返回了,这意味着我们可以在这个客户端的安全上下文中运行任意代码。

在这里,也许大家注意到,我使用的路径是\\localhost\pipe\foo123,而不是\\.\pipe\foo123,这是管道的真实名称。为了模拟成功,服务器必须首先从管道读取数据。如果客户端使用\\.\pipe\foo123作为管道的打开路径,则不会写入任何数据,并且ImpersonateNamedPipeClient()将会失败。另一方面,如果客户端使用\\HOSTNAME\pipe\foo123打开管道,则ImpersonateNamedPipeClient()将会成功。上述结果是经过我们尝试获得的结论,其根本原因目前尚不清楚。

4.png

总而言之,我们现在知道,为了在另一个用户的上下文中创建进程,我们需要有一个令牌。然后,我们看到,由于服务器应用程序利用了命名管道来模拟,因此就可以得到该令牌。到目前为止,这是一个常识,但问题在于——如何欺骗NT AUTHORITY\SYSTEM帐户连接到我们的命名管道呢?

获取SYSTEM Token

去年年底,@decoder_it发表了一篇文章,标题为《看着像土豆的豆子——如何从服务帐户提升到SYSTEM》,在该文章中,作者演示了如何利用后台智能传输服务(BITS)在本地NTLM中继方案中获取SYSTEM令牌,该方案与Potato漏洞利用中使用的技术非常相似。@decoder_it和@splinter_code在名为RogueWinRM的工具中实现了这个技术。

尽管该方法是完全有效的,但它具有明显的缺点。它依赖于BITS在本地TCP/5985端口(默认的WinRM端口)上执行的WinRM请求。如果该端口可用,则可以创建一个恶意WinRM服务器,该服务器将回复此请求,从而捕获SYSTEM帐户的凭据。尽管WinRM服务通常在工作站上处于停止状态,但在服务器实例上却是完全相反的,因此在这种情况下将无法利用。

当这项研究的结果和相关PoC出来时,我也在寻找一种实现相同目标的通用方法——通过本地NTLM中继捕获SYSTEM令牌。尽管这并不是我的首要任务,但我确实找到了类似的技巧,但最终我们发现的技巧也具有着相同的局限性。它并不适用于大多数Windows Server安装,因此我就将其暂时搁置了。然后,在几个月后的一次聊天中,@jonaslyk给了我一个新的思路——利用打印机漏洞。

Lee Christensen(@tifkin_)利用打印机的漏洞,编写了一个名为SpoolSample的工具。根据GitHub上该工具的描述,其目的是“强制Windows主机通过MS-RPRN RPC接口向其他计算机进行身份验证”。该工具背后的思想是通过欺骗域控制器连接回配置有不受约束委派的系统,从而提供一种简单有效的机制以实现AD域环境的漏洞利用。基于这个简单的概念,攻击者可以选择两种方式进行攻击。

该漏洞利用基于对Print Spooler服务公开函数的单独RPC调用。

DWORD RpcRemoteFindFirstPrinterChangeNotificationEx(
    /* [in] */ PRINTER_HANDLE hPrinter,
    /* [in] */ DWORD fdwFlags,
    /* [in] */ DWORD fdwOptions,
    /* [unique][string][in] */ wchar_t *pszLocalMachine,
    /* [in] */ DWORD dwPrinterLocal,
    /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)

根据文档,上述函数创建一个远程更改通知对象,该对象监视对打印机对象的更改,并使用RpcRouterReplyPrinter或RpcRouterReplyPrinterEx将更改发送到打印客户端。

那么,这些通知将如何发送给客户端呢?实际上,是通过命名管道上的RPC。我们发现,Print Spooler服务的RPC结构是通过命名管道公开的(\\.\pipe\spoolss)。

5.png

既然如此,我们来尝试一下Lee Christensen提供的PoC。

6.png

该工具最初需要我们指定两个服务器名称——一个是用于连接(域控制器)的名称,另一个是用于捕获身份验证的服务器名称,由我们来控制。在这里,我们需要连接到本地计算机,并且还要在本地计算机上接收通知。但问题在于,如果这么做,通知将会发送到\\DESKTOP-RTFONKM\pipe\spoolss。该管道由NT AUTHORITY\SYSTEM控制,我们无法创建自己的同名管道,这样没有任何意义。另一方面,如果我们指定任意路径并附加任意字符串,则调用将会在路径验证检查时出现问题。

不过,@jonaslyk与我分享了另一个技巧。如果主机名包含/,它实际上将能够通过路径验证检查,但是在计算要连接的命名管道的路径时,规范化会将其转换为\。这样一来,我们就可以部分控制服务器所使用的路径。

7.gif

如上图所示,服务正在使用的最终路径现在就变为了\\DESKTOP-RTFONKM\foo123\pipe\spoolss。当然,这不是命名管道的有效路径,但如果稍作调整,我们就可以使其成为有效路径。如果我们在RPC调用中指定\\DESKTOP-RTFONKM/pipe/foo123值,那么该服务会将其转换为\\DESKTOP-RTFONKM\pipe\foo123\pipe\spoolss,而这是非常有用的!

在我们的服务器应用程序上,可以快速进行测试验证。下面的截图表明我们成功建立了连接,并且可以成功模拟NT AUTHORITY\SYSTEM。

8.png

使用一个名为PrintSpoofer的工具,我们成功实现了这一技巧。但是还有一个前提条件,就是需要SeImpersonatePrivilege的特权。我已经在Windows 8.1、Windows Service 2012 R2、Windows 10和Windows Server 2019的默认版本上成功进行了测试。在某些情况下,在Windows的较早版本上可能也可以正常工作。

下面的截图中展示了在实际场景中该工具的执行情况。在Windows Server 2019上会打开一个Shell,作为CDPSvc服务的子进程。这个示例非常值得关注,因为该服务是以NT AUTHORITY\LOCAL SERVICE的身份运行,只有两个特权——SeChangeNotifyPrivilege和SeImpersonatePrivilege。

9.png

如何防范命名管道模拟

首先,命名管道模拟是可以采取防范措施的。我们可以指定禁止模拟,或者禁止服务器在安全上下文中运行代码。实际上,我在上一篇文章中已经提及过一个相关的实现,该保护由Microsoft实施,用于修复这个“漏洞”。

在进一步说明前,我们需要一个虚拟客户端应用程序与命名管道服务器进行通信,这将更好地帮助我说明后续的内容。命名管道是文件系统的一部分,那么我们如何连接到管道呢?答案是——可以通过简单地CreateFile()函数调用。

HANDLE hFile = CreateFile(
    argv[1],                        // pipe name
    GENERIC_READ | GENERIC_WRITE,   // read and write access
    0,                              // no sharing
    NULL,                           // default security attributes
    OPEN_EXISTING,                  // opens existing pipe
    0,                              // default attributes
    NULL                            // no template file
);
 
if (hFile != INVALID_HANDLE_VALUE) {
    wprintf(L"[+] CreateFile() OK\n");
    CloseHandle(hFile);
} else {
    wprintf(L"[-] CreateFile() failed. Error: %d - ", GetLastError());
}

如果运行此代码,就可以看到我们在命名管道上获得了连接,并且客户端已经成功模拟。毫无疑问,因为我使用默认值调用了CreateFile()。

10.png

但是,在CreateFile()函数的文档中,我们可以看到有很多属性可以被指定。特别是,如果设置了SECURITY_SQOS_PRESENT标志,我们就可以控制令牌的模拟级别。

11.png

因此,在虚拟客户端应用程序的源代码中,我修改了如下的CreateFile()函数调用。其SECURITY_SQOS_PRESENT和SECURITY_IDENTIFICATION值现在被指定为是dwFlagsAndAttributes参数的一部分。

HANDLE hFile = CreateFile(
    argv[1],                        // pipe name
    GENERIC_READ | GENERIC_WRITE,   // read and write access
    0,                              // no sharing
    NULL,                           // default security attributes
    OPEN_EXISTING,                  // opens existing pipe
    SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, // impersonation level: SecurityIdentification
    NULL                            // no template file
);

12.png

我们仍然能够获得有关令牌的一些信息,但是这一次,如果我们尝试在客户端的安全上下文中执行代码,就会返回错误:未提供所需的模拟级别,或者提供的模拟级别无效。确实,令牌的模拟级别现在为SecurityIdentification,这可以防止我们的恶意服务器应用程序完全模拟客户端。

尽管这样,但其中仍然存在一些理论上的问题,我之前说过,Microsoft之所以实施这个保护措施,是为了修复漏洞。在上一篇文章中,我讨论了服务跟踪功能中存在的漏洞,而该功能可以让我们通过在HKLM配置单元中编辑注册表项的方式,收集有关特定服务的某些调试信息。任何经过身份验证的用户都可以在FileDirectory值中指定日志文件的目标文件夹。例如,如果指定C:\test,则调试后的程序将写入到C:\test\MODULE.log,并且该操作在目标应用程序或服务的安全上下文中执行。

由于我们可以控制文件路径,因此我们也可以直接将管道名称用作目标目录的路径,而这就导致了CVE-2010-2554(MS10-059)漏洞的存在。

@cesarcer已经将该漏洞报告给Microsoft,他在名为Chimichurri的工具中实现了这一漏洞利用。我暂时没有找到源代码,但是我们可以在这个存储库中找到这一工具。这样一来,将会使NT AUTHORITY\SYSTEM运行的服务连接到恶意命名管道,从而捕获其令牌。只要我们拥有SeImpersonatePrivilege,就可以利用这种方法。

接下来,我们来尝试在Windows 10上进行同样的操作会发生什么:

13.png

尽管我们已经拥有了SeImpersonatePrivilege特权,但是当我们尝试在SYSTEM帐户的上下文执行代码时,会得到完全相同的报错。我们查看rtutils.dll中用于打开日志文件的CreateFile()调用,可以看到以下内容:

14.png

十六进制值0x110080实际上是SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION | FILE_ATTRIBUTE_NORMAL。

需要特别说明的是,这种防护方式并不是绝对安全的,只会使攻击者的攻击过程变得更加困难。

最终,Microsoft接收了这一漏洞,并为其分配了CVE编号,甚至还发布了详细的安全公告。但时过境迁,如今,如果我们尝试报告这样的漏洞,Microsoft会答复说,通过利用模拟特权来提升特权是一种预期的行为。他们可能会觉得,这是一场无法取胜的战斗,但事实并非如此。正如James Forshaw曾经在Twitter上所说的:“他们认为,如果你具有模拟特权,那么你也可能会是SYSTEM。他们可能会使用户(攻击者)更难以获得合适的Token,但这就像一场猫鼠游戏,因为总有其他地方可以让我们来利用。”

总结

在这篇文章中,我说明了如何在Windows 10上利用模拟特权来在SYSTEM帐户的上下文中执行代码。有很多作为本地/网络服务运行的Windows服务都具有这些功能。不过,有的时候也没有。在这种情况下,我们仍然可以使用FullPowers工具,或者按照James Forshaw的方法来得到模拟特权。

最后一点,我想对@jonaslyk表示特别感谢。在过去的几周中,我有幸多次与他聊天,我不得不说,他一直乐于分享和解释一些很棒的技巧和窍门。这些对话成为了我们集思广益的来源,转换为了非常富有成效的结果。

相关资源

[1] GitHub - itm4n / PrintSpoofer

[2] Decoder:看着像土豆的豆子——如何从服务帐户提升到SYSTEM

[3] GitHub - antonioCoco / RogueWinRM(从服务帐户到系统的Windows本地特权提升)

[4] GitHub - leechristensen / SpoolSample

[5] Tyranid's Lair - Sharing a Logon Session a Little Too Much

本文翻译自:https://itm4n.github.io/printspoofer-abusing-impersonate-privileges/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/1QG0
如有侵权请联系:admin#unsafe.sh